From b4323b70465e61bc05db59da792a83d7920feda3 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Wed, 31 Jul 2024 14:25:37 +0200 Subject: [PATCH] fix context usage (#166) * fix context usage * fix not found response * remove the whitespace --- .../mintaka/context/LdContextCache.java | 366 +++---- .../domain/EntityTemporalSerializer.java | 2 +- .../fiware/mintaka/exception/ErrorType.java | 2 +- .../JacksonConversionExceptionHandler.java | 2 +- .../mintaka/exception/NotFoundException.java | 8 + .../exception/NotFoundExceptionHandler.java | 43 + .../mintaka/rest/TemporalApiController.java | 908 +++++++++--------- 7 files changed, 695 insertions(+), 636 deletions(-) create mode 100644 src/main/java/org/fiware/mintaka/exception/NotFoundException.java create mode 100644 src/main/java/org/fiware/mintaka/exception/NotFoundExceptionHandler.java diff --git a/src/main/java/org/fiware/mintaka/context/LdContextCache.java b/src/main/java/org/fiware/mintaka/context/LdContextCache.java index 2cd07ffeb..a1a6f40bb 100644 --- a/src/main/java/org/fiware/mintaka/context/LdContextCache.java +++ b/src/main/java/org/fiware/mintaka/context/LdContextCache.java @@ -39,184 +39,192 @@ @RequiredArgsConstructor public class LdContextCache { - private final ContextProperties contextProperties; - - private URL coreContextUrl; - private Object coreContext; - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - @PostConstruct - public void initDefaultContext() { - try { - coreContextUrl = new URL(contextProperties.getDefaultUrl()); - coreContext = JsonUtils.fromURLJavaNet(coreContextUrl); - } catch (IOException e) { - throw new ContextRetrievalException("Invalid core context configured.", e, contextProperties.getDefaultUrl()); - } - } - - /** - * Get context from the given url. Will be cached. - * - * @param url - url to get the context from - * @return the context - */ - @Cacheable - public Object getContextFromURL(URL url) { - try { - if (url.toURI().equals(coreContextUrl.toURI())) { - return coreContext; - } - return JsonUtils.fromURLJavaNet(url); - } catch (IOException e) { - throw new ContextRetrievalException(String.format("Was not able to retrieve context from %s.", url), e, url.toString()); - } catch (URISyntaxException uriSyntaxException) { - throw new IllegalArgumentException(String.format("Received an invalid url: %s", url), uriSyntaxException); - } - } - - /** - * Expand all given attributes with the given contexts. - * - * @param stringsToExpand - strings to be expanded - * @param contextUrls - urls of contexts to be used for expansion - * @return list of expanded attribute-ids - */ - public List expandStrings(List stringsToExpand, List contextUrls) { - Map contextMap = (Map) getContext(contextUrls); - - return stringsToExpand.stream() - .map(stringToExpand -> expandString(stringToExpand, contextMap)) - .collect(Collectors.toList()); - } - - /** - * Expand the given string with the provided contexts. - * - * @param stringToExpand - string to be expanded - * @param contextUrls - urls of contexts to be used for expansion - * @return the expanded attribute-id - */ - public String expandString(String stringToExpand, List contextUrls) { - return expandString(stringToExpand, (Map) getContext(contextUrls)); - } - - private String expandString(String stringToExpand, Map contextMap) { - String jsonLdString = getJsonLdString(stringToExpand); - try { - Map jsonLdObject = (Map) JsonUtils.fromString(jsonLdString); - jsonLdObject.put(JsonLdConsts.CONTEXT, contextMap.get(JsonLdConsts.CONTEXT)); - return getIdFromJsonLDObject(jsonLdObject); - } catch (IOException e) { - throw new StringExpansionException(String.format("Was not able expand %s.", jsonLdString), e); - } - } - - /** - * Retrieve the context as a JsonDocument - * - * @param contextURLs - either be a (URL)String, a URL or a list of urls/urlstrings. - * @return the context - */ - public Document getContextDocument(Object contextURLs) { - try { - Object context = getContext(contextURLs); - return JsonDocument.of(new ByteArrayInputStream(OBJECT_MAPPER.writeValueAsString(context).getBytes(StandardCharsets.UTF_8))); - } catch (JsonLdError | JsonProcessingException e) { - throw new IllegalArgumentException(String.format("No valid context available via %s", contextURLs), e); - } - } - - /** - * Get the context from the given object. Should either be a (URL)String, a URL or a list of urls/urlstrings. - * We use the Json-ld-java lib for retrieval, since the titanium lib is not able to combine context objects. - * - * @param contextURLs - either be a (URL)String, a URL or a list of urls/urlstrings. - * @return the context - */ - private Object getContext(Object contextURLs) { - if (contextURLs instanceof List) { - return Map.of(JsonLdConsts.CONTEXT, ((List) contextURLs).stream() - .map(this::getContext) - .map(contextMap -> ((Map) contextMap).get(JsonLdConsts.CONTEXT)) - .map(contextObject -> { - // follow potential list-contexts - if (contextObject instanceof List) { - return getContext((List) contextObject); - } else { - return contextObject; - } - }) - .flatMap(map -> ((Map) map).entrySet().stream()) - .collect(Collectors.toMap(e -> ((Map.Entry) e).getKey(), e -> ((Map.Entry) e).getValue(), (e1, e2) -> e2))); - } else if (contextURLs instanceof URL) { - return getContextFromURL((URL) contextURLs); - } else if (contextURLs instanceof String) { - return getContextFromURL((String) contextURLs); - } else if (contextURLs instanceof URI) { - return getContextFromURL(contextURLs.toString()); - } - throw new ContextRetrievalException(String.format("Did not receive a valid context: %s.", contextURLs), contextURLs.toString()); - } - - - /** - * Get the context from the given url - * - * @param urlString - string containing the url - * @return the context - */ - private Object getContextFromURL(String urlString) { - try { - return getContextFromURL(new URL(urlString)); - } catch (MalformedURLException e) { - throw new ContextRetrievalException(String.format("Was not able to convert %s to URL.", urlString), e, urlString); - } - } - - - /** - * Extract the context urls from the link header. CORE_CONTEXT will be automatically added. - * - * @param headerString - content of the link header - * @return list of context urls, will either be only the core context or the core-context + the header context - */ - public List getContextURLsFromLinkHeader(String headerString) { - - Optional linkedContextString = Optional.empty(); - - if (headerString != null && !headerString.isEmpty() && !headerString.isBlank()) { - linkedContextString = Optional.of(headerString.split(";")[0].replace("<", "").replace(">", "")); - } - - return linkedContextString - .map(lCS -> { - try { - return new URL(lCS); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Context url is invalid.", e); - } - }) - .map(url -> List.of(url, coreContextUrl)).orElse(List.of(coreContextUrl)); - } - - // extract the Id from the expanded object - private String getIdFromJsonLDObject(Map jsonLdObject) { - Map expandedObject = (Map) JsonLdProcessor.expand(jsonLdObject) - .stream() - .findFirst() - .orElseThrow(() -> new StringExpansionException(String.format("Was not able to get an expanded object for %s.", jsonLdObject))); - Set expandedKeys = expandedObject.keySet(); - if (expandedKeys.size() != 1) { - throw new StringExpansionException(String.format("Was not able to correctly expand key. Got multiple keys: %s", expandedKeys)); - } - return expandedKeys.iterator().next(); - } - - // create a json object for json-ld api to be used for extending the key. - private String getJsonLdString(String string) { - return String.format("{\"%s\":\"\"}", string); - } + private final ContextProperties contextProperties; + + private URL coreContextUrl; + private Object coreContext; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @PostConstruct + public void initDefaultContext() { + try { + coreContextUrl = new URL(contextProperties.getDefaultUrl()); + coreContext = JsonUtils.fromURLJavaNet(coreContextUrl); + } catch (IOException e) { + throw new ContextRetrievalException("Invalid core context configured.", e, contextProperties.getDefaultUrl()); + } + } + + /** + * Get context from the given url. Will be cached. + * + * @param url - url to get the context from + * @return the context + */ + @Cacheable + public Object getContextFromURL(URL url) { + try { + if (url.toURI().equals(coreContextUrl.toURI())) { + return coreContext; + } + return JsonUtils.fromURLJavaNet(url); + } catch (IOException e) { + throw new ContextRetrievalException(String.format("Was not able to retrieve context from %s.", url), e, url.toString()); + } catch (URISyntaxException uriSyntaxException) { + throw new IllegalArgumentException(String.format("Received an invalid url: %s", url), uriSyntaxException); + } + } + + /** + * Expand all given attributes with the given contexts. + * + * @param stringsToExpand - strings to be expanded + * @param contextUrls - urls of contexts to be used for expansion + * @return list of expanded attribute-ids + */ + public List expandStrings(List stringsToExpand, List contextUrls) { + Map contextMap = (Map) getContext(contextUrls); + + return stringsToExpand.stream() + .map(stringToExpand -> expandString(stringToExpand, contextMap)) + .collect(Collectors.toList()); + } + + /** + * Expand the given string with the provided contexts. + * + * @param stringToExpand - string to be expanded + * @param contextUrls - urls of contexts to be used for expansion + * @return the expanded attribute-id + */ + public String expandString(String stringToExpand, List contextUrls) { + return expandString(stringToExpand, (Map) getContext(contextUrls)); + } + + private String expandString(String stringToExpand, Map contextMap) { + String jsonLdString = getJsonLdString(stringToExpand); + try { + Map jsonLdObject = (Map) JsonUtils.fromString(jsonLdString); + jsonLdObject.put(JsonLdConsts.CONTEXT, contextMap.get(JsonLdConsts.CONTEXT)); + return getIdFromJsonLDObject(jsonLdObject); + } catch (IOException e) { + throw new StringExpansionException(String.format("Was not able expand %s.", jsonLdString), e); + } + } + + /** + * Retrieve the context as a JsonDocument + * + * @param contextURLs - either be a (URL)String, a URL or a list of urls/urlstrings. + * @return the context + */ + public Document getContextDocument(Object contextURLs) { + try { + Object context = getContext(contextURLs); + return JsonDocument.of(new ByteArrayInputStream(OBJECT_MAPPER.writeValueAsString(context).getBytes(StandardCharsets.UTF_8))); + } catch (JsonLdError | JsonProcessingException e) { + throw new IllegalArgumentException(String.format("No valid context available via %s", contextURLs), e); + } + } + + /** + * Get the context from the given object. Should either be a (URL)String, a URL or a list of urls/urlstrings. + * We use the Json-ld-java lib for retrieval, since the titanium lib is not able to combine context objects. + * + * @param contextURLs - either be a (URL)String, a URL or a list of urls/urlstrings. + * @return the context + */ + private Object getContext(Object contextURLs) { + if (contextURLs instanceof List) { + var m = Map.of(JsonLdConsts.CONTEXT, ((List) contextURLs).stream() + .map(url -> getContext(url)) + .map(contextMap -> ((Map) contextMap).get(JsonLdConsts.CONTEXT)) + .map(contextObject -> { + // follow potential list-contexts + if (contextObject instanceof List) { + return getContext((List) contextObject); + } + return contextObject; + }) + .map(co -> { + if (co instanceof Map coMap) { + if (coMap.containsKey(JsonLdConsts.CONTEXT)) { + return coMap.get(JsonLdConsts.CONTEXT); + } + } + return co; + }) + .flatMap(map -> ((Map) map).entrySet().stream()) + .collect(Collectors.toMap(e -> ((Map.Entry) e).getKey(), e -> ((Map.Entry) e).getValue(), (e1, e2) -> e2))); + return m; + } else if (contextURLs instanceof URL) { + return getContextFromURL((URL) contextURLs); + } else if (contextURLs instanceof String) { + return getContextFromURL((String) contextURLs); + } else if (contextURLs instanceof URI) { + return getContextFromURL(contextURLs.toString()); + } + throw new ContextRetrievalException(String.format("Did not receive a valid context: %s.", contextURLs), contextURLs.toString()); + } + + + /** + * Get the context from the given url + * + * @param urlString - string containing the url + * @return the context + */ + private Object getContextFromURL(String urlString) { + try { + return getContextFromURL(new URL(urlString)); + } catch (MalformedURLException e) { + throw new ContextRetrievalException(String.format("Was not able to convert %s to URL.", urlString), e, urlString); + } + } + + + /** + * Extract the context urls from the link header. CORE_CONTEXT will be automatically added. + * + * @param headerString - content of the link header + * @return list of context urls, will either be only the core context or the core-context + the header context + */ + public List getContextURLsFromLinkHeader(String headerString) { + + Optional linkedContextString = Optional.empty(); + + if (headerString != null && !headerString.isEmpty() && !headerString.isBlank()) { + linkedContextString = Optional.of(headerString.split(";")[0].replace("<", "").replace(">", "")); + } + + return linkedContextString + .map(lCS -> { + try { + return new URL(lCS); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Context url is invalid.", e); + } + }) + .map(url -> List.of(url, coreContextUrl)).orElse(List.of(coreContextUrl)); + } + + // extract the Id from the expanded object + private String getIdFromJsonLDObject(Map jsonLdObject) { + Map expandedObject = (Map) JsonLdProcessor.expand(jsonLdObject) + .stream() + .findFirst() + .orElseThrow(() -> new StringExpansionException(String.format("Was not able to get an expanded object for %s.", jsonLdObject))); + Set expandedKeys = expandedObject.keySet(); + if (expandedKeys.size() != 1) { + throw new StringExpansionException(String.format("Was not able to correctly expand key. Got multiple keys: %s", expandedKeys)); + } + return expandedKeys.iterator().next(); + } + + // create a json object for json-ld api to be used for extending the key. + private String getJsonLdString(String string) { + return String.format("{\"%s\":\"\"}", string); + } } diff --git a/src/main/java/org/fiware/mintaka/domain/EntityTemporalSerializer.java b/src/main/java/org/fiware/mintaka/domain/EntityTemporalSerializer.java index 7d06961ba..1a9a592d5 100644 --- a/src/main/java/org/fiware/mintaka/domain/EntityTemporalSerializer.java +++ b/src/main/java/org/fiware/mintaka/domain/EntityTemporalSerializer.java @@ -120,7 +120,7 @@ public void serialize(EntityTemporalVO value, JsonGenerator gen, SerializerProvi throw new JacksonConversionException("Was not able to deserialize the retrieved object.", e); } catch (JsonLdError jsonLdError) { log.error("Was not able to deserialize object", jsonLdError); - throw new JacksonConversionException(jsonLdError.getMessage()); + throw new JacksonConversionException(jsonLdError.getMessage()); } } diff --git a/src/main/java/org/fiware/mintaka/exception/ErrorType.java b/src/main/java/org/fiware/mintaka/exception/ErrorType.java index 7f143f1f5..57998de7f 100644 --- a/src/main/java/org/fiware/mintaka/exception/ErrorType.java +++ b/src/main/java/org/fiware/mintaka/exception/ErrorType.java @@ -11,7 +11,7 @@ public enum ErrorType { INVALID_REQUEST(HttpStatus.BAD_REQUEST, "https://uri.etsi.org/ngsi-ld/errors/InvalidRequest"), BAD_REQUEST_DATA(HttpStatus.BAD_REQUEST, "https://uri.etsi.org/ngsi-ld/errors/BadRequestData"), OPERATION_NOT_SUPPORTED(HttpStatus.UNPROCESSABLE_ENTITY, "https://uri.etsi.org/ngsi-ld/errors/OperationNotSupported"), - RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound "), + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound"), INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "https://uri.etsi.org/ngsi-ld/errors/InternalError"), TOO_COMPLEX_QUERY(HttpStatus.FORBIDDEN, "https://uri.etsi.org/ngsi-ld/errors/TooComplexQuery"), TOO_MANY_RESULTS(HttpStatus.FORBIDDEN, "https://uri.etsi.org/ngsi-ld/errors/TooManyResults "), diff --git a/src/main/java/org/fiware/mintaka/exception/JacksonConversionExceptionHandler.java b/src/main/java/org/fiware/mintaka/exception/JacksonConversionExceptionHandler.java index 7d1760396..800364d57 100644 --- a/src/main/java/org/fiware/mintaka/exception/JacksonConversionExceptionHandler.java +++ b/src/main/java/org/fiware/mintaka/exception/JacksonConversionExceptionHandler.java @@ -14,7 +14,7 @@ */ @Produces @Singleton -@Requires(classes = {InvalidTimeRelationException.class, ExceptionHandler.class}) +@Requires(classes = {JacksonConversionException.class, ExceptionHandler.class}) @Slf4j public class JacksonConversionExceptionHandler extends NGSICompliantExceptionHandler{ diff --git a/src/main/java/org/fiware/mintaka/exception/NotFoundException.java b/src/main/java/org/fiware/mintaka/exception/NotFoundException.java new file mode 100644 index 000000000..4be47e2c8 --- /dev/null +++ b/src/main/java/org/fiware/mintaka/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package org.fiware.mintaka.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/org/fiware/mintaka/exception/NotFoundExceptionHandler.java b/src/main/java/org/fiware/mintaka/exception/NotFoundExceptionHandler.java new file mode 100644 index 000000000..2ebedc4a1 --- /dev/null +++ b/src/main/java/org/fiware/mintaka/exception/NotFoundExceptionHandler.java @@ -0,0 +1,43 @@ +package org.fiware.mintaka.exception; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.server.exceptions.ExceptionHandler; +import lombok.extern.slf4j.Slf4j; + +import javax.inject.Singleton; + +/** + * Handle all not-found cases + */ +@Produces +@Singleton +@Requires(classes = {NotFoundException.class, ExceptionHandler.class}) +@Slf4j +public class NotFoundExceptionHandler extends NGSICompliantExceptionHandler{ + + private static final ErrorType ASSOCIATED_ERROR = ErrorType.RESOURCE_NOT_FOUND; + private static final String ERROR_TITLE = "Resource Not Found."; + + @Override + public ErrorType getAssociatedErrorType() { + return ASSOCIATED_ERROR; + } + + @Override + public HttpStatus getStatus() { + return ASSOCIATED_ERROR.getStatus(); + } + + @Override + public String getErrorTitle() { + return ERROR_TITLE; + } + + @Override + public String getInstance(HttpRequest request, NotFoundException exception) { + return null; + } +} diff --git a/src/main/java/org/fiware/mintaka/rest/TemporalApiController.java b/src/main/java/org/fiware/mintaka/rest/TemporalApiController.java index 1f1b3496f..192080a8f 100644 --- a/src/main/java/org/fiware/mintaka/rest/TemporalApiController.java +++ b/src/main/java/org/fiware/mintaka/rest/TemporalApiController.java @@ -20,9 +20,9 @@ import org.fiware.mintaka.domain.query.temporal.TimeRelation; import org.fiware.mintaka.domain.query.temporal.TimeStampType; import org.fiware.mintaka.exception.InvalidTimeRelationException; +import org.fiware.mintaka.exception.NotFoundException; import org.fiware.mintaka.exception.PersistenceRetrievalException; import org.fiware.mintaka.persistence.LimitableResult; -import org.fiware.mintaka.persistence.NgsiEntity; import org.fiware.mintaka.service.EntityTemporalService; import org.fiware.ngsi.api.TemporalRetrievalApi; import org.fiware.ngsi.model.EntityInfoVO; @@ -62,457 +62,457 @@ @RequiredArgsConstructor public class TemporalApiController implements TemporalRetrievalApi { - public static final List WELL_KNOWN_ATTRIBUTES = List.of("location", "observationSpace", "operationSpace", "unitCode"); - - private static final Integer DEFAULT_LIMIT = 100; - private static final String CONTENT_RANGE_HEADER_KEY = "Content-Range"; - private static final String DEFAULT_TIME_PROPERTY = "observedAt"; - private static final String DEFAULT_GEO_PROPERTY = "location"; - private static final String SYS_ATTRS_OPTION = "sysAttrs"; - private static final String TEMPORAL_VALUES_OPTION = "temporalValues"; - private static final String COUNT_OPTION = "count"; - private static final String LINK_HEADER_TEMPLATE = "<%s>; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\""; - private static final String NGSILD_RESULTS_COUNT_HEADER = "NGSILD-Results-Count"; - private static final String PAGE_SIZE_HEADER = "Page-Size"; - private static final String NEXT_PAGE_HEADER = "Next-Page"; - private static final String PREVIOUS_PAGE_HEADER = "Previous-Page"; - private static final String LINK_HEADER = "Link"; - - public static final String COMMA_SEPERATOR = ","; - public static final String TIMERELATION_ERROR_MSG_TEMPLATE = "The given timestamp type is not supported: %s"; - - private final EntityTemporalService entityTemporalService; - private final LdContextCache contextCache; - private final QueryParser queryParser; - private final ApiDomainMapper apiDomainMapper; - - @Override - public HttpResponse queryTemporalEntities( - @Nullable String link, - @Nullable String id, - @Nullable String idPattern, - @Nullable @Size(min = 1) String type, - @Nullable @Size(min = 1) String attrs, - @Nullable @Size(min = 1) String q, - @Nullable String georel, - @Nullable String geometry, - @Nullable String coordinates, - @Nullable @Size(min = 1) String geoproperty, - @Nullable TimerelVO timerel, - @Nullable @Pattern(regexp = "^((\\d|[a-zA-Z]|_)+(:(\\d|[a-zA-Z]|_)+)?(#\\d+)?)$") @Size(min = 1) String timeproperty, - @Nullable Instant timeAt, - @Nullable Instant endTimeAt, - @Nullable @Size(min = 1) String csf, - @Nullable Integer pageSize, - @Nullable URI pageAnchor, - @Nullable Integer limit, - @Nullable String options, - @Nullable @Min(1) Integer lastN) { - - AcceptType acceptType = getAcceptType(); - - List contextUrls = contextCache.getContextURLsFromLinkHeader(link); - String expandedGeoProperty = Optional.ofNullable(geoproperty) - .filter(property -> !WELL_KNOWN_ATTRIBUTES.contains(property)) - .map(property -> contextCache.expandString(property, contextUrls)) - .orElse(DEFAULT_GEO_PROPERTY); - TimeQuery timeQuery = new TimeQuery(apiDomainMapper.timeRelVoToTimeRelation(timerel), timeAt, endTimeAt, getTimeRelevantProperty(timeproperty), false); - - Optional> optionalIdList = Optional.ofNullable(id).map(this::getIdList); - Optional optionalIdPattern = Optional.ofNullable(idPattern); - List expandedTypes = getExpandedTypes(contextUrls, type); - Optional optionalQuery = Optional.ofNullable(q).map(queryString -> queryParser.toTerm(queryString, contextUrls)); - Optional optionalGeoQuery = getGeometryQuery(georel, geometry, coordinates, expandedGeoProperty); - - // if pagesize is null, set it to limit, even though limit might also be null. - pageSize = getPageSize(pageSize, limit); - - LimitableResult> limitableResult = entityTemporalService.getEntitiesWithQuery( - optionalIdList, - optionalIdPattern, - getExpandedTypes(contextUrls, type), - getExpandedAttributes(contextUrls, attrs), - optionalQuery, - optionalGeoQuery, - timeQuery, - lastN, - isSysAttrs(options), - isTemporalValuesOptionSet(options), - pageSize, - Optional.ofNullable(pageAnchor).map(URI::toString)); - - List entityTemporalVOS = limitableResult.getResult(); - entityTemporalVOS.forEach(entityTemporalVO -> addContextToEntityTemporalVO(entityTemporalVO, contextUrls)); - - Optional paginationInformation = Optional.empty(); - if (entityTemporalVOS.size() == pageSize) { - paginationInformation = Optional.of( - entityTemporalService - .getPaginationInfo(optionalIdList, optionalIdPattern, expandedTypes, optionalQuery, optionalGeoQuery, timeQuery, pageSize, Optional.ofNullable(pageAnchor).map(URI::toString))); - } - - EntityTemporalListVO entityTemporalListVO = new EntityTemporalListVO(); - entityTemporalListVO.addAll(entityTemporalVOS); - MutableHttpResponse mutableHttpResponse; - if (limitableResult.isLimited()) { - Range range = getRange(getTimestampListFromEntityTemporalList(entityTemporalVOS, timeQuery), timeQuery, lastN); - mutableHttpResponse = HttpResponse - .status(HttpStatus.PARTIAL_CONTENT) - .body((Object) entityTemporalListVO) - .header(CONTENT_RANGE_HEADER_KEY, getContentRange(range, lastN)); - } else { - mutableHttpResponse = HttpResponse.ok(entityTemporalListVO); - } - paginationInformation.ifPresent(pi -> { - mutableHttpResponse.header(PAGE_SIZE_HEADER, String.valueOf(pi.getPageSize())); - pi.getNextPage().ifPresent(np -> mutableHttpResponse.header(NEXT_PAGE_HEADER, np)); - pi.getPreviousPage().ifPresent(pp -> mutableHttpResponse.header(PREVIOUS_PAGE_HEADER, pp)); - }); - - if (isCountOptionSet(options)) { - Number totalCount = entityTemporalService.countMatchingEntities(optionalIdList, optionalIdPattern, expandedTypes, optionalQuery, optionalGeoQuery, timeQuery); - mutableHttpResponse.header(NGSILD_RESULTS_COUNT_HEADER, totalCount.toString()); - } - - if (acceptType == AcceptType.JSON) { - mutableHttpResponse.header(LINK_HEADER, getLinkHeader(contextUrls)); - } - return mutableHttpResponse; - } - - private Integer getPageSize(Integer pageSize, Integer limit) { - Integer requestedPageSize = Optional.ofNullable(pageSize).orElse(limit); - return Optional.ofNullable(requestedPageSize).orElse(DEFAULT_LIMIT); - } - - @Override - public HttpResponse queryTemporalEntitiesOnPost( - @NotNull QueryVO queryVO, - @Nullable String link, - @Nullable @Min(1) @Max(100) Integer pageSize, - @Nullable URI pageAnchor, - @Nullable @Min(1) @Max(100) Integer limit, - @Nullable String options, - @Nullable @Min(1) Integer lastN) { - - Optional entityInfoVO = Optional.ofNullable(queryVO.entities()); - Optional geoQueryVO = Optional.ofNullable(queryVO.geoQ()); - Optional temporalQueryVO = Optional.ofNullable(queryVO.temporalQ()); - - return queryTemporalEntities(link, - entityInfoVO.map(EntityInfoVO::getId).map(this::idToString).orElse(null), - entityInfoVO.map(EntityInfoVO::getIdPattern).orElse(null), - entityInfoVO.map(EntityInfoVO::type).orElse(null), - Optional.ofNullable(queryVO.attrs()).map(attrsList -> attrsList.stream().collect(Collectors.joining(","))).orElse(null), queryVO.q(), - geoQueryVO.map(GeoQueryVO::georel).orElse(null), - geoQueryVO.map(GeoQueryVO::geometry).orElse(null), - geoQueryVO.map(GeoQueryVO::coordinates).map(this::coordinatesToString).orElse(null), - geoQueryVO.map(GeoQueryVO::geoproperty).orElse(null), - temporalQueryVO.map(TemporalQueryVO::getTimerel).map(TimerelVO::toEnum).orElse(null), - temporalQueryVO.map(TemporalQueryVO::getTimeproperty).orElse(null), - temporalQueryVO.map(TemporalQueryVO::timeAt).orElse(null), - temporalQueryVO.map(TemporalQueryVO::endTimeAt).orElse(null), - queryVO.csf(), pageSize, pageAnchor, limit, options, lastN); - } - - private String idToString(Object id) { - if (id instanceof List) { - return ((List) id).stream().map(Object::toString).collect(Collectors.joining(",")); - } else if (id instanceof URI) { - return ((URI) id).toString(); - } - return id.toString(); - } - - private String coordinatesToString(List coordinates) { - return coordinates.stream().map(Object::toString).collect(Collectors.joining(",")); - } - - @Override - public HttpResponse retrieveEntityTemporalById( - URI entityId, - @Nullable String link, - @Nullable @Size(min = 1) String attrs, - @Nullable String options, - @Nullable TimerelVO timerel, - @Nullable @Pattern(regexp = "^((\\d|[a-zA-Z]|_)+(:(\\d|[a-zA-Z]|_)+)?(#\\d+)?)$") @Size(min = 1) String timeproperty, - @Nullable Instant timeAt, - @Nullable Instant endTimeAt, - @Nullable @Min(1) Integer lastN) { - - AcceptType acceptType = getAcceptType(); - List contextUrls = contextCache.getContextURLsFromLinkHeader(link); - TimeQuery timeQuery = new TimeQuery(apiDomainMapper.timeRelVoToTimeRelation(timerel), timeAt, endTimeAt, getTimeRelevantProperty(timeproperty)); - - Optional> optionalLimitableResult = entityTemporalService - .getNgsiEntitiesWithTimerel(entityId.toString(), - timeQuery, - getExpandedAttributes(contextUrls, attrs), - lastN, - isSysAttrs(options), - isTemporalValuesOptionSet(options)); - - if (optionalLimitableResult.isEmpty()) { - Optional optionalEntity = entityTemporalService.getNgsiEntity(entityId.toString()); - if (optionalEntity.isPresent()) { - return HttpResponse.ok(addContextToEntityTemporalVO(optionalEntity.get(), contextUrls)); - } - return HttpResponse.notFound(); - } - - MutableHttpResponse mutableHttpResponse; - LimitableResult limitableResult = optionalLimitableResult.get(); - if (limitableResult.isLimited()) { - Range range = getRange(getTimestampListFromEntityTemporal(limitableResult.getResult(), timeQuery), timeQuery, lastN); - mutableHttpResponse = HttpResponse - .status(HttpStatus.PARTIAL_CONTENT) - .body((Object) addContextToEntityTemporalVO(limitableResult.getResult(), contextUrls)) - .header(CONTENT_RANGE_HEADER_KEY, getContentRange(range, lastN)); - } else { - mutableHttpResponse = HttpResponse.ok(addContextToEntityTemporalVO(limitableResult.getResult(), contextUrls)); - } - if (acceptType == AcceptType.JSON) { - mutableHttpResponse.header("Link", getLinkHeader(contextUrls)); - } - return mutableHttpResponse; - } - - private List getIdList(String id) { - return Arrays.asList(id.split(",")); - } - - private String getLinkHeader(List contextUrls) { - // its either core or the current, since core is always the latest entry - return String.format(LINK_HEADER_TEMPLATE, contextUrls.get(0)); - } - - private String getContentRange(Range range, Integer lastN) { - String size = "*"; - if (lastN != null && lastN > 0) { - size = lastN.toString(); - } - return String.format("date-time %s-%s/%s", range.getStart(), range.getEnd(), size); - } - - private List getTimestampListFromEntityTemporalList(List entityTemporalVOS, TimeQuery timeQuery) { - return entityTemporalVOS - .stream() - .flatMap(entityTemporalVO -> getTimestampListFromEntityTemporal(entityTemporalVO, timeQuery).stream()) - .collect(Collectors.toList()); - } - - private List getTimestampListFromEntityTemporal(EntityTemporalVO entityTemporalVO, TimeQuery timeQuery) { - List timeStampList = new ArrayList<>(); - timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getObservationSpace()) - .stream() - .flatMap(List::stream) - .map(geoPropertyVO -> Optional.ofNullable(getTimestampFromGeoProperty(geoPropertyVO, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) - .collect(Collectors.toList())); - timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getOperationSpace()) - .stream() - .flatMap(List::stream) - .map(geoPropertyVO -> Optional.ofNullable(getTimestampFromGeoProperty(geoPropertyVO, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) - .collect(Collectors.toList())); - timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getLocation()) - .stream() - .flatMap(List::stream) - .map(geoPropertyVO -> Optional.ofNullable(getTimestampFromGeoProperty(geoPropertyVO, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) - .collect(Collectors.toList())); - timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getAdditionalProperties()) - .stream() - .map(Map::values) - .flatMap(Collection::stream) - .flatMap(instanceList -> ((List) instanceList).stream()) - .map(propertyObject -> Optional.ofNullable(getTimestampFromPropertyObject(propertyObject, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) - .collect(Collectors.toList())); - return timeStampList; - } - - private Range getRange(List timeStampList, TimeQuery timeQuery, Integer lastN) { - if (lastN != null) { - return getRangeWithLastN(timeStampList, timeQuery); - } else { - return getRangeWithoutLastN(timeStampList, timeQuery); - } - } - - private Range getRangeWithoutLastN(List timeStamps, TimeQuery timeQuery) { - TimeRelation timeRelation = Optional.ofNullable(timeQuery.getTimeRelation()).orElse(TimeRelation.BEFORE); - timeStamps.sort(Comparator.naturalOrder()); - - Instant startOfList = timeStamps.get(0); - Instant endOfList = timeStamps.get(timeStamps.size() - 1); - - switch (timeRelation) { - case BEFORE: - return new Range(startOfList, endOfList); - case BETWEEN: - case AFTER: - return new Range(timeQuery.getTimeAt(), endOfList); - default: - throw new InvalidTimeRelationException(String.format("Received an invalid timerelation: %s", timeRelation)); - } - } - - private Range getRangeWithLastN(List timeStamps, TimeQuery timeQuery) { - TimeRelation timeRelation = Optional.ofNullable(timeQuery.getTimeRelation()).orElse(TimeRelation.AFTER); - timeStamps.sort(Comparator.naturalOrder()); - - switch (timeRelation) { - case BETWEEN: - return new Range(timeQuery.getEndTime(), timeStamps.get(0)); - case BEFORE: - return new Range(timeQuery.getTimeAt(), timeStamps.get(0)); - case AFTER: - return new Range(timeStamps.get(timeStamps.size() - 1), timeStamps.get(0)); - default: - throw new InvalidTimeRelationException(String.format("Received an invalid timerelation: %s", timeRelation)); - } - } - - private Instant getTimestampFromPropertyObject(Object propertyObject, TimeStampType timeStampType) { - if (propertyObject instanceof RelationshipVO) { - return getTimestampFromRelationShip((RelationshipVO) propertyObject, timeStampType); - } else if (propertyObject instanceof PropertyVO) { - return getTimestampFromProperty((PropertyVO) propertyObject, timeStampType); - } else if (propertyObject instanceof GeoPropertyVO) { - return getTimestampFromGeoProperty((GeoPropertyVO) propertyObject, timeStampType); - } - throw new PersistenceRetrievalException(String.format("The given propertyObject is not valid: %s", propertyObject)); - } - - private Instant getTimestampFromProperty(PropertyVO propertyVO, TimeStampType timeStampType) { - switch (timeStampType) { - case CREATED_AT: - return propertyVO.getCreatedAt(); - case OBSERVED_AT: - return propertyVO.getObservedAt(); - case MODIFIED_AT: - case TS: - return propertyVO.getModifiedAt(); - default: - throw new InvalidTimeRelationException(String.format(TIMERELATION_ERROR_MSG_TEMPLATE, timeStampType)); - } - } - - private Instant getTimestampFromRelationShip(RelationshipVO relationshipVO, TimeStampType timeStampType) { - switch (timeStampType) { - case CREATED_AT: - return relationshipVO.getCreatedAt(); - case OBSERVED_AT: - return relationshipVO.getObservedAt(); - case MODIFIED_AT: - case TS: - return relationshipVO.getModifiedAt(); - default: - throw new InvalidTimeRelationException(String.format(TIMERELATION_ERROR_MSG_TEMPLATE, timeStampType)); - } - } - - private Instant getTimestampFromGeoProperty(GeoPropertyVO geoPropertyVO, TimeStampType timeStampType) { - switch (timeStampType) { - case CREATED_AT: - return geoPropertyVO.getCreatedAt(); - case OBSERVED_AT: - return geoPropertyVO.getObservedAt(); - case MODIFIED_AT: - case TS: - return geoPropertyVO.getModifiedAt(); - default: - throw new InvalidTimeRelationException(String.format("The given timestamp type is not supported: %s", timeStampType)); - } - } - - /** - * Add the context urls to the entities temporal represenation - */ - private EntityTemporalVO addContextToEntityTemporalVO(EntityTemporalVO entityTemporalVO, List contextUrls) { - if (contextUrls.size() > 1) { - entityTemporalVO.atContext(contextUrls); - } else { - entityTemporalVO.atContext(contextUrls.get(0)); - } - return entityTemporalVO; - } - - /** - * Expand all attributes present in the attrs parameter - * - * @param contextUrls - * @param attrs - * @return - */ - private List getExpandedAttributes(List contextUrls, String attrs) { - if (attrs == null) { - return List.of(); - } - - return Arrays.stream(attrs.split(COMMA_SEPERATOR)) - .map(attribute -> { - if (WELL_KNOWN_ATTRIBUTES.contains(attribute)) { - return attribute; - } - return contextCache.expandString(attribute, contextUrls); - }).collect(Collectors.toList()); - } - - private List getExpandedTypes(List contextUrls, String types) { - return Optional.ofNullable(types) - .map(al -> contextCache.expandStrings(Arrays.asList(types.split(COMMA_SEPERATOR)), contextUrls)) - .orElse(List.of()); - } - - /** - * Get the timeProperty string or the default property if null - * - * @param timeProperty timeProperty retrieved through the api - * @return timeProperty to be used - */ - private String getTimeRelevantProperty(String timeProperty) { - return Optional.ofNullable(timeProperty).orElse(DEFAULT_TIME_PROPERTY); - } - - private boolean isSysAttrs(String options) { - if (options == null) { - return false; - } - return Arrays.asList(options.split(COMMA_SEPERATOR)).contains(SYS_ATTRS_OPTION); - } - - private boolean isTemporalValuesOptionSet(String options) { - Optional optionalOptions = Optional.ofNullable(options); - if (optionalOptions.isEmpty()) { - return false; - } - return Arrays.asList(options.split(COMMA_SEPERATOR)).contains(TEMPORAL_VALUES_OPTION); - } - - private boolean isCountOptionSet(String options) { - Optional optionalOptions = Optional.ofNullable(options); - if (optionalOptions.isEmpty()) { - return false; - } - return Arrays.asList(options.split(COMMA_SEPERATOR)).contains(COUNT_OPTION); - } - - private Optional getGeometryQuery(String georel, String geometry, String coordinates, String geoproperty) { - if (georel == null && coordinates == null && geometry == null) { - return Optional.empty(); - } - - if (georel == null || coordinates == null || geometry == null) { - throw new IllegalArgumentException( - String.format("When querying for geoRelations, all 3 parameters(georel: %s, coordinates: %s, geometry: %s) need to be present.", georel, coordinates, geometry)); - } - - return Optional.of(new GeoQuery(georel, Geometry.byName(geometry), coordinates, geoproperty)); - } - - private AcceptType getAcceptType() { - return ServerRequestContext.currentRequest() - .map(HttpRequest::getHeaders) - .map(headers -> headers.get("Accept")) - .map(AcceptType::getEnum) - .orElse(AcceptType.JSON); - } + public static final List WELL_KNOWN_ATTRIBUTES = List.of("location", "observationSpace", "operationSpace", "unitCode"); + + private static final Integer DEFAULT_LIMIT = 100; + private static final String CONTENT_RANGE_HEADER_KEY = "Content-Range"; + private static final String DEFAULT_TIME_PROPERTY = "observedAt"; + private static final String DEFAULT_GEO_PROPERTY = "location"; + private static final String SYS_ATTRS_OPTION = "sysAttrs"; + private static final String TEMPORAL_VALUES_OPTION = "temporalValues"; + private static final String COUNT_OPTION = "count"; + private static final String LINK_HEADER_TEMPLATE = "<%s>; rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\""; + private static final String NGSILD_RESULTS_COUNT_HEADER = "NGSILD-Results-Count"; + private static final String PAGE_SIZE_HEADER = "Page-Size"; + private static final String NEXT_PAGE_HEADER = "Next-Page"; + private static final String PREVIOUS_PAGE_HEADER = "Previous-Page"; + private static final String LINK_HEADER = "Link"; + + public static final String COMMA_SEPERATOR = ","; + public static final String TIMERELATION_ERROR_MSG_TEMPLATE = "The given timestamp type is not supported: %s"; + + private final EntityTemporalService entityTemporalService; + private final LdContextCache contextCache; + private final QueryParser queryParser; + private final ApiDomainMapper apiDomainMapper; + + @Override + public HttpResponse queryTemporalEntities( + @Nullable String link, + @Nullable String id, + @Nullable String idPattern, + @Nullable @Size(min = 1) String type, + @Nullable @Size(min = 1) String attrs, + @Nullable @Size(min = 1) String q, + @Nullable String georel, + @Nullable String geometry, + @Nullable String coordinates, + @Nullable @Size(min = 1) String geoproperty, + @Nullable TimerelVO timerel, + @Nullable @Pattern(regexp = "^((\\d|[a-zA-Z]|_)+(:(\\d|[a-zA-Z]|_)+)?(#\\d+)?)$") @Size(min = 1) String timeproperty, + @Nullable Instant timeAt, + @Nullable Instant endTimeAt, + @Nullable @Size(min = 1) String csf, + @Nullable Integer pageSize, + @Nullable URI pageAnchor, + @Nullable Integer limit, + @Nullable String options, + @Nullable @Min(1) Integer lastN) { + + AcceptType acceptType = getAcceptType(); + + List contextUrls = contextCache.getContextURLsFromLinkHeader(link); + String expandedGeoProperty = Optional.ofNullable(geoproperty) + .filter(property -> !WELL_KNOWN_ATTRIBUTES.contains(property)) + .map(property -> contextCache.expandString(property, contextUrls)) + .orElse(DEFAULT_GEO_PROPERTY); + TimeQuery timeQuery = new TimeQuery(apiDomainMapper.timeRelVoToTimeRelation(timerel), timeAt, endTimeAt, getTimeRelevantProperty(timeproperty), false); + + Optional> optionalIdList = Optional.ofNullable(id).map(this::getIdList); + Optional optionalIdPattern = Optional.ofNullable(idPattern); + List expandedTypes = getExpandedTypes(contextUrls, type); + Optional optionalQuery = Optional.ofNullable(q).map(queryString -> queryParser.toTerm(queryString, contextUrls)); + Optional optionalGeoQuery = getGeometryQuery(georel, geometry, coordinates, expandedGeoProperty); + + // if pagesize is null, set it to limit, even though limit might also be null. + pageSize = getPageSize(pageSize, limit); + + LimitableResult> limitableResult = entityTemporalService.getEntitiesWithQuery( + optionalIdList, + optionalIdPattern, + getExpandedTypes(contextUrls, type), + getExpandedAttributes(contextUrls, attrs), + optionalQuery, + optionalGeoQuery, + timeQuery, + lastN, + isSysAttrs(options), + isTemporalValuesOptionSet(options), + pageSize, + Optional.ofNullable(pageAnchor).map(URI::toString)); + + List entityTemporalVOS = limitableResult.getResult(); + entityTemporalVOS.forEach(entityTemporalVO -> addContextToEntityTemporalVO(entityTemporalVO, contextUrls)); + + Optional paginationInformation = Optional.empty(); + if (entityTemporalVOS.size() == pageSize) { + paginationInformation = Optional.of( + entityTemporalService + .getPaginationInfo(optionalIdList, optionalIdPattern, expandedTypes, optionalQuery, optionalGeoQuery, timeQuery, pageSize, Optional.ofNullable(pageAnchor).map(URI::toString))); + } + + EntityTemporalListVO entityTemporalListVO = new EntityTemporalListVO(); + entityTemporalListVO.addAll(entityTemporalVOS); + MutableHttpResponse mutableHttpResponse; + if (limitableResult.isLimited()) { + Range range = getRange(getTimestampListFromEntityTemporalList(entityTemporalVOS, timeQuery), timeQuery, lastN); + mutableHttpResponse = HttpResponse + .status(HttpStatus.PARTIAL_CONTENT) + .body((Object) entityTemporalListVO) + .header(CONTENT_RANGE_HEADER_KEY, getContentRange(range, lastN)); + } else { + mutableHttpResponse = HttpResponse.ok(entityTemporalListVO); + } + paginationInformation.ifPresent(pi -> { + mutableHttpResponse.header(PAGE_SIZE_HEADER, String.valueOf(pi.getPageSize())); + pi.getNextPage().ifPresent(np -> mutableHttpResponse.header(NEXT_PAGE_HEADER, np)); + pi.getPreviousPage().ifPresent(pp -> mutableHttpResponse.header(PREVIOUS_PAGE_HEADER, pp)); + }); + + if (isCountOptionSet(options)) { + Number totalCount = entityTemporalService.countMatchingEntities(optionalIdList, optionalIdPattern, expandedTypes, optionalQuery, optionalGeoQuery, timeQuery); + mutableHttpResponse.header(NGSILD_RESULTS_COUNT_HEADER, totalCount.toString()); + } + + if (acceptType == AcceptType.JSON) { + mutableHttpResponse.header(LINK_HEADER, getLinkHeader(contextUrls)); + } + return mutableHttpResponse; + } + + private Integer getPageSize(Integer pageSize, Integer limit) { + Integer requestedPageSize = Optional.ofNullable(pageSize).orElse(limit); + return Optional.ofNullable(requestedPageSize).orElse(DEFAULT_LIMIT); + } + + @Override + public HttpResponse queryTemporalEntitiesOnPost( + @NotNull QueryVO queryVO, + @Nullable String link, + @Nullable @Min(1) @Max(100) Integer pageSize, + @Nullable URI pageAnchor, + @Nullable @Min(1) @Max(100) Integer limit, + @Nullable String options, + @Nullable @Min(1) Integer lastN) { + + Optional entityInfoVO = Optional.ofNullable(queryVO.entities()); + Optional geoQueryVO = Optional.ofNullable(queryVO.geoQ()); + Optional temporalQueryVO = Optional.ofNullable(queryVO.temporalQ()); + + return queryTemporalEntities(link, + entityInfoVO.map(EntityInfoVO::getId).map(this::idToString).orElse(null), + entityInfoVO.map(EntityInfoVO::getIdPattern).orElse(null), + entityInfoVO.map(EntityInfoVO::type).orElse(null), + Optional.ofNullable(queryVO.attrs()).map(attrsList -> attrsList.stream().collect(Collectors.joining(","))).orElse(null), queryVO.q(), + geoQueryVO.map(GeoQueryVO::georel).orElse(null), + geoQueryVO.map(GeoQueryVO::geometry).orElse(null), + geoQueryVO.map(GeoQueryVO::coordinates).map(this::coordinatesToString).orElse(null), + geoQueryVO.map(GeoQueryVO::geoproperty).orElse(null), + temporalQueryVO.map(TemporalQueryVO::getTimerel).map(TimerelVO::toEnum).orElse(null), + temporalQueryVO.map(TemporalQueryVO::getTimeproperty).orElse(null), + temporalQueryVO.map(TemporalQueryVO::timeAt).orElse(null), + temporalQueryVO.map(TemporalQueryVO::endTimeAt).orElse(null), + queryVO.csf(), pageSize, pageAnchor, limit, options, lastN); + } + + private String idToString(Object id) { + if (id instanceof List) { + return ((List) id).stream().map(Object::toString).collect(Collectors.joining(",")); + } else if (id instanceof URI) { + return ((URI) id).toString(); + } + return id.toString(); + } + + private String coordinatesToString(List coordinates) { + return coordinates.stream().map(Object::toString).collect(Collectors.joining(",")); + } + + @Override + public HttpResponse retrieveEntityTemporalById( + URI entityId, + @Nullable String link, + @Nullable @Size(min = 1) String attrs, + @Nullable String options, + @Nullable TimerelVO timerel, + @Nullable @Pattern(regexp = "^((\\d|[a-zA-Z]|_)+(:(\\d|[a-zA-Z]|_)+)?(#\\d+)?)$") @Size(min = 1) String timeproperty, + @Nullable Instant timeAt, + @Nullable Instant endTimeAt, + @Nullable @Min(1) Integer lastN) { + + AcceptType acceptType = getAcceptType(); + List contextUrls = contextCache.getContextURLsFromLinkHeader(link); + TimeQuery timeQuery = new TimeQuery(apiDomainMapper.timeRelVoToTimeRelation(timerel), timeAt, endTimeAt, getTimeRelevantProperty(timeproperty)); + + Optional> optionalLimitableResult = entityTemporalService + .getNgsiEntitiesWithTimerel(entityId.toString(), + timeQuery, + getExpandedAttributes(contextUrls, attrs), + lastN, + isSysAttrs(options), + isTemporalValuesOptionSet(options)); + + if (optionalLimitableResult.isEmpty()) { + Optional optionalEntity = entityTemporalService.getNgsiEntity(entityId.toString()); + if (optionalEntity.isPresent()) { + return HttpResponse.ok(addContextToEntityTemporalVO(optionalEntity.get(), contextUrls)); + } + throw new NotFoundException(String.format("Unable to find the entity '%s'.", entityId.toString())); + } + + MutableHttpResponse mutableHttpResponse; + LimitableResult limitableResult = optionalLimitableResult.get(); + if (limitableResult.isLimited()) { + Range range = getRange(getTimestampListFromEntityTemporal(limitableResult.getResult(), timeQuery), timeQuery, lastN); + mutableHttpResponse = HttpResponse + .status(HttpStatus.PARTIAL_CONTENT) + .body((Object) addContextToEntityTemporalVO(limitableResult.getResult(), contextUrls)) + .header(CONTENT_RANGE_HEADER_KEY, getContentRange(range, lastN)); + } else { + mutableHttpResponse = HttpResponse.ok(addContextToEntityTemporalVO(limitableResult.getResult(), contextUrls)); + } + if (acceptType == AcceptType.JSON) { + mutableHttpResponse.header("Link", getLinkHeader(contextUrls)); + } + return mutableHttpResponse; + } + + private List getIdList(String id) { + return Arrays.asList(id.split(",")); + } + + private String getLinkHeader(List contextUrls) { + // its either core or the current, since core is always the latest entry + return String.format(LINK_HEADER_TEMPLATE, contextUrls.get(0)); + } + + private String getContentRange(Range range, Integer lastN) { + String size = "*"; + if (lastN != null && lastN > 0) { + size = lastN.toString(); + } + return String.format("date-time %s-%s/%s", range.getStart(), range.getEnd(), size); + } + + private List getTimestampListFromEntityTemporalList(List entityTemporalVOS, TimeQuery timeQuery) { + return entityTemporalVOS + .stream() + .flatMap(entityTemporalVO -> getTimestampListFromEntityTemporal(entityTemporalVO, timeQuery).stream()) + .collect(Collectors.toList()); + } + + private List getTimestampListFromEntityTemporal(EntityTemporalVO entityTemporalVO, TimeQuery timeQuery) { + List timeStampList = new ArrayList<>(); + timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getObservationSpace()) + .stream() + .flatMap(List::stream) + .map(geoPropertyVO -> Optional.ofNullable(getTimestampFromGeoProperty(geoPropertyVO, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) + .collect(Collectors.toList())); + timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getOperationSpace()) + .stream() + .flatMap(List::stream) + .map(geoPropertyVO -> Optional.ofNullable(getTimestampFromGeoProperty(geoPropertyVO, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) + .collect(Collectors.toList())); + timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getLocation()) + .stream() + .flatMap(List::stream) + .map(geoPropertyVO -> Optional.ofNullable(getTimestampFromGeoProperty(geoPropertyVO, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) + .collect(Collectors.toList())); + timeStampList.addAll(Optional.ofNullable(entityTemporalVO.getAdditionalProperties()) + .stream() + .map(Map::values) + .flatMap(Collection::stream) + .flatMap(instanceList -> ((List) instanceList).stream()) + .map(propertyObject -> Optional.ofNullable(getTimestampFromPropertyObject(propertyObject, timeQuery.getTimeStampType())).orElse(Instant.ofEpochMilli(0))) + .collect(Collectors.toList())); + return timeStampList; + } + + private Range getRange(List timeStampList, TimeQuery timeQuery, Integer lastN) { + if (lastN != null) { + return getRangeWithLastN(timeStampList, timeQuery); + } else { + return getRangeWithoutLastN(timeStampList, timeQuery); + } + } + + private Range getRangeWithoutLastN(List timeStamps, TimeQuery timeQuery) { + TimeRelation timeRelation = Optional.ofNullable(timeQuery.getTimeRelation()).orElse(TimeRelation.BEFORE); + timeStamps.sort(Comparator.naturalOrder()); + + Instant startOfList = timeStamps.get(0); + Instant endOfList = timeStamps.get(timeStamps.size() - 1); + + switch (timeRelation) { + case BEFORE: + return new Range(startOfList, endOfList); + case BETWEEN: + case AFTER: + return new Range(timeQuery.getTimeAt(), endOfList); + default: + throw new InvalidTimeRelationException(String.format("Received an invalid timerelation: %s", timeRelation)); + } + } + + private Range getRangeWithLastN(List timeStamps, TimeQuery timeQuery) { + TimeRelation timeRelation = Optional.ofNullable(timeQuery.getTimeRelation()).orElse(TimeRelation.AFTER); + timeStamps.sort(Comparator.naturalOrder()); + + switch (timeRelation) { + case BETWEEN: + return new Range(timeQuery.getEndTime(), timeStamps.get(0)); + case BEFORE: + return new Range(timeQuery.getTimeAt(), timeStamps.get(0)); + case AFTER: + return new Range(timeStamps.get(timeStamps.size() - 1), timeStamps.get(0)); + default: + throw new InvalidTimeRelationException(String.format("Received an invalid timerelation: %s", timeRelation)); + } + } + + private Instant getTimestampFromPropertyObject(Object propertyObject, TimeStampType timeStampType) { + if (propertyObject instanceof RelationshipVO) { + return getTimestampFromRelationShip((RelationshipVO) propertyObject, timeStampType); + } else if (propertyObject instanceof PropertyVO) { + return getTimestampFromProperty((PropertyVO) propertyObject, timeStampType); + } else if (propertyObject instanceof GeoPropertyVO) { + return getTimestampFromGeoProperty((GeoPropertyVO) propertyObject, timeStampType); + } + throw new PersistenceRetrievalException(String.format("The given propertyObject is not valid: %s", propertyObject)); + } + + private Instant getTimestampFromProperty(PropertyVO propertyVO, TimeStampType timeStampType) { + switch (timeStampType) { + case CREATED_AT: + return propertyVO.getCreatedAt(); + case OBSERVED_AT: + return propertyVO.getObservedAt(); + case MODIFIED_AT: + case TS: + return propertyVO.getModifiedAt(); + default: + throw new InvalidTimeRelationException(String.format(TIMERELATION_ERROR_MSG_TEMPLATE, timeStampType)); + } + } + + private Instant getTimestampFromRelationShip(RelationshipVO relationshipVO, TimeStampType timeStampType) { + switch (timeStampType) { + case CREATED_AT: + return relationshipVO.getCreatedAt(); + case OBSERVED_AT: + return relationshipVO.getObservedAt(); + case MODIFIED_AT: + case TS: + return relationshipVO.getModifiedAt(); + default: + throw new InvalidTimeRelationException(String.format(TIMERELATION_ERROR_MSG_TEMPLATE, timeStampType)); + } + } + + private Instant getTimestampFromGeoProperty(GeoPropertyVO geoPropertyVO, TimeStampType timeStampType) { + switch (timeStampType) { + case CREATED_AT: + return geoPropertyVO.getCreatedAt(); + case OBSERVED_AT: + return geoPropertyVO.getObservedAt(); + case MODIFIED_AT: + case TS: + return geoPropertyVO.getModifiedAt(); + default: + throw new InvalidTimeRelationException(String.format("The given timestamp type is not supported: %s", timeStampType)); + } + } + + /** + * Add the context urls to the entities temporal represenation + */ + private EntityTemporalVO addContextToEntityTemporalVO(EntityTemporalVO entityTemporalVO, List contextUrls) { + if (contextUrls.size() > 1) { + entityTemporalVO.atContext(contextUrls); + } else { + entityTemporalVO.atContext(contextUrls.get(0)); + } + return entityTemporalVO; + } + + /** + * Expand all attributes present in the attrs parameter + * + * @param contextUrls + * @param attrs + * @return + */ + private List getExpandedAttributes(List contextUrls, String attrs) { + if (attrs == null) { + return List.of(); + } + + return Arrays.stream(attrs.split(COMMA_SEPERATOR)) + .map(attribute -> { + if (WELL_KNOWN_ATTRIBUTES.contains(attribute)) { + return attribute; + } + return contextCache.expandString(attribute, contextUrls); + }).collect(Collectors.toList()); + } + + private List getExpandedTypes(List contextUrls, String types) { + return Optional.ofNullable(types) + .map(al -> contextCache.expandStrings(Arrays.asList(types.split(COMMA_SEPERATOR)), contextUrls)) + .orElse(List.of()); + } + + /** + * Get the timeProperty string or the default property if null + * + * @param timeProperty timeProperty retrieved through the api + * @return timeProperty to be used + */ + private String getTimeRelevantProperty(String timeProperty) { + return Optional.ofNullable(timeProperty).orElse(DEFAULT_TIME_PROPERTY); + } + + private boolean isSysAttrs(String options) { + if (options == null) { + return false; + } + return Arrays.asList(options.split(COMMA_SEPERATOR)).contains(SYS_ATTRS_OPTION); + } + + private boolean isTemporalValuesOptionSet(String options) { + Optional optionalOptions = Optional.ofNullable(options); + if (optionalOptions.isEmpty()) { + return false; + } + return Arrays.asList(options.split(COMMA_SEPERATOR)).contains(TEMPORAL_VALUES_OPTION); + } + + private boolean isCountOptionSet(String options) { + Optional optionalOptions = Optional.ofNullable(options); + if (optionalOptions.isEmpty()) { + return false; + } + return Arrays.asList(options.split(COMMA_SEPERATOR)).contains(COUNT_OPTION); + } + + private Optional getGeometryQuery(String georel, String geometry, String coordinates, String geoproperty) { + if (georel == null && coordinates == null && geometry == null) { + return Optional.empty(); + } + + if (georel == null || coordinates == null || geometry == null) { + throw new IllegalArgumentException( + String.format("When querying for geoRelations, all 3 parameters(georel: %s, coordinates: %s, geometry: %s) need to be present.", georel, coordinates, geometry)); + } + + return Optional.of(new GeoQuery(georel, Geometry.byName(geometry), coordinates, geoproperty)); + } + + private AcceptType getAcceptType() { + return ServerRequestContext.currentRequest() + .map(HttpRequest::getHeaders) + .map(headers -> headers.get("Accept")) + .map(AcceptType::getEnum) + .orElse(AcceptType.JSON); + } }