diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java index 820e8428..740e0448 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java @@ -3,6 +3,7 @@ public enum FeatureId { summary("summary"), wfs_fields("wfs_fields"), // Query field based on pure wfs and given layer + wfs_field_value("wfs_field_value"), wms_fields("wms_fields"), // Query field based on value from wms describe layer query wave_buoy_first_data_available("wave_buoy_first_data_available"), wave_buoy_latest_date("wave_buoy_latest_date"), diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/FeatureRequest.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/FeatureRequest.java index 72a340be..a419be9e 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/FeatureRequest.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/FeatureRequest.java @@ -1,9 +1,8 @@ package au.org.aodn.ogcapi.server.core.model.ogc; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Builder; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.*; +import lombok.experimental.SuperBuilder; import java.io.Serializable; import java.math.BigDecimal; @@ -11,11 +10,26 @@ @Schema(description = "Query parameters for feature requests") @Data -@Builder +@SuperBuilder +@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED) // Need when using @SuperBuilder @EqualsAndHashCode public class FeatureRequest implements Serializable { + // Define a fix name for fields, the geoserver data have all sorts of different name, + // map it here so that we hide the complexity of call from UI. + public enum PropertyName { + wildcard, + time; + + public static PropertyName fromString(String input) { + if (input == null || "*".equals(input.trim())) { + return wildcard; // or throw exception / return default + } + return valueOf(input.trim()); + } + } + @Schema(description = "Property to be return") - private List properties; + private List properties; @Schema(description = "Only records that have a geometry that intersects the bounding box are selected. The bounding box is provided as four or six numbers, depending on whether the coordinate reference system includes a vertical axis (height or depth): * Lower left corner, coordinate axis 1 * Lower left corner, coordinate axis 2 * Minimum value, coordinate axis 3 (optional) * Upper right corner, coordinate axis 1 * Upper right corner, coordinate axis 2 * Maximum value, coordinate axis 3 (optional) The coordinate reference system of the values is WGS 84 long/lat (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate reference system is specified in the parameter `bbox-crs`. For WGS 84 longitude/latitude the values are in most cases the sequence of minimum longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where the box spans the antimeridian the first value (west-most box edge) is larger than the third value (east-most box edge). If the vertical axis is included, the third and the sixth number are the bottom and the top of the 3-dimensional bounding box. If a record has multiple spatial geometry properties, it is the decision of the server whether only a single spatial geometry property is used to determine the extent or all relevant geometries.") private List bbox; @@ -44,6 +58,7 @@ public class FeatureRequest implements Serializable { @Schema(description = "Enable or disable geoserver whitelist") @Builder.Default private Boolean enableGeoServerWhiteList = Boolean.TRUE; + /** * Make sure if json indicate null, we still return true by default * @return - Utility function with default diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java deleted file mode 100644 index e02ae4be..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java +++ /dev/null @@ -1,31 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model.ogc.wfs; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Data; - -import java.util.List; - -@Data -@Builder -public class WFSFieldModel { - - @JsonProperty("typename") - private String typename; - - @JsonProperty("fields") - private List fields; - - @Data - @Builder - public static class Field { - @JsonProperty("label") - private String label; - - @JsonProperty("name") - private String name; - - @JsonProperty("type") - private String type; - } -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsField.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsField.java new file mode 100644 index 00000000..3caa2985 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsField.java @@ -0,0 +1,20 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wfs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@Builder +@EqualsAndHashCode +public class WfsField { + @JsonProperty("label") + private String label; + + @JsonProperty("name") + private String name; + + @JsonProperty("type") + private String type; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsFields.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsFields.java new file mode 100644 index 00000000..75c29d79 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsFields.java @@ -0,0 +1,18 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wfs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class WfsFields { + + @JsonProperty("typename") + private String typename; + + @JsonProperty("fields") + private List fields; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java index 315d9571..0cf25a78 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java @@ -5,6 +5,7 @@ import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.SearchSuggestionsModel; import au.org.aodn.ogcapi.server.core.model.enumeration.*; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.parser.elastic.CQLToElasticFilterFactory; import au.org.aodn.ogcapi.server.core.parser.elastic.QueryHandler; import co.elastic.clients.elasticsearch.ElasticsearchClient; @@ -632,7 +633,7 @@ protected static FieldValue toFieldValue(String s) { // } @Override - public SearchResult searchFeatureSummary(String collectionId, List properties, String filter) { + public SearchResult searchFeatureSummary(String collectionId, List properties, String filter) { try { SearchRequest searchRequest = new SearchRequest.Builder() .index(dataIndexName) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java index 4a319602..7242cd89 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java @@ -6,6 +6,7 @@ import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureId; import au.org.aodn.ogcapi.server.core.model.enumeration.OGCMediaTypeMapper; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.parser.stac.CQLToStacFilterFactory; import au.org.aodn.ogcapi.server.tile.RestApi; import org.geotools.filter.text.commons.CompilerUtil; @@ -39,9 +40,9 @@ public abstract class OGCApiService { public abstract List getConformanceDeclaration(); public ResponseEntity getFeature(String collectionId, - FeatureId fid, - List properties, - String filter) throws Exception { + FeatureId fid, + List properties, + String filter) throws Exception { switch(fid) { case summary -> { var result = search.searchFeatureSummary(collectionId, properties, filter); @@ -160,9 +161,9 @@ else if (datetime.contains("/") && !datetime.contains("..")) { } /** * Convert the bbox parameter to CQL - * @param bbox - * @param filter - * @return + * @param bbox - Bounding box + * @param filter - CQL filter string + * @return - String format as cql */ public static String processBBoxParameter(String fieldName, List bbox, String filter) { String f = null; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java index f03b655b..2cf4b995 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java @@ -3,6 +3,7 @@ import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import co.elastic.clients.transport.endpoints.BinaryResponse; import org.springframework.http.ResponseEntity; @@ -17,7 +18,7 @@ public interface Search { ElasticSearchBase.SearchResult searchCollections(String id); ElasticSearchBase.SearchResult searchCollections(List ids, String sortBy); ElasticSearchBase.SearchResult searchAllCollections(String sortBy) throws Exception; - ElasticSearchBase.SearchResultsearchFeatureSummary(String collectionId, List properties, String filter) throws Exception; + ElasticSearchBase.SearchResultsearchFeatureSummary(String collectionId, List properties, String filter) throws Exception; ElasticSearchBase.SearchResult searchByParameters( List targets, diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java index 27212868..e3e901e8 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java @@ -1,7 +1,8 @@ package au.org.aodn.ogcapi.server.core.service.wfs; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.model.ogc.wms.DescribeLayerResponse; import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; import au.org.aodn.ogcapi.server.core.util.DatetimeUtils; @@ -42,17 +43,17 @@ public DownloadWfsDataService( /** * Build CQL filter for temporal and spatial constraints */ - private String buildCqlFilter(String startDate, String endDate, Object multiPolygon, WFSFieldModel wfsFieldModel) { + private String buildCqlFilter(String startDate, String endDate, Object multiPolygon, WfsFields wfsFieldModel) { StringBuilder cqlFilter = new StringBuilder(); if (wfsFieldModel == null || wfsFieldModel.getFields() == null) { return cqlFilter.toString(); } - List fields = wfsFieldModel.getFields(); + List fields = wfsFieldModel.getFields(); // Find temporal field - Optional temporalField = fields.stream() + Optional temporalField = fields.stream() .filter(field -> "dateTime".equals(field.getType()) || "date".equals(field.getType())) .findFirst(); @@ -66,7 +67,7 @@ private String buildCqlFilter(String startDate, String endDate, Object multiPoly } // Find geometry field - Optional geometryField = fields.stream() + Optional geometryField = fields.stream() .filter(field -> "geometrypropertytype".equals(field.getType())) .findFirst(); @@ -106,14 +107,14 @@ public String prepareWfsRequestUrl( String wfsServerUrl; String wfsTypeName; - WFSFieldModel wfsFieldModel; + WfsFields wfsFieldModel; // Try to get WFS details from DescribeLayer first, then fallback to searching by layer name if (describeLayerResponse != null && describeLayerResponse.getLayerDescription().getWfs() != null) { wfsServerUrl = describeLayerResponse.getLayerDescription().getWfs(); wfsTypeName = describeLayerResponse.getLayerDescription().getQuery().getTypeName(); - wfsFieldModel = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); + wfsFieldModel = wfsServer.getDownloadableFields(uuid, WfsServer.WfsFeatureRequest.builder().layerName(wfsTypeName).server(wfsServerUrl).build()); log.info("WFSFieldModel by describeLayer: {}", wfsFieldModel); } else { Optional featureServerUrl = wfsServer.getFeatureServerUrlByTitle(uuid, layerName); @@ -121,7 +122,7 @@ public String prepareWfsRequestUrl( if (featureServerUrl.isPresent()) { wfsServerUrl = featureServerUrl.get(); wfsTypeName = layerName; - wfsFieldModel = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); + wfsFieldModel = wfsServer.getDownloadableFields(uuid, WfsServer.WfsFeatureRequest.builder().layerName(wfsTypeName).server(wfsServerUrl).build()); log.info("WFSFieldModel by wfs typename: {}", wfsFieldModel); } else { throw new IllegalArgumentException("No WFS server URL found for the given UUID and layer name"); diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServer.java index a67ff7a2..4013a688 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServer.java @@ -5,10 +5,7 @@ import au.org.aodn.ogcapi.server.core.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsDescribeFeatureTypeResponse; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsGetCapabilitiesResponse; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.*; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import au.org.aodn.ogcapi.server.core.service.Search; import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils; @@ -16,10 +13,14 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Lazy; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -52,6 +53,16 @@ public class WfsServer { @Autowired protected WfsServer self; + /** + * Internal use only to compress the number of argument pass on function call. + */ + @Getter + @Setter + @SuperBuilder + public static class WfsFeatureRequest extends FeatureRequest { + private String server; + } + public WfsServer(Search search, RestTemplate restTemplate, RestTemplateUtils restTemplateUtils, @@ -152,7 +163,7 @@ protected String createFeatureFieldQueryUrl(String url, FeatureRequest request) // This is the normal route UriComponentsBuilder builder = UriComponentsBuilder .newInstance() - .scheme(components.getScheme()) + .scheme("https") .port(components.getPort()) .host(components.getHost()) .path(components.getPath()); @@ -171,17 +182,63 @@ protected String createFeatureFieldQueryUrl(String url, FeatureRequest request) return null; } + protected String createFeatureValueQueryUrl(String url, FeatureRequest request) { + UriComponents components = UriComponentsBuilder.fromUriString(url).build(); + if (components.getPath() != null) { + // Now depends on the service, we need to have different arguments + List pathSegments = components.getPathSegments(); + if (!pathSegments.isEmpty()) { + Map param = new HashMap<>(wfsDefaultParam.getDownload()); + + // Now we add the missing argument from the request + param.put("TYPENAME", request.getLayerName()); + param.put("outputFormat", "application/json"); + + if(request.getProperties() != null && !request.getProperties().contains(FeatureRequest.PropertyName.wildcard)) { + param.put("propertyName", String.join( + ",", + request.getProperties().stream().map(Enum::name).toList()) + ); + param.put("sortBy", String.join( + ",", + // Assume always sort by desc + request.getProperties().stream().map(p -> String.format("%s+D", p.name())).toList()) + ); + } + + // This is the normal route + UriComponentsBuilder builder = UriComponentsBuilder + .newInstance() + .scheme("https") + .port(components.getPort()) + .host(components.getHost()) + .path(components.getPath()); + + param.forEach((key, value) -> { + if (value != null) { + builder.queryParam(key, value); + } + }); + String target = builder.build().toUriString(); + log.debug("Url query field value in wfs {}", target); + + return target; + } + } + return null; + } + /** * Convert WFS response to WFSFieldModel. * The typename is extracted from the top-level xsd:element (e.g., ) */ - protected static WFSFieldModel convertWfsResponseToDownloadableFields(WfsDescribeFeatureTypeResponse wfsResponse) { + protected static WfsFields convertWfsResponseToDownloadableFields(WfsDescribeFeatureTypeResponse wfsResponse) { String typename = null; if (wfsResponse.getTopLevelElements() != null && !wfsResponse.getTopLevelElements().isEmpty()) { typename = wfsResponse.getTopLevelElements().get(0).getName(); } - List fields = wfsResponse.getComplexTypes() != null ? + List fields = wfsResponse.getComplexTypes() != null ? wfsResponse.getComplexTypes().stream() .filter(complexType -> complexType.getComplexContent() != null) .filter(complexType -> complexType.getComplexContent().getExtension() != null) @@ -192,7 +249,7 @@ protected static WFSFieldModel convertWfsResponseToDownloadableFields(WfsDescrib return elements != null ? elements.stream() : Stream.empty(); }) .filter(element -> element.getName() != null && element.getType() != null) - .map(element -> WFSFieldModel.Field.builder() + .map(element -> WfsField.builder() .label(element.getName()) .name(element.getName()) // The type can be in format of "xsd:date", we only want the actual type name "date" @@ -200,25 +257,50 @@ protected static WFSFieldModel convertWfsResponseToDownloadableFields(WfsDescrib .build()) .collect(Collectors.toList()) : new ArrayList<>(); - return WFSFieldModel.builder() + return WfsFields.builder() .typename(typename) .fields(fields) .build(); } + public T getFieldValues(String collectionId, WfsFeatureRequest request, ParameterizedTypeReference tClass) { + Optional> mapFeatureUrl = request.getServer() != null ? + Optional.of(List.of(request.getServer())) : + getAllFeatureServerUrls(collectionId); + + if (mapFeatureUrl.isPresent()) { + // Keep trying all possible url until one get response + for (String url : mapFeatureUrl.get()) { + String uri = createFeatureValueQueryUrl(url, request); + try { + if (uri != null) { + ResponseEntity response = + restTemplate.exchange(uri, HttpMethod.GET, pretendUserEntity, tClass + ); + + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { + return response.getBody(); + } + } + } catch (Exception e) { + log.debug("Ignore error for {}, will try another url", uri); + } + } + } + return null; + } /** * Get the downloadable fields for a given collection id and layer name * * @param collectionId - The uuid of the collection * @param request - The feature request containing the layer name - * @param assumedWfsServer - An optional wfs server url to use instead of searching for one * @return - WFSFieldModel containing typename and fields */ @Cacheable(value = DOWNLOADABLE_FIELDS) - public WFSFieldModel getDownloadableFields(String collectionId, FeatureRequest request, String assumedWfsServer) { + public WfsFields getDownloadableFields(String collectionId, WfsFeatureRequest request) { - Optional> mapFeatureUrl = assumedWfsServer != null ? - Optional.of(List.of(assumedWfsServer)) : + Optional> mapFeatureUrl = request.getServer() != null ? + Optional.of(List.of(request.getServer())) : getAllFeatureServerUrls(collectionId); if (mapFeatureUrl.isPresent()) { @@ -251,13 +333,13 @@ public WFSFieldModel getDownloadableFields(String collectionId, FeatureRequest r throw new GeoserverFieldsNotFoundException("No downloadable fields found for all url"); } - public List getWFSFields(String collectionId, FeatureRequest request) { - List wfsFields = new ArrayList<>(); + public List getWFSFields(String collectionId, WfsServer.WfsFeatureRequest request) { + List wfsFields = new ArrayList<>(); // If typename is provided, use it directly // If no typename provided, get fields for all layers from collection WFS links if (request.getLayerName() != null && !request.getLayerName().isEmpty()) { - wfsFields.add(self.getDownloadableFields(collectionId, request, null)); + wfsFields.add(self.getDownloadableFields(collectionId, request)); } else { log.debug("No layer name provided in request, get fields for all WFS links"); List typeNamesToProcess = new ArrayList<>(); @@ -269,12 +351,12 @@ public List getWFSFields(String collectionId, FeatureRequest requ } // fetch downloadable fields for each typename for (String typeName : typeNamesToProcess) { - FeatureRequest requestModified = FeatureRequest.builder() + WfsServer.WfsFeatureRequest requestModified = WfsServer.WfsFeatureRequest.builder() .layerName(typeName) .build(); try { - WFSFieldModel fields = self.getDownloadableFields(collectionId, requestModified, null); + WfsFields fields = self.getDownloadableFields(collectionId, requestModified); if (fields != null) { wfsFields.add(fields); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java index ce1f25e2..55436ec1 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServer.java @@ -5,7 +5,8 @@ import au.org.aodn.ogcapi.server.core.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.model.ogc.wms.*; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import au.org.aodn.ogcapi.server.core.service.Search; @@ -101,20 +102,20 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { // Special handle for date time field, the field name will be diff across dataset. So we need // to look it up try { - List wfsFieldModels = this.getWMSFields(uuid, request); + List wfsFieldModels = this.getWMSFields(uuid, request); // Flatten all fields from all WFSFieldModels - List allFields = wfsFieldModels.stream() + List allFields = wfsFieldModels.stream() .filter(m -> m.getFields() != null) .flatMap(m -> m.getFields().stream()) .toList(); - List target = allFields.stream() + List target = allFields.stream() .filter(value -> "dateTime".equalsIgnoreCase(value.getType())) .toList(); if (!target.isEmpty()) { - List range; + List range; if (target.size() > 2) { // Try to find possible fields where it contains start end min max range = target.stream() @@ -139,7 +140,7 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { } else { // There are more than 1 dateTime field, it is not range type, so we try to guess the individual one // based on some common name. Add more if needed - List individual = target.stream() + List individual = target.stream() .filter(v -> Stream.of("juld", "time").anyMatch(k -> v.getName().equalsIgnoreCase(k))) .toList(); @@ -583,6 +584,22 @@ public DescribeLayerResponse describeLayer(String collectionId, FeatureRequest r return null; } + public WfsServer.WfsFeatureRequest createRequestFromLayerName(String collectionId, String layerName) { + FeatureRequest layerRequest = FeatureRequest.builder().layerName(layerName).build(); + + DescribeLayerResponse response = this.describeLayer(collectionId, layerRequest); + if(response != null && response.getLayerDescription() != null) { + return WfsServer.WfsFeatureRequest.builder() + .layerName(response.getLayerDescription().getQuery().getTypeName()) + .server(response.getLayerDescription().getWfs()) + .build(); + } + else { + return WfsServer.WfsFeatureRequest.builder() + .layerName(layerName) + .build(); + } + } /** * Fetch fields for a single layer using WFS.getDownloadableFields * @@ -590,21 +607,10 @@ public DescribeLayerResponse describeLayer(String collectionId, FeatureRequest r * @param layerName - The layer name to fetch fields for * @return - WFSFieldModel containing typename and fields, or null if not found */ - private WFSFieldModel fetchFieldsForLayer(String collectionId, String layerName) { - FeatureRequest layerRequest = FeatureRequest.builder().layerName(layerName).build(); - - DescribeLayerResponse response = this.describeLayer(collectionId, layerRequest); + protected WfsFields fetchFieldsForLayer(String collectionId, String layerName) { - if (response != null && response.getLayerDescription().getWfs() != null) { - // Use describe layer to find the real layer name and wfs server for fields - FeatureRequest requestWithDescribeLayer = FeatureRequest.builder() - .layerName(response.getLayerDescription().getQuery().getTypeName()) - .build(); - return wfsServer.getDownloadableFields(collectionId, requestWithDescribeLayer, response.getLayerDescription().getWfs()); - } else { - // Fallback: trust what is found inside the elastic search metadata - return wfsServer.getDownloadableFields(collectionId, layerRequest, null); - } + WfsServer.WfsFeatureRequest request = createRequestFromLayerName(collectionId, layerName); + return wfsServer.getDownloadableFields(collectionId, request); } /** @@ -614,8 +620,8 @@ private WFSFieldModel fetchFieldsForLayer(String collectionId, String layerName) * @param request - Request item for this WMS layer, usually layer name * @return - List of WFSFieldModel containing typename and fields for each WMS layer */ - public List getWMSFields(String collectionId, FeatureRequest request) { - List wmsFields = new ArrayList<>(); + public List getWMSFields(String collectionId, FeatureRequest request) { + List wmsFields = new ArrayList<>(); List layerNamesToProcess = new ArrayList<>(); // If layer name is provided, use it directly @@ -632,7 +638,7 @@ public List getWMSFields(String collectionId, FeatureRequest requ // Fetch fields for each layer name for (String layerName : layerNamesToProcess) { - WFSFieldModel fieldModel = fetchFieldsForLayer(collectionId, layerName); + WfsFields fieldModel = fetchFieldsForLayer(collectionId, layerName); if (fieldModel != null) { wmsFields.add(fieldModel); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/CommonUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/CommonUtils.java index 1e7a1e45..e6db5083 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/CommonUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/CommonUtils.java @@ -1,5 +1,9 @@ package au.org.aodn.ogcapi.server.core.util; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; + +import java.beans.PropertyDescriptor; import java.util.Optional; import java.util.function.Supplier; @@ -14,4 +18,22 @@ public static Optional safeGet(Supplier supplier) { return Optional.empty(); } } + + public static void copyIgnoringNull(T source, T target) { + if(source == null || target == null) return; + + BeanWrapper src = new BeanWrapperImpl(source); + BeanWrapper tgt = new BeanWrapperImpl(target); + + for (PropertyDescriptor pd : src.getPropertyDescriptors()) { + if (pd.getReadMethod() == null || pd.getWriteMethod() == null) continue; + String name = pd.getName(); + if ("class".equals(name)) continue; + + Object value = src.getPropertyValue(name); + if (value != null) { + tgt.setPropertyValue(name, value); + } + } + } } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java index 384f468b..e6ba917d 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java @@ -127,6 +127,9 @@ public ResponseEntity getFeature( case wfs_fields -> { return featuresService.getWfsFields(collectionId, request); } + case wfs_field_value -> { + return featuresService.getWfsFieldValue(collectionId, request); + } case wms_fields -> { return featuresService.getWmsFields(collectionId, request); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java index f30a9d51..03a3f7c3 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java @@ -3,28 +3,38 @@ import au.org.aodn.ogcapi.features.model.Collection; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; import au.org.aodn.ogcapi.server.core.model.ogc.wms.FeatureInfoResponse; import au.org.aodn.ogcapi.server.core.model.ogc.wms.LayerInfo; import au.org.aodn.ogcapi.server.core.service.DasService; import au.org.aodn.ogcapi.server.core.mapper.StacToCollection; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.OGCApiService; import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; import au.org.aodn.ogcapi.server.core.service.wms.WmsDefaultParam; import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; +import au.org.aodn.ogcapi.server.core.util.CommonUtils; import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; +import org.geotools.feature.FeatureCollection; +import org.geotools.feature.FeatureIterator; +import org.geotools.geojson.feature.FeatureJSON; +import org.opengis.feature.simple.SimpleFeature; +import org.opengis.feature.simple.SimpleFeatureType; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.io.StringReader; import java.net.URISyntaxException; -import java.util.List; -import java.util.NoSuchElementException; +import java.util.*; @Slf4j @Service("FeaturesRestService") @@ -93,13 +103,68 @@ public ResponseEntity getWmsMapTile(String collectionId, FeatureRequest * @return - The WFS fields */ public ResponseEntity getWfsFields(String collectionId, FeatureRequest request) { - List result = wfsServer.getWFSFields(collectionId, request); + WfsServer.WfsFeatureRequest wfsFeatureRequest = WfsServer.WfsFeatureRequest.builder().build(); + BeanUtils.copyProperties(request, wfsFeatureRequest); + List result = wfsServer.getWFSFields(collectionId, wfsFeatureRequest); return result == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(result); } + /** + * Get the list of values from the WFS, the FeatureRequest have predefined enum to control what can pass in for the + * properties name. You may need to update it if you want to expand the list. + * @param collectionId - The uuid of the metadata + * @param request - The request property you want to query + * @return - The return value, which is sorted by desc + */ + public ResponseEntity getWfsFieldValue(String collectionId, FeatureRequest request) { + WfsServer.WfsFeatureRequest wfsWithServer = wmsServer.createRequestFromLayerName(collectionId, request.getLayerName()); + WfsServer.WfsFeatureRequest wfsFeatureRequest = WfsServer.WfsFeatureRequest.builder().build(); + // clone value from request + BeanUtils.copyProperties(request, wfsFeatureRequest); + // copy enhanced value + CommonUtils.copyIgnoringNull(wfsWithServer, wfsFeatureRequest); + + if(wfsFeatureRequest.getProperties() != null) { + // Now check if we need to map field + List supportedFields = wfsServer.getWFSFields(collectionId, wfsFeatureRequest); + List extractedName = supportedFields.get(0).getFields().stream() + .map(WfsField::getName) + .toList(); + + for (FeatureRequest.PropertyName name : wfsFeatureRequest.getProperties()) { + if (extractedName.contains(name.name())) { + // TODO: If missing then may need map + log.info("Field {} need map", name); + // + } + } + } + + String result = wfsServer.getFieldValues(collectionId, wfsFeatureRequest, new ParameterizedTypeReference<>() {}); + FeatureJSON json = new FeatureJSON(); + try { + @SuppressWarnings("unchecked") + FeatureCollection collection = json.readFeatureCollection(new StringReader(result)); + try(FeatureIterator i = collection.features()) { + Map> results = new HashMap<>(); + while(i.hasNext()) { + SimpleFeature s = i.next(); + s.getProperties() + .forEach(property -> { + results.computeIfAbsent(property.getName().toString(), k -> new ArrayList<>()); + results.get(property.getName().toString()).add(s.getAttribute(property.getName())); + }); + } + return ResponseEntity.ok().body(results); + } + } + catch (IOException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } /** * This is used to get the WMS fields from the describe wfs layer given a wms layer * @@ -112,7 +177,7 @@ public ResponseEntity getWmsFields(String collectionId, FeatureRequest reques if (request.getEnableGeoServerWhiteList() && wmsDefaultParam.getAllowId() != null && !wmsDefaultParam.getAllowId().contains(collectionId)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } else { - List result = wmsServer.getWMSFields(collectionId, request); + List result = wmsServer.getWMSFields(collectionId, request); return result.isEmpty() ? ResponseEntity.notFound().build() : @@ -152,7 +217,6 @@ public ResponseEntity getWfsLayers(String collectionId, FeatureRequest reques ResponseEntity.notFound().build() : ResponseEntity.ok(result); } - } /** diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java index c55f671c..9e3d1adc 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java @@ -1,7 +1,8 @@ package au.org.aodn.ogcapi.server.core.service.wfs; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.model.ogc.wms.DescribeLayerResponse; import au.org.aodn.ogcapi.server.core.service.Search; import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; @@ -66,24 +67,24 @@ public void setUp() { /** * Helper method to create a WFSFieldModel for testing */ - private WFSFieldModel createTestWFSFieldModel() { - List fields = new ArrayList<>(); + private WfsFields createTestWFSFieldModel() { + List fields = new ArrayList<>(); // Add geometry field - fields.add(WFSFieldModel.Field.builder() + fields.add(WfsField.builder() .name("geom") .label("geom") .type("geometrypropertytype") .build()); // Add datetime field - fields.add(WFSFieldModel.Field.builder() + fields.add(WfsField.builder() .name("timestamp") .label("timestamp") .type("dateTime") .build()); - return WFSFieldModel.builder() + return WfsFields.builder() .typename("testLayer") .fields(fields) .build(); @@ -94,7 +95,7 @@ public void testPrepareWfsRequestUrl_WithNullDates() { // Setup String uuid = "test-uuid"; String layerName = "test:layer"; - WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); + WfsFields wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -108,7 +109,7 @@ public void testPrepareWfsRequestUrl_WithNullDates() { doReturn(describeLayerResponse) .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); doReturn(wfsFieldModel) - .when(wfsServer).getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString()); + .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); // Test with null dates (non-specified dates from frontend) String result = downloadWfsDataService.prepareWfsRequestUrl( @@ -126,7 +127,7 @@ public void testPrepareWfsRequestUrl_WithEmptyDates() { // Setup String uuid = "test-uuid"; String layerName = "test:layer"; - WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); + WfsFields wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -140,7 +141,7 @@ public void testPrepareWfsRequestUrl_WithEmptyDates() { doReturn(describeLayerResponse) .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); doReturn(wfsFieldModel) - .when(wfsServer).getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString()); + .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); // Test with empty string dates String result = downloadWfsDataService.prepareWfsRequestUrl( @@ -160,7 +161,7 @@ public void testPrepareWfsRequestUrl_WithValidDates() { String layerName = "test:layer"; String startDate = "2023-01-01"; String endDate = "2023-12-31"; - WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); + WfsFields wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -174,7 +175,7 @@ public void testPrepareWfsRequestUrl_WithValidDates() { doReturn(describeLayerResponse) .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); doReturn(wfsFieldModel) - .when(wfsServer).getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString()); + .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); // Test with valid dates String result = downloadWfsDataService.prepareWfsRequestUrl( @@ -196,7 +197,7 @@ public void testPrepareWfsRequestUrl_WithOnlyStartDate() { String uuid = "test-uuid"; String layerName = "test:layer"; String startDate = "2023-01-01"; - WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); + WfsFields wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -210,7 +211,7 @@ public void testPrepareWfsRequestUrl_WithOnlyStartDate() { doReturn(describeLayerResponse) .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); doReturn(wfsFieldModel) - .when(wfsServer).getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString()); + .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); // Test with only start date (end date is null) String result = downloadWfsDataService.prepareWfsRequestUrl( @@ -230,7 +231,7 @@ public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { String layerName = "test:layer"; String startDate = "01-2023"; // MM-YYYY format String endDate = "12-2023"; // MM-YYYY format - WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); + WfsFields wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -244,7 +245,7 @@ public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { doReturn(describeLayerResponse) .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); doReturn(wfsFieldModel) - .when(wfsServer).getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString()); + .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); // Test with MM-YYYY format dates String result = downloadWfsDataService.prepareWfsRequestUrl( diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServerTest.java index 1859fc76..26bdca50 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServerTest.java @@ -5,7 +5,8 @@ import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import au.org.aodn.ogcapi.server.core.service.Search; import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils; @@ -140,7 +141,7 @@ public void testGetDownloadableFieldsSuccess() { """; - FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); + WfsServer.WfsFeatureRequest request = WfsServer.WfsFeatureRequest.builder().layerName("test:layer").build(); ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); stac.setCollections(List.of( @@ -164,7 +165,7 @@ public void testGetDownloadableFieldsSuccess() { .thenReturn(stac); WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); - WFSFieldModel result = server.getDownloadableFields(id, request, null); + WfsFields result = server.getDownloadableFields(id, request); assertNotNull(result); assertNotNull(result.getFields()); @@ -172,7 +173,7 @@ public void testGetDownloadableFieldsSuccess() { assertEquals(3, result.getFields().size()); // Check geometry field - WFSFieldModel.Field geomField = result.getFields().stream() + WfsField geomField = result.getFields().stream() .filter(f -> "geom".equals(f.getName())) .findFirst() .orElse(null); @@ -181,7 +182,7 @@ public void testGetDownloadableFieldsSuccess() { assertEquals("GeometryPropertyType", geomField.getType()); // Check datetime field - WFSFieldModel.Field timeField = result.getFields().stream() + WfsField timeField = result.getFields().stream() .filter(f -> "timestamp".equals(f.getName())) .findFirst() .orElse(null); @@ -190,7 +191,7 @@ public void testGetDownloadableFieldsSuccess() { assertEquals("dateTime", timeField.getType()); // Check string field - WFSFieldModel.Field nameField = result.getFields().stream() + WfsField nameField = result.getFields().stream() .filter(f -> "name".equals(f.getName())) .findFirst() .orElse(null); @@ -202,7 +203,7 @@ public void testGetDownloadableFieldsSuccess() { @Test public void testGetDownloadableFieldsNotFoundResponse() { // Mock WFS response with NOT_FOUND status - FeatureRequest request = FeatureRequest.builder().layerName("test:layer2").build(); + WfsServer.WfsFeatureRequest request = WfsServer.WfsFeatureRequest.builder().layerName("test:layer2").build(); ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); stac.setCollections(List.of( @@ -229,7 +230,7 @@ public void testGetDownloadableFieldsNotFoundResponse() { GeoserverFieldsNotFoundException exception = assertThrows( GeoserverFieldsNotFoundException.class, - () -> server.getDownloadableFields(id, request, null) + () -> server.getDownloadableFields(id, request) ); assertEquals("No downloadable fields found for all url", @@ -240,7 +241,7 @@ public void testGetDownloadableFieldsNotFoundResponse() { @Test public void testGetDownloadableFieldsWfsError() { - FeatureRequest request = FeatureRequest.builder().layerName("invalid:layer").build(); + WfsServer.WfsFeatureRequest request = WfsServer.WfsFeatureRequest.builder().layerName("invalid:layer").build(); ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); stac.setCollections(List.of( @@ -267,7 +268,7 @@ public void testGetDownloadableFieldsWfsError() { GeoserverFieldsNotFoundException exception = assertThrows( GeoserverFieldsNotFoundException.class, - () -> server.getDownloadableFields(id, request, null) + () -> server.getDownloadableFields(id, request) ); assertTrue(exception.getMessage().contains("No downloadable fields found")); @@ -275,7 +276,7 @@ public void testGetDownloadableFieldsWfsError() { @Test public void testGetDownloadableFieldsNetworkError() { - FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); + WfsServer.WfsFeatureRequest request = WfsServer.WfsFeatureRequest.builder().layerName("test:layer").build(); ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); stac.setCollections(List.of( @@ -303,7 +304,7 @@ public void testGetDownloadableFieldsNetworkError() { RuntimeException exception = assertThrows( RuntimeException.class, - () -> server.getDownloadableFields(id, request, null) + () -> server.getDownloadableFields(id, request) ); assertTrue(exception.getMessage().contains("Connection timeout")); @@ -311,7 +312,7 @@ public void testGetDownloadableFieldsNetworkError() { @Test public void testGetDownloadableFieldsNoCollection() { - FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); + WfsServer.WfsFeatureRequest request = WfsServer.WfsFeatureRequest.builder().layerName("test:layer").build(); ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); stac.setCollections(Collections.emptyList()); @@ -322,7 +323,7 @@ public void testGetDownloadableFieldsNoCollection() { WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); - WFSFieldModel result = server.getDownloadableFields(id, request, null); + WfsFields result = server.getDownloadableFields(id, request); assertNull(result, "Should return null when no collection found"); } diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/features/RestServicesTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/features/RestServicesTest.java index 63500c64..8dcc16e7 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/features/RestServicesTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/features/RestServicesTest.java @@ -1,16 +1,27 @@ package au.org.aodn.ogcapi.server.features; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.service.DasService; +import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; +import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; public class RestServicesTest { @@ -18,6 +29,12 @@ public class RestServicesTest { @Mock private DasService dasService; + @Mock + private WmsServer wmsServer; + + @Mock + private WfsServer wfsServer; + @InjectMocks private RestServices restServices; @@ -65,4 +82,69 @@ public void testGetWaveBuoysLatestDateServiceError() { assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); } + + @Test + public void testGetWfsTimeFieldWorks() { + when(wfsServer.getFieldValues(anyString(), any(WfsServer.WfsFeatureRequest.class), any(ParameterizedTypeReference.class))) + .thenReturn( + """ + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "srs_ghrsst_l3s_M_1d_ngt_url.fid-4218f2fa_19c6cde1def_1ef0", + "geometry": null, + "properties": { + "time": "2023-11-26T15:20:00Z" + } + }, + { + "type": "Feature", + "id": "srs_ghrsst_l3s_M_1d_ngt_url.fid-4218f2fa_19c6cde1def_1ef2", + "geometry": null, + "properties": { + "time": "2023-11-25T15:20:00Z" + } + } + ] + } + """ + ); + + when(wfsServer.getWFSFields(anyString(), any(WfsServer.WfsFeatureRequest.class))) + .thenReturn(List.of(WfsFields.builder() + .fields(List.of( + WfsField.builder() + .name("TIME") + .build() + ) + ) + .build() + )); + + ResponseEntity response = restServices.getWfsFieldValue( + "any-works", + FeatureRequest.builder() + .properties(List.of(FeatureRequest.PropertyName.time)) + .build() + ); + assertInstanceOf(Map.class, response.getBody()); + + @SuppressWarnings("unchecked") + Map> v = (Map>)response.getBody(); + + assertTrue(v.containsKey("time"), "time field found"); + assertEquals("2023-11-26T15:20:00Z", v.get("time").get(0)); + assertEquals("2023-11-25T15:20:00Z", v.get("time").get(1)); + + // It works even property is null + response = restServices.getWfsFieldValue( + "any-works", + FeatureRequest.builder() + .build() + ); + assertInstanceOf(Map.class, response.getBody()); + + } } diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java index 919e2b49..96a51769 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/service/ElasticSearchTest.java @@ -4,6 +4,7 @@ import au.org.aodn.ogcapi.server.core.model.EsFeatureCollectionModel; import au.org.aodn.ogcapi.server.core.model.EsFeatureModel; import au.org.aodn.ogcapi.server.core.model.EsPolygonModel; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import co.elastic.clients.elasticsearch.ElasticsearchClient; @@ -50,7 +51,7 @@ public void searchFeatureSummaryTest() throws IOException { // Arrange String collectionId = "test-collection"; - List properties = List.of("prop1", "prop2"); + List properties = List.of(FeatureRequest.PropertyName.wildcard); String filter = null; SearchResponse mockResponse = mock(SearchResponse.class); @@ -61,7 +62,7 @@ public void searchFeatureSummaryTest() throws IOException { featureCollectionProperties.put("collection", "2d496463-600c-465a-84a1-8a4ab76bd505"); featureCollectionProperties.put("key", "satellite_ghrsst_l4_gamssa_1day_multi_sensor_world.zarr"); esFeatureCollection.setProperties(featureCollectionProperties); - var coords = new ArrayList>>(); + List>> coords = new ArrayList<>(); var esFeature = new EsFeatureModel(); // mock a single point [147.338884, -43.190779]