From 14640d0d584fec7499458bfcd3103e345ccca5be Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 24 Feb 2026 16:14:51 +1100 Subject: [PATCH 1/4] update wfs prepare download url --- .../service/wfs/DownloadWfsDataService.java | 51 ++++++++++--------- .../server/core/service/wfs/WfsServer.java | 18 ++++--- 2 files changed, 38 insertions(+), 31 deletions(-) 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 03e914b1..d936a6c3 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 @@ -43,6 +43,7 @@ public DownloadWfsDataService( this.pretendUserEntity = pretendUserEntity; this.chunkSize = chunkSize; } + /** * Build CQL filter for temporal and spatial constraints */ @@ -64,7 +65,7 @@ protected String buildCqlFilter(String startDate, String endDate, Object multiPo if (!temporalField.isEmpty() && startDate != null && !startDate.isEmpty() && endDate != null && !endDate.isEmpty()) { List cqls = new ArrayList<>(); temporalField.forEach(temp -> - cqls.add(String.format("(%s DURING %sT00:00:00Z/%sT23:59:59Z)", temp.getName(), startDate, endDate)) + cqls.add(String.format("(%s DURING %sT00:00:00Z/%sT23:59:59Z)", temp.getName(), startDate, endDate)) ); cqlFilter.append("(").append(String.join(" OR ", cqls)).append(")"); } @@ -95,6 +96,7 @@ protected String buildCqlFilter(String startDate, String endDate, Object multiPo return cqlFilter.toString(); } + /** * Does collection lookup, WFS validation, field retrieval, and URL building */ @@ -107,17 +109,31 @@ public String prepareWfsRequestUrl( String layerName, String outputFormat) { - DescribeLayerResponse describeLayerResponse = wmsServer.describeLayer(uuid, FeatureRequest.builder().layerName(layerName).build()); +// DescribeLayerResponse describeLayerResponse = wmsServer.describeLayer(uuid, FeatureRequest.builder().layerName(layerName).build()); String wfsServerUrl; String wfsTypeName; 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(); - + // We trust the layername from request to be valid +// if (describeLayerResponse != null && describeLayerResponse.getLayerDescription().getWfs() != null) { +// wfsServerUrl = describeLayerResponse.getLayerDescription().getWfs(); +// wfsTypeName = describeLayerResponse.getLayerDescription().getQuery().getTypeName(); +// +// wfsFieldModel = wfsServer.getDownloadableFields( +// uuid, +// WfsServer.WfsFeatureRequest.builder() +// .layerName(layerName) +// .server(wfsServerUrl) +// .build() +// ); +// log.info("WFSFieldModel by describeLayer: {}", wfsFieldModel); +// } else { + Optional featureServerUrl = wfsServer.getFeatureServerUrlByTitleOrQueryParam(uuid, layerName); + + if (featureServerUrl.isPresent()) { + wfsServerUrl = featureServerUrl.get(); + wfsTypeName = layerName; wfsFieldModel = wfsServer.getDownloadableFields( uuid, WfsServer.WfsFeatureRequest.builder() @@ -125,25 +141,11 @@ public String prepareWfsRequestUrl( .server(wfsServerUrl) .build() ); - log.info("WFSFieldModel by describeLayer: {}", wfsFieldModel); + log.info("WFSFieldModel by wfs typename: {}", wfsFieldModel); } else { - Optional featureServerUrl = wfsServer.getFeatureServerUrlByTitle(uuid, layerName); - - if (featureServerUrl.isPresent()) { - wfsServerUrl = featureServerUrl.get(); - wfsTypeName = layerName; - 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"); - } + throw new IllegalArgumentException("No WFS server URL found for the given UUID and layer name"); } +// } // Validate start and end dates String validStartDate = DatetimeUtils.validateAndFormatDate(startDate, true); @@ -163,6 +165,7 @@ public String prepareWfsRequestUrl( log.info("Prepared WFS request URL: {}", wfsRequestUrl); return wfsRequestUrl; } + /** * Execute WFS request with SSE support */ 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 4013a688..f84314fa 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 @@ -79,6 +79,7 @@ public WfsServer(Search search, this.pretendUserEntity = entity; this.wfsDefaultParam = wfsDefaultParam; } + /** * Build WFS GetFeature URL */ @@ -90,7 +91,7 @@ protected String createWfsRequestUrl(String wfsUrl, String layerName, List T getFieldValues(String collectionId, WfsFeatureRequest request, Para if (uri != null) { ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, pretendUserEntity, tClass - ); + ); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { return response.getBody(); @@ -289,11 +291,12 @@ public T getFieldValues(String collectionId, WfsFeatureRequest request, Para } 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 collectionId - The uuid of the collection + * @param request - The feature request containing the layer name * @return - WFSFieldModel containing typename and fields */ @Cacheable(value = DOWNLOADABLE_FIELDS) @@ -416,7 +419,7 @@ public Optional getFeatureServerUrlByTitle(String collectionId, String l return model.getLinks() .stream() .filter(link -> link.getAiGroup() != null) - .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER) && link.getTitle().equalsIgnoreCase(layerName)) + .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER) && roughlyMatch(link.getTitle(), layerName)) .map(LinkModel::getHref) .findFirst(); } else { @@ -435,13 +438,14 @@ public Optional getFeatureServerUrlByTitleOrQueryParam(String collection ElasticSearchBase.SearchResult result = search.searchCollections(collectionId); if (!result.getCollections().isEmpty()) { StacCollectionModel model = result.getCollections().get(0); + log.info("start to find wfs link for collectionId {} with layerName {}, total links to check {}", collectionId, layerName, model.getLinks().size()); return model.getLinks() .stream() .filter(link -> link.getAiGroup() != null) .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER)) .filter(link -> { Optional name = extractLayernameOrTypenameFromUrl(link.getHref()); - return link.getTitle().equalsIgnoreCase(layerName) || + return roughlyMatch(link.getTitle(), layerName) || (name.isPresent() && roughlyMatch(name.get(), layerName)); }) .map(LinkModel::getHref) From 1c4fe0feab0fa78608fb06c540f1584c4102b2a4 Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 24 Feb 2026 16:34:04 +1100 Subject: [PATCH 2/4] refactor and update test --- .../service/wfs/DownloadWfsDataService.java | 26 +---- .../server/core/service/wfs/WfsServer.java | 41 ++++--- .../wfs/DownloadWfsDataServiceTest.java | 101 ++++-------------- 3 files changed, 42 insertions(+), 126 deletions(-) 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 d936a6c3..59b6e076 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 @@ -3,7 +3,6 @@ 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.model.ogc.wms.DescribeLayerResponse; import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; import au.org.aodn.ogcapi.server.core.util.DatetimeUtils; import au.org.aodn.ogcapi.server.core.util.GeometryUtils; @@ -24,7 +23,6 @@ @Slf4j @Service public class DownloadWfsDataService { - private final WmsServer wmsServer; private final WfsServer wfsServer; private final RestTemplate restTemplate; private final HttpEntity pretendUserEntity; @@ -37,7 +35,6 @@ public DownloadWfsDataService( @Qualifier("pretendUserEntity") HttpEntity pretendUserEntity, @Value("${app.sse.chunkSize:16384}") int chunkSize ) { - this.wmsServer = wmsServer; this.wfsServer = wfsServer; this.restTemplate = restTemplate; this.pretendUserEntity = pretendUserEntity; @@ -109,28 +106,14 @@ public String prepareWfsRequestUrl( String layerName, String outputFormat) { -// DescribeLayerResponse describeLayerResponse = wmsServer.describeLayer(uuid, FeatureRequest.builder().layerName(layerName).build()); - String wfsServerUrl; String wfsTypeName; WfsFields wfsFieldModel; - // We trust the layername from request to be valid -// if (describeLayerResponse != null && describeLayerResponse.getLayerDescription().getWfs() != null) { -// wfsServerUrl = describeLayerResponse.getLayerDescription().getWfs(); -// wfsTypeName = describeLayerResponse.getLayerDescription().getQuery().getTypeName(); -// -// wfsFieldModel = wfsServer.getDownloadableFields( -// uuid, -// WfsServer.WfsFeatureRequest.builder() -// .layerName(layerName) -// .server(wfsServerUrl) -// .build() -// ); -// log.info("WFSFieldModel by describeLayer: {}", wfsFieldModel); -// } else { - Optional featureServerUrl = wfsServer.getFeatureServerUrlByTitleOrQueryParam(uuid, layerName); + // Get WFS server URL and field model for the given UUID and layer name + Optional featureServerUrl = wfsServer.getFeatureServerUrl(uuid, layerName); + // Get the wfs fields to build the CQL filter if (featureServerUrl.isPresent()) { wfsServerUrl = featureServerUrl.get(); wfsTypeName = layerName; @@ -141,11 +124,10 @@ public String prepareWfsRequestUrl( .server(wfsServerUrl) .build() ); - log.info("WFSFieldModel by wfs typename: {}", wfsFieldModel); + log.debug("WFSFieldModel by wfs typename: {}", wfsFieldModel); } else { throw new IllegalArgumentException("No WFS server URL found for the given UUID and layer name"); } -// } // Validate start and end dates String validStartDate = DatetimeUtils.validateAndFormatDate(startDate, true); 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 f84314fa..5a64abfe 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 @@ -405,27 +405,6 @@ protected Optional> getAllFeatureServerUrls(String collectionId) { } } - /** - * Find the url that is able to get WFS call, this can be found in ai:Group - * - * @param collectionId - The uuid - * @param layerName - The layer name to match the title - * @return - The first wfs server link if found - */ - public Optional getFeatureServerUrlByTitle(String collectionId, String layerName) { - ElasticSearchBase.SearchResult result = search.searchCollections(collectionId); - if (!result.getCollections().isEmpty()) { - StacCollectionModel model = result.getCollections().get(0); - return model.getLinks() - .stream() - .filter(link -> link.getAiGroup() != null) - .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER) && roughlyMatch(link.getTitle(), layerName)) - .map(LinkModel::getHref) - .findFirst(); - } else { - return Optional.empty(); - } - } /** * Find the url that is able to get WFS call, this can be found in ai:Group @@ -455,6 +434,26 @@ public Optional getFeatureServerUrlByTitleOrQueryParam(String collection } } + + /** + * Find the WFS server URL for a given collection and layer name. + * First tries to match by title or query param, then falls back to the first available WFS link. + * + * @param collectionId - The uuid + * @param layerName - The layer name to match the title + * @return - The matched wfs server link, or the first available one if no match found + */ + public Optional getFeatureServerUrl(String collectionId, String layerName) { + Optional url = getFeatureServerUrlByTitleOrQueryParam(collectionId, layerName); + if (url.isPresent()) { + return url; + } + + log.debug("No WFS link matched by title/query param for collectionId {} with layerName {}, falling back to first available WFS link", collectionId, layerName); + Optional> allUrls = getAllFeatureServerUrls(collectionId); + return allUrls.filter(list -> !list.isEmpty()).map(list -> list.get(0)); + } + /** * Fetch raw feature types from WFS GetCapabilities - cached by URL. * This allows multiple collections sharing the same WFS server to use cached results. 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 ef8024aa..e2a389e8 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,9 +1,7 @@ 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.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; import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils; @@ -25,6 +23,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Optional; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -100,17 +100,8 @@ public void testPrepareWfsRequestUrl_WithNullDates() { String layerName = "test:layer"; WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); @@ -132,17 +123,8 @@ public void testPrepareWfsRequestUrl_WithEmptyDates() { String layerName = "test:layer"; WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); @@ -166,17 +148,8 @@ public void testPrepareWfsRequestUrl_WithValidDates() { String endDate = "2023-12-31"; WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); @@ -202,17 +175,8 @@ public void testPrepareWfsRequestUrl_WithOnlyStartDate() { String startDate = "2023-01-01"; WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); @@ -236,17 +200,8 @@ public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { String endDate = "12-2023"; // MM-YYYY format WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); @@ -269,10 +224,8 @@ public void testPrepareWfsRequestUrl_NoWfsServerUrl() { String uuid = "test-uuid"; String layerName = "test:layer"; - doReturn(null) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); - doReturn(java.util.Optional.empty()) - .when(wfsServer).getFeatureServerUrlByTitle(eq(uuid), eq(layerName)); + doReturn(Optional.empty()) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); // Test with no WFS server URL available Exception exception = assertThrows(IllegalArgumentException.class, () -> downloadWfsDataService.prepareWfsRequestUrl( @@ -291,17 +244,8 @@ public void verifyRequestUrlGenerateCorrect() { String endDate = "12-2024"; // MM-YYYY format WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); @@ -330,17 +274,8 @@ public void verifyRequestUrlGenerateCorrectWithPolygon() throws JsonProcessingEx String endDate = "12-2024"; // MM-YYYY format WfsFields wfsFieldModel = createTestWFSFieldModel(); - DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); - DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); - DescribeLayerResponse.Query query = mock(DescribeLayerResponse.Query.class); - - when(describeLayerResponse.getLayerDescription()).thenReturn(layerDescription); - when(layerDescription.getWfs()).thenReturn("https://test.com/geoserver/wfs"); - when(layerDescription.getQuery()).thenReturn(query); - when(query.getTypeName()).thenReturn(layerName); - - doReturn(describeLayerResponse) - .when(wmsServer).describeLayer(eq(uuid), any(FeatureRequest.class)); + doReturn(Optional.of("https://test.com/geoserver/wfs")) + .when(wfsServer).getFeatureServerUrl(eq(uuid), eq(layerName)); doReturn(wfsFieldModel) .when(wfsServer).getDownloadableFields(eq(uuid), any(WfsServer.WfsFeatureRequest.class)); From e10c0caf9b6077c5ef1758a32f6f71d7b7fee1ea Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 24 Feb 2026 17:03:36 +1100 Subject: [PATCH 3/4] fix warning --- .../service/wfs/DownloadWfsDataService.java | 2 - .../wfs/DownloadWfsDataServiceTest.java | 4 +- .../wfs/DownloadWfsDataServiceTest.java | 64 +++++++++---------- 3 files changed, 35 insertions(+), 35 deletions(-) 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 59b6e076..f99e061b 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 @@ -3,7 +3,6 @@ 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.wms.WmsServer; import au.org.aodn.ogcapi.server.core.util.DatetimeUtils; import au.org.aodn.ogcapi.server.core.util.GeometryUtils; import lombok.extern.slf4j.Slf4j; @@ -29,7 +28,6 @@ public class DownloadWfsDataService { private final int chunkSize; public DownloadWfsDataService( - WmsServer wmsServer, WfsServer wfsServer, RestTemplate restTemplate, @Qualifier("pretendUserEntity") HttpEntity pretendUserEntity, 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 e2a389e8..b900003c 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 @@ -63,7 +63,7 @@ public void setUp() { wmsServer = Mockito.spy(new WmsServer(search, wfsServer, pretendUserEntity)); downloadWfsDataService = new DownloadWfsDataService( - wmsServer, wfsServer, restTemplate, pretendUserEntity, 16384 + wfsServer, restTemplate, pretendUserEntity, 16384 ); } @@ -261,8 +261,10 @@ public void verifyRequestUrlGenerateCorrect() { ); assertEquals("https://test.com/geoserver/wfs?VERSION=1.0.0&typeName=test:layer&SERVICE=WFS&REQUEST=GetFeature&outputFormat=shape-zip&cql_filter=((timestamp DURING 2024-01-01T00:00:00Z/2024-12-31T23:59:59Z))", result, "Correct url 1"); } + /** * Make sure the url generated contains the correct polygon + * * @throws JsonProcessingException - Not expected */ @Test diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/DownloadWfsDataServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/DownloadWfsDataServiceTest.java index c10bc669..4fd79852 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/DownloadWfsDataServiceTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/DownloadWfsDataServiceTest.java @@ -3,7 +3,6 @@ import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.service.wfs.DownloadWfsDataService; 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.Test; import org.mockito.Mock; import org.springframework.http.HttpEntity; @@ -30,13 +29,12 @@ public class DownloadWfsDataServiceTest { @Mock protected WfsServer wfsServer; - @Mock - protected WmsServer wmsServer; - @Mock protected HttpEntity entity; + /** * Test a text file from source is break down into chunk correct and reconstruct correctly + * * @throws Exception - Not expected */ @Test @@ -45,7 +43,7 @@ void verifyDecodeTextCorrectlyForSSE() throws Exception { RestTemplate restTemplateMock = mock(RestTemplate.class); // Intended to set a very small chunk size to test the edge case DownloadWfsDataService service = new DownloadWfsDataService( - wmsServer, wfsServer, restTemplateMock, entity, 10); + wfsServer, restTemplateMock, entity, 10); String original = "id,name,age,city\n1,Alice,30,Sydney\n2,Bob,25,Melbourne\n3,„Café“,42,Perth\n"; byte[] originalBytes = original.getBytes(StandardCharsets.UTF_8); @@ -67,11 +65,11 @@ void verifyDecodeTextCorrectlyForSSE() throws Exception { var d = builder.build(); d.forEach(s -> { - if(s.getData() instanceof Map) { + if (s.getData() instanceof Map) { @SuppressWarnings("unchecked") - Map data = (Map)s.getData(); + Map data = (Map) s.getData(); - if(data.containsKey("data")) { + if (data.containsKey("data")) { base64Chunks.add((String) data.get("data")); } if (data.containsKey("filename")) { @@ -105,8 +103,10 @@ void verifyDecodeTextCorrectlyForSSE() throws Exception { assertEquals(original, result); } + /** * Test a binary file from source is break down into chunk correct and reconstruct correctly + * * @throws Exception - Not expected */ @Test @@ -115,47 +115,47 @@ void verifyDecodeBinaryCorrectlyForSSE() throws Exception { RestTemplate restTemplateMock = mock(RestTemplate.class); // Intended to set a very small chunk size to test the edge case DownloadWfsDataService service = new DownloadWfsDataService( - wmsServer, wfsServer, restTemplateMock, entity, 10); + wfsServer, restTemplateMock, entity, 10); // Just some big enough random binary to trigger chunk split - byte[] originalBytes = new byte[] { + byte[] originalBytes = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - (byte)0xFF, (byte)0xFE, (byte)0xFD, (byte)0xFC, (byte)0xFB, (byte)0xFA, (byte)0xF9, (byte)0xF8, - (byte)0x80, (byte)0x81, (byte)0x82, (byte)0x83, 0x7F, 0x7E, 0x7D, 0x7C, + (byte) 0xFF, (byte) 0xFE, (byte) 0xFD, (byte) 0xFC, (byte) 0xFB, (byte) 0xFA, (byte) 0xF9, (byte) 0xF8, + (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, 0x7F, 0x7E, 0x7D, 0x7C, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, - (byte)0xA0, (byte)0xA1, (byte)0xA2, (byte)0xA3, (byte)0xA4, (byte)0xA5, (byte)0xA6, (byte)0xA7, - (byte)0xB0, (byte)0xB1, (byte)0xB2, (byte)0xB3, (byte)0xB4, (byte)0xB5, (byte)0xB6, (byte)0xB7, + (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, + (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, - (byte)0xC0, (byte)0xC1, (byte)0xC2, (byte)0xC3, (byte)0xC4, (byte)0xC5, (byte)0xC6, (byte)0xC7, - (byte)0xD0, (byte)0xD1, (byte)0xD2, (byte)0xD3, (byte)0xD4, (byte)0xD5, (byte)0xD6, (byte)0xD7, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, - (byte)0xE0, (byte)0xE1, (byte)0xE2, (byte)0xE3, (byte)0xE4, (byte)0xE5, (byte)0xE6, (byte)0xE7, - (byte)0xF0, (byte)0xF1, (byte)0xF2, (byte)0xF3, (byte)0xF4, (byte)0xF5, (byte)0xF6, (byte)0xF7, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, - (byte)0xFF, (byte)0xFE, (byte)0xFD, (byte)0xFC, (byte)0xFB, (byte)0xFA, (byte)0xF9, (byte)0xF8, - (byte)0x80, (byte)0x81, (byte)0x82, (byte)0x83, 0x7F, 0x7E, 0x7D, 0x7C, + (byte) 0xFF, (byte) 0xFE, (byte) 0xFD, (byte) 0xFC, (byte) 0xFB, (byte) 0xFA, (byte) 0xF9, (byte) 0xF8, + (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, 0x7F, 0x7E, 0x7D, 0x7C, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, - (byte)0xA0, (byte)0xA1, (byte)0xA2, (byte)0xA3, (byte)0xA4, (byte)0xA5, (byte)0xA6, (byte)0xA7, - (byte)0xB0, (byte)0xB1, (byte)0xB2, (byte)0xB3, (byte)0xB4, (byte)0xB5, (byte)0xB6, (byte)0xB7, + (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, + (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, - (byte)0xC0, (byte)0xC1, (byte)0xC2, (byte)0xC3, (byte)0xC4, (byte)0xC5, (byte)0xC6, (byte)0xC7, - (byte)0xD0, (byte)0xD1, (byte)0xD2, (byte)0xD3, (byte)0xD4, (byte)0xD5, (byte)0xD6, (byte)0xD7, + (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, - (byte)0xE0, (byte)0xE1, (byte)0xE2, (byte)0xE3, (byte)0xE4, (byte)0xE5, (byte)0xE6, (byte)0xE7, - (byte)0xF0, (byte)0xF1, (byte)0xF2, (byte)0xF3, (byte)0xF4, (byte)0xF5, (byte)0xF6, (byte)0xF7, - (byte)0xD0, (byte)0xD1, (byte)0xD2, (byte)0xD3, (byte)0xD4, (byte)0xD5, (byte)0xD6, (byte)0xD7, + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, + (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, - (byte)0xE0, (byte)0xE1, (byte)0xE2, (byte)0xE3, (byte)0xE4, (byte)0xE5, (byte)0xE6, (byte)0xE7, - (byte)0xF0, (byte)0xF1, (byte)0xF2, (byte)0xF3, (byte)0xF4, (byte)0xF5 + (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, + (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5 }; // Mock WFS response @@ -175,11 +175,11 @@ void verifyDecodeBinaryCorrectlyForSSE() throws Exception { var d = builder.build(); d.forEach(s -> { - if(s.getData() instanceof Map) { + if (s.getData() instanceof Map) { @SuppressWarnings("unchecked") - Map data = (Map)s.getData(); + Map data = (Map) s.getData(); - if(data.containsKey("data")) { + if (data.containsKey("data")) { base64Chunks.add((String) data.get("data")); } if (data.containsKey("filename")) { From bfea71ca9a7b0fa4c1b5817bbf29016b09eb2594 Mon Sep 17 00:00:00 2001 From: Lyn Long Date: Tue, 24 Feb 2026 17:27:40 +1100 Subject: [PATCH 4/4] fix wrong server url that contains unexpected params --- .../server/core/service/wfs/WfsServer.java | 12 ++++- .../core/service/wfs/WfsServerTest.java | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 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 5a64abfe..4a3c29a8 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 @@ -84,8 +84,15 @@ public WfsServer(Search search, * Build WFS GetFeature URL */ protected String createWfsRequestUrl(String wfsUrl, String layerName, List fields, String cqlFilter, String outputFormat) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(wfsUrl) - .scheme("https"); // Force HTTPS to fix redirect + UriComponents components = UriComponentsBuilder.fromUriString(wfsUrl).build(); + UriComponentsBuilder builder = UriComponentsBuilder.newInstance() + .scheme("https") // Force HTTPS to fix redirect + .host(components.getHost()) + .path(Objects.requireNonNull(components.getPath())); + + if (components.getPort() != -1) { + builder.port(components.getPort()); + } Map param = new HashMap<>(wfsDefaultParam.getDownload()); param.put("typeName", layerName); @@ -446,6 +453,7 @@ public Optional getFeatureServerUrlByTitleOrQueryParam(String collection public Optional getFeatureServerUrl(String collectionId, String layerName) { Optional url = getFeatureServerUrlByTitleOrQueryParam(collectionId, layerName); if (url.isPresent()) { + log.debug("Found WFS link by title/query param for collectionId {} with layerName {}: {}", collectionId, layerName, url.get()); return url; } 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 7da10655..612ca5e8 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 @@ -349,6 +349,50 @@ void createFeatureFieldQueryUrl_buildsCorrectUrlWithTypename() { assertTrue(result.contains("REQUEST=DescribeFeatureType")); // original one is replaced } + @Test + void createWfsRequestUrl_stripsOldParamsFromServerUrl() { + // The server URL already has query params that would cause duplicates + String serverUrlWithParams = "https://geoserver.imas.utas.edu.au/geoserver/seamap/wfs" + + "?version=1.0.0&request=GetFeature&typeName=SeamapAus_VIC_statewide_habitats_2023&outputFormat=SHAPE-ZIP"; + + String layerName = "seamap:SeamapAus_VIC_statewide_habitats_2023"; + String outputFormat = "text/csv"; + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + String result = server.createWfsRequestUrl(serverUrlWithParams, layerName, null, null, outputFormat); + + assertNotNull(result); + + // Old param values from the server URL must NOT appear + assertFalse(result.contains("SHAPE-ZIP"), "Old outputFormat value should be removed"); + assertFalse(result.contains("typeName=SeamapAus_VIC_statewide_habitats_2023&"), + "Old typeName (without namespace prefix) should be removed"); + + // New param values must be present + assertTrue(result.contains("typeName=seamap:SeamapAus_VIC_statewide_habitats_2023"), + "New typeName with namespace prefix should be present"); + assertTrue(result.contains("outputFormat=text/csv"), + "New outputFormat should be present"); + + // Default download params from config + assertTrue(result.contains("SERVICE=WFS"), "SERVICE param should be present"); + assertTrue(result.contains("VERSION=1.0.0"), "VERSION param should be present"); + assertTrue(result.contains("REQUEST=GetFeature"), "REQUEST param should be present"); + + // No duplicate keys — each param name should appear exactly once + String query = result.substring(result.indexOf('?') + 1); + String[] pairs = query.split("&"); + long typeNameCount = java.util.Arrays.stream(pairs).filter(p -> p.toLowerCase().startsWith("typename=")).count(); + long outputFormatCount = java.util.Arrays.stream(pairs).filter(p -> p.toLowerCase().startsWith("outputformat=")).count(); + long versionCount = java.util.Arrays.stream(pairs).filter(p -> p.toLowerCase().startsWith("version=")).count(); + long requestCount = java.util.Arrays.stream(pairs).filter(p -> p.toLowerCase().startsWith("request=")).count(); + + assertEquals(1, typeNameCount, "typeName should appear exactly once"); + assertEquals(1, outputFormatCount, "outputFormat should appear exactly once"); + assertEquals(1, versionCount, "VERSION should appear exactly once"); + assertEquals(1, requestCount, "REQUEST should appear exactly once"); + } + @Test void createCapabilitiesQueryUrl_buildsCorrectUrl() { // arrange