diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java index 02d19713..841c9dce 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java @@ -217,11 +217,10 @@ protected List getWfsLinks(String collectionId) { StacCollectionModel model = result.getCollections().get(0); - // Filter WMS links where ai:group contains WMS_LINK_MARKER + // Filter WFS links by ai:group marker, falling back to rel="wfs" return model.getLinks() .stream() - .filter(link -> link.getAiGroup() != null) - .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER)) + .filter(isWfsLink(WFS_LINK_MARKER)) .toList(); } @@ -482,13 +481,9 @@ protected Optional> getAllFeatureServerUrls(String collectionId) { return Optional.of( model.getLinks() .stream() - .filter(link -> link.getAiGroup() != null) - .filter(link -> - // This is the pattern for wfs link - link.getAiGroup().contains(WFS_LINK_MARKER) || - // The data itself can be unclean, ows is another option where it works with wfs - link.getHref().contains("/ows") - ) + .filter(isWfsLink(WFS_LINK_MARKER) + // The data itself can be unclean, ows is another option where it works with wfs + .or(link -> link.getHref().contains("/ows"))) .map(LinkModel::getHref) .toList() ); @@ -512,8 +507,7 @@ public Optional getFeatureServerUrlByTitleOrQueryParam(String collection 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(isWfsLink(WFS_LINK_MARKER)) .filter(link -> { Optional name = extractLayernameOrTypenameFromUrl(link.getHref()); return roughlyMatch(link.getTitle(), layerName) || @@ -611,11 +605,10 @@ public List filterFeatureTypesByWfsLinks(String collectionId, L StacCollectionModel model = result.getCollections().get(0); - // Filter WFS links where ai:group contains "Data Access > wfs" + // Filter WFS links by ai:group marker, falling back to rel="wfs" List wfsLinks = model.getLinks() .stream() - .filter(link -> link.getAiGroup() != null) - .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER)) + .filter(isWfsLink(WFS_LINK_MARKER)) .toList(); // Filter feature types based on matching with WFS links diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java index a5b4a838..e2aea30a 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java @@ -449,11 +449,10 @@ protected Optional getMapServerUrl(String collectionId, FeatureRequest r StacCollectionModel model = result.getCollections().get(0); String layerName = request != null ? request.getLayerName() : null; - // Filter for WMS links + // Filter WMS links by ai:group marker, falling back to rel="wms" List wmsLinks = model.getLinks() .stream() - .filter(link -> link.getAiGroup() != null) - .filter(link -> link.getAiGroup().contains(WMS_LINK_MARKER)) + .filter(isWmsLink(WMS_LINK_MARKER)) .toList(); if (wmsLinks.isEmpty()) { @@ -725,11 +724,10 @@ protected List getWmsLinks(String collectionId) { StacCollectionModel model = result.getCollections().get(0); - // Filter WMS links where ai:group contains WMS_LINK_MARKER + // Filter WMS links by ai:group marker, falling back to rel="wms" return model.getLinks() .stream() - .filter(link -> link.getAiGroup() != null) - .filter(link -> link.getAiGroup().contains(WMS_LINK_MARKER)) + .filter(isWmsLink(WMS_LINK_MARKER)) .toList(); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java index 5a19a6c0..2c026aac 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java @@ -8,9 +8,36 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; @Slf4j public class GeoserverUtils { + /** + * Predicate that matches a link as a WFS link. + * Primary: link has ai:group containing the WFS marker. + * Fallback: link has rel = "wfs" (for links without ai:group). + * + * @param wfsLinkMarker - The WFS marker string (e.g. "Data Access > wfs") + * @return Predicate matching WFS links + */ + public static Predicate isWfsLink(String wfsLinkMarker) { + return link -> (link.getAiGroup() != null && link.getAiGroup().contains(wfsLinkMarker)) + || "wfs".equalsIgnoreCase(link.getRel()); + } + + /** + * Predicate that matches a link as a WMS link. + * Primary: link has ai:group containing the WMS marker. + * Fallback: link has rel = "wms" (for links without ai:group). + * + * @param wmsLinkMarker - The WMS marker string (e.g. "Data Access > wms") + * @return Predicate matching WMS links + */ + public static Predicate isWmsLink(String wmsLinkMarker) { + return link -> (link.getAiGroup() != null && link.getAiGroup().contains(wmsLinkMarker)) + || "wms".equalsIgnoreCase(link.getRel()); + } + /** * Fuzzy match utility to compare layer names, ignoring namespace prefixes * For example: "underway:nuyina_underway_202122020" matches "nuyina_underway_202122020" diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java index 2ee76c70..87f4aa15 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import static au.org.aodn.ogcapi.server.core.service.geoserver.wfs.WfsDefaultParam.WFS_LINK_MARKER; import static org.junit.jupiter.api.Assertions.*; @@ -393,6 +394,28 @@ void createWfsRequestUrl_stripsOldParamsFromServerUrl() { assertEquals(1, requestCount, "REQUEST should appear exactly once"); } + @Test + void getFeatureServerUrl_withRelWfsFallback_returnsUrl() { + // Link has rel="wfs" but no aiGroup — should be found via fallback + String expectedUrl = "http://geoserver.example.com/geoserver/wfs"; + LinkModel wfsLink = LinkModel.builder() + .rel("wfs") + .href(expectedUrl) + .title("test_layer") + .build(); + + StacCollectionModel model = StacCollectionModel.builder().links(List.of(wfsLink)).build(); + ElasticSearchBase.SearchResult result = new ElasticSearchBase.SearchResult<>(); + result.setCollections(List.of(model)); + when(mockSearch.searchCollections(anyString())).thenReturn(result); + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + Optional url = server.getFeatureServerUrl("id", "test_layer"); + + assertTrue(url.isPresent(), "URL should be found via rel=wfs fallback when aiGroup is absent"); + assertEquals(expectedUrl, url.get()); + } + @Test void createCapabilitiesQueryUrl_buildsCorrectUrl() { // arrange diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java index 0054a0bc..f3d4c8ec 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java @@ -274,6 +274,48 @@ public void verifyHandleServiceExceptionReportCorrect() { "DownloadableFieldsNotFoundException expected when layer is not found on server"); } + @Test + public void describeLayer_withRelWmsFallback_returnsResult() { + // Link has rel="wms" but no aiGroup — getMapServerUrl should find it via fallback + FeatureRequest request = FeatureRequest.builder().layerName("imos:test_layer").build(); + + ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); + stac.setCollections(new ArrayList<>()); + stac.getCollections().add( + StacCollectionModel + .builder() + .links(List.of( + LinkModel.builder() + .rel("wms") + .href("http://geoserver-123.aodn.org.au/geoserver/wms") + .title(request.getLayerName()) + .build()) // no aiGroup + ) + .build() + ); + + String describeLayerXml = """ + + + + + + + """; + + when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), eq(entity), eq(String.class))) + .thenReturn(ResponseEntity.ok(describeLayerXml)); + + String id = "id"; + when(search.searchCollections(eq(id))).thenReturn(stac); + + DescribeLayerResponse response = wmsServer.describeLayer(id, request); + + assertNotNull(response, "describeLayer should succeed when link is found via rel=wms fallback"); + assertNotNull(response.getLayerDescription()); + assertEquals("imos:test_layer", response.getLayerDescription().getName()); + } + @Test public void verifyParseCorrect() throws JsonProcessingException { DescribeLayerResponse value = wmsServer.xmlMapper.readValue(