From f645da5c1ced07b7d7a73a75c599fe2904d687f6 Mon Sep 17 00:00:00 2001 From: rng Date: Tue, 17 Feb 2026 16:01:14 +1100 Subject: [PATCH 1/7] Refactor before change --- .../core/model/enumeration/FeatureId.java | 1 + .../server/core/model/ogc/FeatureRequest.java | 4 +- .../service/wfs/DownloadWfsDataService.java | 4 +- .../server/core/service/wfs/WfsServer.java | 88 +++++++++++++++++-- .../server/core/service/wms/WmsServer.java | 33 ++++--- .../aodn/ogcapi/server/features/RestApi.java | 11 ++- .../ogcapi/server/features/RestServices.java | 10 ++- .../wfs/DownloadWfsDataServiceTest.java | 10 +-- .../core/service/wfs/WfsServerTest.java | 20 ++--- 9 files changed, 138 insertions(+), 43 deletions(-) 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..75cf93d7 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 @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.experimental.SuperBuilder; import java.io.Serializable; import java.math.BigDecimal; @@ -11,7 +12,7 @@ @Schema(description = "Query parameters for feature requests") @Data -@Builder +@SuperBuilder(toBuilder = true) @EqualsAndHashCode public class FeatureRequest implements Serializable { @Schema(description = "Property to be return") @@ -44,6 +45,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/service/wfs/DownloadWfsDataService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java index 27212868..eb67aaa5 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 @@ -113,7 +113,7 @@ public String prepareWfsRequestUrl( 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 +121,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..8be5f1c4 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 @@ -16,10 +16,13 @@ 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.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 +55,15 @@ public class WfsServer { @Autowired protected WfsServer self; + /** + * Internal use only to compress the number of argument pass on function call. + */ + @Getter + @SuperBuilder(toBuilder = true) + public static class WfsFeatureRequest extends FeatureRequest { + private String server; + } + public WfsServer(Search search, RestTemplate restTemplate, RestTemplateUtils restTemplateUtils, @@ -152,7 +164,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,6 +183,41 @@ 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"); + param.put("propertyName", String.join(",", request.getProperties())); + + // 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., ) @@ -206,19 +253,42 @@ protected static WFSFieldModel convertWfsResponseToDownloadableFields(WfsDescrib .build(); } + public T getFieldValues(String collectionId, FeatureRequest request, ParameterizedTypeReference tClass) { + Optional> mapFeatureUrl = 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 (RestClientException 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 WFSFieldModel 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 +321,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) { + 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 +339,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); + WFSFieldModel 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..4c3564e3 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 @@ -583,6 +583,22 @@ public DescribeLayerResponse describeLayer(String collectionId, FeatureRequest r return null; } + protected 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 +606,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 WFSFieldModel 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); } /** 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..3ba4fa44 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 @@ -7,6 +7,7 @@ import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureId; import au.org.aodn.ogcapi.server.core.service.OGCApiService; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; +import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -17,6 +18,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -125,7 +127,14 @@ public ResponseEntity getFeature( return featuresService.getWaveBuoyData(collectionId, request.getDatetime(), request.getWaveBuoy()); } case wfs_fields -> { - return featuresService.getWfsFields(collectionId, request); + WfsServer.WfsFeatureRequest wfsFeatureRequest = WfsServer.WfsFeatureRequest.builder().build(); + BeanUtils.copyProperties(request, wfsFeatureRequest); + return featuresService.getWfsFields(collectionId, wfsFeatureRequest); + } + case wfs_field_value -> { + WfsServer.WfsFeatureRequest wfsFeatureRequest = WfsServer.WfsFeatureRequest.builder().build(); + BeanUtils.copyProperties(request, wfsFeatureRequest); + return featuresService.getWfsFieldValue(collectionId, wfsFeatureRequest); } 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..65c235a4 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 @@ -17,6 +17,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import lombok.extern.slf4j.Slf4j; 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; @@ -92,7 +93,7 @@ public ResponseEntity getWmsMapTile(String collectionId, FeatureRequest * @param request -Request to get field given a WFS layer name; if no layer name provided, it will return fields for all WFS links in the collection * @return - The WFS fields */ - public ResponseEntity getWfsFields(String collectionId, FeatureRequest request) { + public ResponseEntity getWfsFields(String collectionId, WfsServer.WfsFeatureRequest request) { List result = wfsServer.getWFSFields(collectionId, request); return result == null ? @@ -100,6 +101,13 @@ public ResponseEntity getWfsFields(String collectionId, FeatureRequest reques ResponseEntity.ok(result); } + public ResponseEntity getWfsFieldValue(String collectionId, WfsServer.WfsFeatureRequest request) { + List result = wfsServer.getFieldValues(collectionId, request, new ParameterizedTypeReference<>() {}); + + return result == null ? + ResponseEntity.notFound().build() : + ResponseEntity.ok(result); + } /** * This is used to get the WMS fields from the describe wfs layer given a wms layer * 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..6f9b515f 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 @@ -108,7 +108,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( @@ -140,7 +140,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( @@ -174,7 +174,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( @@ -210,7 +210,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( @@ -244,7 +244,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..df217a9e 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 @@ -140,7 +140,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 +164,7 @@ public void testGetDownloadableFieldsSuccess() { .thenReturn(stac); WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); - WFSFieldModel result = server.getDownloadableFields(id, request, null); + WFSFieldModel result = server.getDownloadableFields(id, request); assertNotNull(result); assertNotNull(result.getFields()); @@ -202,7 +202,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 +229,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 +240,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 +267,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 +275,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 +303,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 +311,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 +322,7 @@ public void testGetDownloadableFieldsNoCollection() { WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); - WFSFieldModel result = server.getDownloadableFields(id, request, null); + WFSFieldModel result = server.getDownloadableFields(id, request); assertNull(result, "Should return null when no collection found"); } From 1228ccc6aae91d1383b20b8e8a3bc1d471ae3c15 Mon Sep 17 00:00:00 2001 From: rng Date: Wed, 18 Feb 2026 10:21:27 +1100 Subject: [PATCH 2/7] Add getWfsFieldValue function --- .../server/core/model/ogc/FeatureRequest.java | 7 ++- .../core/model/ogc/wfs/WFSFieldModel.java | 31 ------------- .../server/core/model/ogc/wfs/WfsField.java | 18 ++++++++ .../server/core/model/ogc/wfs/WfsFields.java | 18 ++++++++ .../service/wfs/DownloadWfsDataService.java | 13 +++--- .../server/core/service/wfs/WfsServer.java | 38 +++++++++------- .../server/core/service/wms/WmsServer.java | 23 +++++----- .../ogcapi/server/core/util/CommonUtils.java | 22 +++++++++ .../aodn/ogcapi/server/features/RestApi.java | 10 +---- .../ogcapi/server/features/RestServices.java | 39 +++++++++++----- .../wfs/DownloadWfsDataServiceTest.java | 23 +++++----- .../core/service/wfs/WfsServerTest.java | 13 +++--- .../server/features/RestServicesTest.java | 45 +++++++++++++++++++ 13 files changed, 197 insertions(+), 103 deletions(-) delete mode 100644 server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java create mode 100644 server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsField.java create mode 100644 server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsFields.java 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 75cf93d7..3d49cde4 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,7 @@ 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; @@ -12,7 +10,8 @@ @Schema(description = "Query parameters for feature requests") @Data -@SuperBuilder(toBuilder = true) +@SuperBuilder +@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED) // Need when using @SuperBuilder @EqualsAndHashCode public class FeatureRequest implements Serializable { @Schema(description = "Property to be return") 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..bc454a81 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsField.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; + +@Data +@Builder +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/wfs/DownloadWfsDataService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java index eb67aaa5..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,7 +107,7 @@ 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) { 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 8be5f1c4..dc04fcfe 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; @@ -17,6 +14,7 @@ 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; @@ -59,7 +57,8 @@ public class WfsServer { * Internal use only to compress the number of argument pass on function call. */ @Getter - @SuperBuilder(toBuilder = true) + @Setter + @SuperBuilder public static class WfsFeatureRequest extends FeatureRequest { private String server; } @@ -195,6 +194,11 @@ protected String createFeatureValueQueryUrl(String url, FeatureRequest request) param.put("TYPENAME", request.getLayerName()); param.put("outputFormat", "application/json"); param.put("propertyName", String.join(",", request.getProperties())); + param.put("sortBy", String.join( + ",", + // Assume always sort by desc + request.getProperties().stream().map(p -> String.format("%s+D", p)).toList()) + ); // This is the normal route UriComponentsBuilder builder = UriComponentsBuilder @@ -222,13 +226,13 @@ protected String createFeatureValueQueryUrl(String url, FeatureRequest request) * 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) @@ -239,7 +243,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" @@ -247,14 +251,16 @@ 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, FeatureRequest request, ParameterizedTypeReference tClass) { - Optional> mapFeatureUrl = getAllFeatureServerUrls(collectionId); + 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 @@ -270,7 +276,7 @@ public T getFieldValues(String collectionId, FeatureRequest request, Paramet return response.getBody(); } } - } catch (RestClientException e) { + } catch (Exception e) { log.debug("Ignore error for {}, will try another url", uri); } } @@ -285,7 +291,7 @@ public T getFieldValues(String collectionId, FeatureRequest request, Paramet * @return - WFSFieldModel containing typename and fields */ @Cacheable(value = DOWNLOADABLE_FIELDS) - public WFSFieldModel getDownloadableFields(String collectionId, WfsFeatureRequest request) { + public WfsFields getDownloadableFields(String collectionId, WfsFeatureRequest request) { Optional> mapFeatureUrl = request.getServer() != null ? Optional.of(List.of(request.getServer())) : @@ -321,8 +327,8 @@ public WFSFieldModel getDownloadableFields(String collectionId, WfsFeatureReques throw new GeoserverFieldsNotFoundException("No downloadable fields found for all url"); } - public List getWFSFields(String collectionId, WfsServer.WfsFeatureRequest 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 @@ -344,7 +350,7 @@ public List getWFSFields(String collectionId, WfsServer.WfsFeatur .build(); try { - WFSFieldModel fields = self.getDownloadableFields(collectionId, requestModified); + 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 4c3564e3..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,7 +584,7 @@ public DescribeLayerResponse describeLayer(String collectionId, FeatureRequest r return null; } - protected WfsServer.WfsFeatureRequest createRequestFromLayerName(String collectionId, String layerName) { + public WfsServer.WfsFeatureRequest createRequestFromLayerName(String collectionId, String layerName) { FeatureRequest layerRequest = FeatureRequest.builder().layerName(layerName).build(); DescribeLayerResponse response = this.describeLayer(collectionId, layerRequest); @@ -606,7 +607,7 @@ protected WfsServer.WfsFeatureRequest createRequestFromLayerName(String collecti * @param layerName - The layer name to fetch fields for * @return - WFSFieldModel containing typename and fields, or null if not found */ - protected WFSFieldModel fetchFieldsForLayer(String collectionId, String layerName) { + protected WfsFields fetchFieldsForLayer(String collectionId, String layerName) { WfsServer.WfsFeatureRequest request = createRequestFromLayerName(collectionId, layerName); return wfsServer.getDownloadableFields(collectionId, request); @@ -619,8 +620,8 @@ protected WFSFieldModel fetchFieldsForLayer(String collectionId, String layerNam * @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 @@ -637,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 3ba4fa44..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 @@ -7,7 +7,6 @@ import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureId; import au.org.aodn.ogcapi.server.core.service.OGCApiService; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import org.springdoc.core.annotations.ParameterObject; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -127,14 +125,10 @@ public ResponseEntity getFeature( return featuresService.getWaveBuoyData(collectionId, request.getDatetime(), request.getWaveBuoy()); } case wfs_fields -> { - WfsServer.WfsFeatureRequest wfsFeatureRequest = WfsServer.WfsFeatureRequest.builder().build(); - BeanUtils.copyProperties(request, wfsFeatureRequest); - return featuresService.getWfsFields(collectionId, wfsFeatureRequest); + return featuresService.getWfsFields(collectionId, request); } case wfs_field_value -> { - WfsServer.WfsFeatureRequest wfsFeatureRequest = WfsServer.WfsFeatureRequest.builder().build(); - BeanUtils.copyProperties(request, wfsFeatureRequest); - return featuresService.getWfsFieldValue(collectionId, wfsFeatureRequest); + 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 65c235a4..396d5cb6 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 @@ -8,14 +8,18 @@ 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.geojson.feature.FeatureJSON; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; @@ -23,6 +27,8 @@ 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; @@ -93,20 +99,34 @@ public ResponseEntity getWmsMapTile(String collectionId, FeatureRequest * @param request -Request to get field given a WFS layer name; if no layer name provided, it will return fields for all WFS links in the collection * @return - The WFS fields */ - public ResponseEntity getWfsFields(String collectionId, WfsServer.WfsFeatureRequest request) { - List result = wfsServer.getWFSFields(collectionId, request); + public ResponseEntity getWfsFields(String collectionId, FeatureRequest 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); } - public ResponseEntity getWfsFieldValue(String collectionId, WfsServer.WfsFeatureRequest request) { - List result = wfsServer.getFieldValues(collectionId, request, new ParameterizedTypeReference<>() {}); + 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); - return result == null ? - ResponseEntity.notFound().build() : - ResponseEntity.ok(result); + String result = wfsServer.getFieldValues(collectionId, wfsFeatureRequest, new ParameterizedTypeReference<>() {}); + + FeatureJSON json = new FeatureJSON(); + try { + FeatureCollection collection = json.readFeatureCollection(new StringReader(result)); + return ResponseEntity.ok().body(collection); + } + 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 @@ -120,7 +140,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() : @@ -160,7 +180,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 6f9b515f..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); @@ -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); @@ -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); @@ -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); @@ -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); 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 df217a9e..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; @@ -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); + 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); @@ -322,7 +323,7 @@ public void testGetDownloadableFieldsNoCollection() { WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); - WFSFieldModel result = server.getDownloadableFields(id, request); + 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..48e532a6 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,22 @@ package au.org.aodn.ogcapi.server.features; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; public class RestServicesTest { @@ -18,6 +24,12 @@ public class RestServicesTest { @Mock private DasService dasService; + @Mock + private WmsServer wmsServer; + + @Mock + private WfsServer wfsServer; + @InjectMocks private RestServices restServices; @@ -65,4 +77,37 @@ 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" + } + } + ] + } + """ + ); + + ResponseEntity response = restServices.getWfsFieldValue("any-works", FeatureRequest.builder().build()); + + } } From 60a260f528b3e6456e9732bad214ea63a4dcfcf9 Mon Sep 17 00:00:00 2001 From: rng Date: Wed, 18 Feb 2026 11:48:45 +1100 Subject: [PATCH 3/7] Fix test --- .../server/core/model/ogc/FeatureRequest.java | 8 ++++- .../server/core/model/ogc/wfs/WfsField.java | 2 ++ .../server/core/service/ElasticSearch.java | 3 +- .../server/core/service/OGCApiService.java | 13 ++++---- .../ogcapi/server/core/service/Search.java | 3 +- .../server/core/service/wfs/WfsServer.java | 5 +++- .../ogcapi/server/features/RestServices.java | 30 +++++++++++++++---- .../server/features/RestServicesTest.java | 12 +++++++- .../server/service/ElasticSearchTest.java | 3 +- 9 files changed, 62 insertions(+), 17 deletions(-) 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 3d49cde4..675a8f59 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 @@ -14,8 +14,14 @@ @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 { + TIME + } + @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; 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 index bc454a81..3caa2985 100644 --- 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 @@ -3,9 +3,11 @@ 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; 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/WfsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/WfsServer.java index dc04fcfe..49037a28 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 @@ -193,7 +193,10 @@ protected String createFeatureValueQueryUrl(String url, FeatureRequest request) // Now we add the missing argument from the request param.put("TYPENAME", request.getLayerName()); param.put("outputFormat", "application/json"); - param.put("propertyName", String.join(",", request.getProperties())); + param.put("propertyName", String.join( + ",", + request.getProperties().stream().map(v -> v.name().toLowerCase()).toList()) + ); param.put("sortBy", String.join( ",", // Assume always sort by desc 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 396d5cb6..179203f4 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 @@ -18,7 +18,10 @@ 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; @@ -30,8 +33,7 @@ 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") @@ -108,7 +110,13 @@ public ResponseEntity getWfsFields(String collectionId, FeatureRequest reques 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(); @@ -121,8 +129,20 @@ public ResponseEntity getWfsFieldValue(String collectionId, FeatureRequest re FeatureJSON json = new FeatureJSON(); try { - FeatureCollection collection = json.readFeatureCollection(new StringReader(result)); - return ResponseEntity.ok().body(collection); + @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()); 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 48e532a6..0b56f2b2 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 @@ -14,7 +14,10 @@ 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; @@ -108,6 +111,13 @@ public void testGetWfsTimeFieldWorks() { ); ResponseEntity response = restServices.getWfsFieldValue("any-works", FeatureRequest.builder().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)); } } 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..bb7c6e2c 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.TIME); String filter = null; SearchResponse mockResponse = mock(SearchResponse.class); From 26982fdc842c562d4481af9863fa3a97e2a95667 Mon Sep 17 00:00:00 2001 From: rng Date: Wed, 18 Feb 2026 12:10:04 +1100 Subject: [PATCH 4/7] Fix test --- .../ogcapi/server/features/RestServices.java | 13 ++++++++++++ .../server/features/RestServicesTest.java | 20 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) 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 179203f4..2c1d29ee 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 @@ -125,6 +125,19 @@ public ResponseEntity getWfsFieldValue(String collectionId, FeatureRequest re // copy enhanced value CommonUtils.copyIgnoringNull(wfsWithServer, wfsFeatureRequest); + // Now check if we need to map field + List supportedFields = wfsServer.getWFSFields(collectionId, wfsFeatureRequest); + List extractedName = supportedFields.get(0).getFields().stream() + .map(f -> f.getName().toLowerCase()) + .toList(); + + for(FeatureRequest.PropertyName name : wfsFeatureRequest.getProperties()) { + if(extractedName.contains(name.name().toLowerCase())) { + // 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(); 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 0b56f2b2..51f5e4d0 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,6 +1,8 @@ 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; @@ -110,7 +112,23 @@ public void testGetWfsTimeFieldWorks() { """ ); - ResponseEntity response = restServices.getWfsFieldValue("any-works", FeatureRequest.builder().build()); + 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") From 2491af7888de947c6ea7abc168c3d8bf63b38d5c Mon Sep 17 00:00:00 2001 From: rng Date: Wed, 18 Feb 2026 12:19:35 +1100 Subject: [PATCH 5/7] Add mapping check --- .../aodn/ogcapi/server/core/model/ogc/FeatureRequest.java | 2 +- .../org/aodn/ogcapi/server/core/service/wfs/WfsServer.java | 4 ++-- .../au/org/aodn/ogcapi/server/features/RestServices.java | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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 675a8f59..f45d2371 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 @@ -17,7 +17,7 @@ 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 { - TIME + time } @Schema(description = "Property to be return") 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 49037a28..45d8cf70 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 @@ -195,12 +195,12 @@ protected String createFeatureValueQueryUrl(String url, FeatureRequest request) param.put("outputFormat", "application/json"); param.put("propertyName", String.join( ",", - request.getProperties().stream().map(v -> v.name().toLowerCase()).toList()) + 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)).toList()) + request.getProperties().stream().map(p -> String.format("%s+D", p.name())).toList()) ); // This is the normal route 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 2c1d29ee..d86f51fc 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,6 +3,7 @@ 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; @@ -128,13 +129,14 @@ public ResponseEntity getWfsFieldValue(String collectionId, FeatureRequest re // Now check if we need to map field List supportedFields = wfsServer.getWFSFields(collectionId, wfsFeatureRequest); List extractedName = supportedFields.get(0).getFields().stream() - .map(f -> f.getName().toLowerCase()) + .map(WfsField::getName) .toList(); for(FeatureRequest.PropertyName name : wfsFeatureRequest.getProperties()) { - if(extractedName.contains(name.name().toLowerCase())) { + if(extractedName.contains(name.name())) { // TODO: If missing then may need map log.info("Field {} need map", name); + // } } From 302be488e722456b6773cf755eea25a00e3f4c95 Mon Sep 17 00:00:00 2001 From: rng Date: Wed, 18 Feb 2026 13:31:28 +1100 Subject: [PATCH 6/7] Add enum --- .../server/core/model/ogc/FeatureRequest.java | 10 ++++++++- .../server/core/service/wfs/WfsServer.java | 21 +++++++++++-------- .../server/features/RestServicesTest.java | 2 +- .../server/service/ElasticSearchTest.java | 4 ++-- 4 files changed, 24 insertions(+), 13 deletions(-) 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 f45d2371..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 @@ -17,7 +17,15 @@ 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 { - time + 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") 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 45d8cf70..78ea57f1 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 @@ -193,15 +193,18 @@ protected String createFeatureValueQueryUrl(String url, FeatureRequest request) // Now we add the missing argument from the request param.put("TYPENAME", request.getLayerName()); param.put("outputFormat", "application/json"); - 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()) - ); + + if(!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 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 51f5e4d0..55aae20d 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 @@ -126,7 +126,7 @@ public void testGetWfsTimeFieldWorks() { ResponseEntity response = restServices.getWfsFieldValue( "any-works", FeatureRequest.builder() - .properties(List.of(FeatureRequest.PropertyName.TIME)) + .properties(List.of(FeatureRequest.PropertyName.time)) .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 bb7c6e2c..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 @@ -51,7 +51,7 @@ public void searchFeatureSummaryTest() throws IOException { // Arrange String collectionId = "test-collection"; - List properties = List.of(FeatureRequest.PropertyName.TIME); + List properties = List.of(FeatureRequest.PropertyName.wildcard); String filter = null; SearchResponse mockResponse = mock(SearchResponse.class); @@ -62,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] From 35e4e381f1fc3d8b0e7b51317bda4e69b3c3ac34 Mon Sep 17 00:00:00 2001 From: rng Date: Wed, 18 Feb 2026 13:38:30 +1100 Subject: [PATCH 7/7] Check null --- .../server/core/service/wfs/WfsServer.java | 2 +- .../ogcapi/server/features/RestServices.java | 24 ++++++++++--------- .../server/features/RestServicesTest.java | 9 +++++++ 3 files changed, 23 insertions(+), 12 deletions(-) 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 78ea57f1..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 @@ -194,7 +194,7 @@ protected String createFeatureValueQueryUrl(String url, FeatureRequest request) param.put("TYPENAME", request.getLayerName()); param.put("outputFormat", "application/json"); - if(!request.getProperties().contains(FeatureRequest.PropertyName.wildcard)) { + if(request.getProperties() != null && !request.getProperties().contains(FeatureRequest.PropertyName.wildcard)) { param.put("propertyName", String.join( ",", request.getProperties().stream().map(Enum::name).toList()) 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 d86f51fc..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 @@ -126,17 +126,19 @@ public ResponseEntity getWfsFieldValue(String collectionId, FeatureRequest re // copy enhanced value CommonUtils.copyIgnoringNull(wfsWithServer, wfsFeatureRequest); - // 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); - // + 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); + // + } } } 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 55aae20d..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 @@ -137,5 +137,14 @@ public void testGetWfsTimeFieldWorks() { 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()); + } }