Skip to content

Commit

Permalink
perf(geodata): Cache metadata object in OGC API Features clients for …
Browse files Browse the repository at this point in the history
…short periods #181
  • Loading branch information
navispatial committed Aug 2, 2023
1 parent c5648f1 commit 5b1927c
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 158 deletions.
1 change: 1 addition & 0 deletions dart/geodata/CHANGELOG.md
Expand Up @@ -13,6 +13,7 @@ NOTE: Version 0.12.0 currently [under development](https://github.com/navibyte/g
* [Add support for API definition (like Open API 3.0) when accessing OGC API Features clients #170](https://github.com/navibyte/geospatial/issues/170)
* [Map HTTP status codes to feature service exception (OGC API Features) #68](https://github.com/navibyte/geospatial/issues/68)
* [Add initial support for OGC API - Features - Part 3: Filtering #180](https://github.com/navibyte/geospatial/issues/180)
* [Cache metadata object in OGC API Features clients for short periods #181](https://github.com/navibyte/geospatial/issues/181)

🛠 Maintenance:
* Removed extra internal export files and made internal imports more excplicit.
Expand Down
Expand Up @@ -27,8 +27,12 @@ class GeoJSONFeatures {
/// The required [location] should refer to a web resource containing GeoJSON
/// compliant data.
///
/// When given the optional [client] is used for http requests, otherwise the
/// default client of the `package:http/http.dart` package is used.
/// When given an optional [client] is used for http requests, otherwise the
/// default client of the `package:http/http.dart` package is used (a new
/// instance of default client for each service request). When [client] is
/// given, this allows a client to better maintain persistent connections to a
/// service, but it's also responsibility of a caller to close it
/// appropriately.
///
/// When given [headers] are injected to http requests (however some can be
/// overridden by the feature source implementation).
Expand Down
112 changes: 61 additions & 51 deletions dart/geodata/lib/src/ogcapi_common/service/client/ogc_client.dart
Expand Up @@ -12,6 +12,7 @@ import '/src/common/service/service_exception.dart';
import '/src/core/api/open_api_document.dart';
import '/src/ogcapi_common/model/ogc_service.dart';
import '/src/ogcapi_common/model/ogc_service_meta.dart';
import '/src/utils/cached_object.dart';
import '/src/utils/feature_http_adapter.dart';
import '/src/utils/resolve_api_call.dart';

Expand All @@ -33,80 +34,89 @@ const _expectJSONOpenAPI = [
@internal
abstract class OGCClientHttp implements OGCService {
/// Create a http client with HTTP(S) [endpoint] and [adapter].
const OGCClientHttp(
OGCClientHttp(
this.endpoint, {
required this.adapter,
});
this.metaMaxAge = const Duration(minutes: 15),
}) : _cachedMeta = CachedObject(metaMaxAge);

/// The endpoint for this client.
final Uri endpoint;

/// An adapter used to access HTTP(S) resource.
final FeatureHttpAdapter adapter;

/// The max age to cache metadata objects retrieved from a service and that
/// are cached internally (in-memory) by this client.
final Duration metaMaxAge;

final CachedObject<OGCServiceMeta> _cachedMeta;

@override
Future<OGCServiceMeta> meta() async {
// fetch data as JSON Object, and parse meta data
return adapter.getEntityFromJsonObject(
endpoint,
toEntity: (data, _) {
final links = Links.fromJson(data['links'] as Iterable<dynamic>);
return _OGCServiceMetaImpl(
service: this,
title: data['title'] as String? ??
links.self().first.title ??
'An OGC API service',
links: links,
description: data['description'] as String?,
attribution: data['attribution'] as String?,
Future<OGCServiceMeta> meta() => _cachedMeta.getAsync(() {
// fetch data as JSON Object, and parse meta data
return adapter.getEntityFromJsonObject(
endpoint,
toEntity: (data, _) {
final links = Links.fromJson(data['links'] as Iterable<dynamic>);
return _OGCServiceMetaImpl(
service: this,
title: data['title'] as String? ??
links.self().first.title ??
'An OGC API service',
links: links,
description: data['description'] as String?,
attribution: data['attribution'] as String?,
);
},
);
},
);
}
});
}

class _OGCServiceMetaImpl extends OGCServiceMeta {
final OGCClientHttp service;

const _OGCServiceMetaImpl({
final CachedObject<OpenAPIDocument> _cachedAPI;

_OGCServiceMetaImpl({
required this.service,
required super.title,
super.description,
super.attribution,
required super.links,
});
}) : _cachedAPI = CachedObject(service.metaMaxAge);

@override
Future<OpenAPIDocument> openAPI() {
// 1. Get a link for the relation "service-desc".
// 2. Ensure it's type is "application/vnd.oai.openapi+json".
// (here we are allowing other JSON based content types too)
final link = _resolveServiceDescLink();
if (link == null) {
throw const ServiceException('No valid service-desc link.');
}
final url = resolveLinkReferenceUri(service.endpoint, link.href);

// 3. Read JSON content from a HTTP service.
// 4. Decode content received as JSON Object using the standard JSON decoder
// 5. Wrap such decoded object in an [OpenAPIDefinition] instance.
final type = link.type;
if (type != null) {
return service.adapter.getEntityFromJsonObject(
url,
headers: {'accept': type},
expect: _expectJSONOpenAPI,
toEntity: (data, _) => OpenAPIDocument(content: data),
);
} else {
return service.adapter.getEntityFromJsonObject(
url,
headers: _acceptJSONOpenAPI,
expect: _expectJSONOpenAPI,
toEntity: (data, _) => OpenAPIDocument(content: data),
);
}
}
Future<OpenAPIDocument> openAPI() => _cachedAPI.getAsync(() {
// 1. Get a link for the relation "service-desc".
// 2. Ensure it's type is "application/vnd.oai.openapi+json".
// (here we are allowing other JSON based content types too)
final link = _resolveServiceDescLink();
if (link == null) {
throw const ServiceException('No valid service-desc link.');
}
final url = resolveLinkReferenceUri(service.endpoint, link.href);

// 3. Read JSON content from a HTTP service.
// 4. Decode content received as JSON Object using standard JSON decoder
// 5. Wrap such decoded object in an [OpenAPIDefinition] instance.
final type = link.type;
if (type != null) {
return service.adapter.getEntityFromJsonObject(
url,
headers: {'accept': type},
expect: _expectJSONOpenAPI,
toEntity: (data, _) => OpenAPIDocument(content: data),
);
} else {
return service.adapter.getEntityFromJsonObject(
url,
headers: _acceptJSONOpenAPI,
expect: _expectJSONOpenAPI,
toEntity: (data, _) => OpenAPIDocument(content: data),
);
}
});

/// Resolve an url that's providing OpenAPI or JSON service description.
Link? _resolveServiceDescLink() {
Expand Down

0 comments on commit 5b1927c

Please sign in to comment.