diff --git a/server/pom.xml b/server/pom.xml index c332c1b1..7f5c80c9 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -76,6 +76,10 @@ javax.cache cache-api + + org.springframework.retry + spring-retry + org.mapstruct mapstruct diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java index 25d82cc7..250da0c5 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java @@ -81,7 +81,7 @@ public JCacheCacheManager cacheManager() throws IOException { CacheConfigurationBuilder.newCacheConfigurationBuilder( Object.class, Object.class, ResourcePoolsBuilder.heap(50) - ).withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(5))) + ).withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(15))) ) .withCache(STRING_TO_GEOMETRY, CacheConfigurationBuilder.newCacheConfigurationBuilder( diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/WfsWmsConfig.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/WfsWmsConfig.java index ed61437a..9bbfdf87 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/WfsWmsConfig.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/WfsWmsConfig.java @@ -1,16 +1,23 @@ package au.org.aodn.ogcapi.server.core.configuration; +import au.org.aodn.ogcapi.server.core.service.Search; +import au.org.aodn.ogcapi.server.core.service.wfs.DownloadableFieldsService; import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; +import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; @Configuration public class WfsWmsConfig { @Bean - public WfsServer createWfsServer() { - return new WfsServer(); + public WfsServer createWfsServer(Search search, + DownloadableFieldsService downloadableFieldsService, + RestTemplate restTemplate, + RestTemplateUtils restTemplateUtils) { + return new WfsServer(search, downloadableFieldsService, restTemplate, restTemplateUtils); } @Bean diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/BoundingBox.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/BoundingBox.java new file mode 100644 index 00000000..873fea22 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/BoundingBox.java @@ -0,0 +1,28 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wms; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BoundingBox { + @JacksonXmlProperty(isAttribute = true) + private String CRS; + + @JacksonXmlProperty(isAttribute = true) + private double minx; + + @JacksonXmlProperty(isAttribute = true) + private double miny; + + @JacksonXmlProperty(isAttribute = true) + private double maxx; + + @JacksonXmlProperty(isAttribute = true) + private double maxy; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/GeographicBoundingBox.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/GeographicBoundingBox.java new file mode 100644 index 00000000..b2f29bf2 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/GeographicBoundingBox.java @@ -0,0 +1,25 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wms; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GeographicBoundingBox { + @JacksonXmlProperty(localName = "westBoundLongitude") + private double westBoundLongitude; + + @JacksonXmlProperty(localName = "eastBoundLongitude") + private double eastBoundLongitude; + + @JacksonXmlProperty(localName = "southBoundLatitude") + private double southBoundLatitude; + + @JacksonXmlProperty(localName = "northBoundLatitude") + private double northBoundLatitude; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/KeywordList.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/KeywordList.java new file mode 100644 index 00000000..05f4d0ff --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/KeywordList.java @@ -0,0 +1,20 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wms; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class KeywordList { + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "Keyword") + private List keyword; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LayerInfo.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LayerInfo.java index 7e829e9f..34d996c9 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LayerInfo.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LayerInfo.java @@ -1,20 +1,51 @@ package au.org.aodn.ogcapi.server.core.model.ogc.wms; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + @Data @Builder @NoArgsConstructor @AllArgsConstructor +@JacksonXmlRootElement(localName = "Layer") public class LayerInfo { + @JacksonXmlProperty(isAttribute = true) + private String queryable; + + @JacksonXmlProperty(isAttribute = true) + private String opaque; + @JacksonXmlProperty(localName = "Name") protected String name; @JacksonXmlProperty(localName = "Title") protected String title; + + @JacksonXmlProperty(localName = "Abstract") + private String abstract_; + + @JacksonXmlProperty(localName = "KeywordList") + private KeywordList keywordList; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "CRS") + private List crs; + + @JacksonXmlProperty(localName = "EX_GeographicBoundingBox") + private GeographicBoundingBox geographicBoundingBox; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "BoundingBox") + private List boundingBoxes; + + @JacksonXmlProperty(localName = "Style") + private Style style; } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LegendURL.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LegendURL.java new file mode 100644 index 00000000..349b7fad --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/LegendURL.java @@ -0,0 +1,25 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wms; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LegendURL { + @JacksonXmlProperty(isAttribute = true) + private int width; + + @JacksonXmlProperty(isAttribute = true) + private int height; + + @JacksonXmlProperty(localName = "Format") + private String format; + + @JacksonXmlProperty(localName = "OnlineResource") + private OnlineResource onlineResource; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/OnlineResource.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/OnlineResource.java new file mode 100644 index 00000000..2cda6877 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/OnlineResource.java @@ -0,0 +1,22 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wms; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OnlineResource { + @JacksonXmlProperty(isAttribute = true, localName = "xlink", namespace = "http://www.w3.org/2000/xmlns/") + private String xlink; + + @JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/1999/xlink") + private String type; + + @JacksonXmlProperty(isAttribute = true, localName = "href", namespace = "http://www.w3.org/1999/xlink") + private String href; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/Style.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/Style.java new file mode 100644 index 00000000..5cbd05e0 --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wms/Style.java @@ -0,0 +1,25 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wms; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Style { + @JacksonXmlProperty(localName = "Name") + private String name; + + @JacksonXmlProperty(localName = "Title") + private String title; + + @JacksonXmlProperty(localName = "Abstract") + private String abstract_; + + @JacksonXmlProperty(localName = "LegendURL") + private LegendURL legendURL; +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java index 8dce97c5..30effbff 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java @@ -19,7 +19,8 @@ public class CacheWarm { // Hardcode server list as not expect to change much overtime, add more if needed protected List getCapabilitiesUrls = List.of( - "https://data.aad.gov.au/geoserver/underway/wms" + "https://data.aad.gov.au/geoserver/underway/ows" + // "https://www.cmar.csiro.au/geoserver/ows" <- This one super slow and do not return complete XML, need a ticket to fix ); protected WmsServer wmsServer; protected GeometryUtils geometryUtils; 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 1f32c405..02343ec1 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 @@ -136,7 +136,7 @@ public String prepareWfsRequestUrl( downloadableFields = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); log.info("DownloadableFields by describeLayer: {}", downloadableFields); } else { - Optional featureServerUrl = wfsServer.getFeatureServerUrl(uuid, layerName); + Optional featureServerUrl = wfsServer.getFeatureServerUrlByTitle(uuid, layerName); if (featureServerUrl.isPresent()) { wfsServerUrl = featureServerUrl.get(); 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 b5ba90e6..e6429820 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 @@ -15,7 +15,6 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClientException; @@ -26,6 +25,7 @@ import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import static au.org.aodn.ogcapi.server.core.configuration.CacheConfig.DOWNLOADABLE_FIELDS; @@ -34,23 +34,24 @@ public class WfsServer { // Cannot use singleton bean as it impacted other dependency protected final XmlMapper xmlMapper; - - @Autowired protected DownloadableFieldsService downloadableFieldsService; - - @Autowired protected RestTemplateUtils restTemplateUtils; - - @Autowired protected RestTemplate restTemplate; - - @Autowired protected Search search; - public WfsServer() { + public WfsServer(Search search, + DownloadableFieldsService downloadableFieldsService, + RestTemplate restTemplate, + RestTemplateUtils restTemplateUtils) { + xmlMapper = new XmlMapper(); xmlMapper.registerModule(new JavaTimeModule()); // Add JavaTimeModule xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + this.search = search; + this.restTemplate = restTemplate; + this.restTemplateUtils = restTemplateUtils; + this.downloadableFieldsService = downloadableFieldsService; } /** @@ -121,7 +122,6 @@ protected Optional> getAllFeatureServerUrls(String collectionId) { return Optional.empty(); } } - /** * Find the url that is able to get WFS call, this can be found in ai:Group * @@ -129,7 +129,7 @@ protected Optional> getAllFeatureServerUrls(String collectionId) { * @param layerName - The layer name to match the title * @return - The first wfs server link if found */ - protected Optional getFeatureServerUrl(String collectionId, String layerName) { + public Optional getFeatureServerUrlByTitle(String collectionId, String layerName) { ElasticSearchBase.SearchResult result = search.searchCollections(collectionId); if (!result.getCollections().isEmpty()) { StacCollectionModel model = result.getCollections().get(0); @@ -143,7 +143,32 @@ protected Optional getFeatureServerUrl(String collectionId, String layer return Optional.empty(); } } - + /** + * 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 getFeatureServerUrlByTitleOrQueryParam(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("Data Access > wfs")) + .filter(link -> { + Optional name = extractTypenameFromUrl(link.getHref()); + return link.getTitle().equalsIgnoreCase(layerName) || + (name.isPresent() && roughlyMatch(name.get(), layerName)); + }) + .map(LinkModel::getHref) + .findFirst(); + } else { + return Optional.empty(); + } + } /** * Fuzzy match utility to compare layer names, ignoring namespace prefixes * For example: "underway:nuyina_underway_202122020" matches "nuyina_underway_202122020" @@ -152,7 +177,7 @@ protected Optional getFeatureServerUrl(String collectionId, String layer * @param text2 - Second text to compare * @return true if texts match (after removing namespace prefix) */ - protected boolean fuzzyMatch(String text1, String text2) { + protected boolean roughlyMatch(String text1, String text2) { if (text1 == null || text2 == null) { return false; } @@ -161,9 +186,14 @@ protected boolean fuzzyMatch(String text1, String text2) { String normalized1 = text1.contains(":") ? text1.substring(text1.indexOf(":") + 1) : text1; String normalized2 = text2.contains(":") ? text2.substring(text2.indexOf(":") + 1) : text2; - return normalized1.equalsIgnoreCase(normalized2); + if (normalized1.length() < normalized2.length()) { + // Swap the text so that compare startsWith using longer text. + String temp = normalized1; + normalized1 = normalized2; + normalized2 = temp; + } + return normalized1.startsWith(normalized2); } - /** * Extract typename from WFS URL query parameters * @@ -193,7 +223,6 @@ protected Optional extractTypenameFromUrl(String url) { } return Optional.empty(); } - /** * Filter WMS layers based on matching with WFS links * Matching logic: @@ -208,8 +237,8 @@ public List filterLayersByWfsLinks(String collectionId, List result = search.searchCollections(collectionId); if (result.getCollections().isEmpty()) { - log.info("Return all layers if as no collection found for collectionId: {}", collectionId); - return layers; + log.info("Return empty layers if as no collection found for collectionId: {}", collectionId); + return Collections.emptyList(); } StacCollectionModel model = result.getCollections().get(0); @@ -222,8 +251,8 @@ public List filterLayersByWfsLinks(String collectionId, List filterLayersByWfsLinks(String collectionId, List typename = extractTypenameFromUrl(wfsLink.getHref()); if (typename.isPresent()) { - if (fuzzyMatch(typename.get(), layer.getName()) || - fuzzyMatch(typename.get(), layer.getTitle())) { + if (roughlyMatch(typename.get(), layer.getName()) || + roughlyMatch(typename.get(), layer.getTitle())) { log.debug(" ✓ Fallback match found - typename '{}' matches layer '{}'", typename.get(), layer.getName()); matched = true; @@ -265,6 +294,15 @@ public List filterLayersByWfsLinks(String collectionId, List aodn_map = filteredLayers.stream().filter(l -> + l.getName().endsWith("_aodn_map") || l.getTitle().endsWith("_aodn_map") + ).toList(); + if(!aodn_map.isEmpty()) { + filteredLayers = aodn_map; + } + log.info("Filtered {} layers out of {} based on WFS link matching", filteredLayers.size(), layers.size()); return filteredLayers; 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 ab6e22d9..23186e30 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 @@ -70,9 +70,20 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { if (request.getDatetime() != null) { // Special handle for date time field, the field name will be diff across dataset. So we need // to look it up - String cql = null; - + String cql = ""; try { + Optional wfsUrl = wfsServer.getFeatureServerUrlByTitleOrQueryParam(uuid, request.getLayerName()); + if(wfsUrl.isPresent()) { + UriComponents wfsUrlComponents = UriComponentsBuilder.fromUriString(wfsUrl.get()).build(); + // Extract the CQL if existing in the WFS, we need to apply it to the WMS as well + if(wfsUrlComponents.getQueryParams().get("cql_filter") != null) { + cql = wfsUrlComponents.getQueryParams().get("cql_filter").get(0) + " AND "; + } + else if(wfsUrlComponents.getQueryParams().get("CQL_FILTER") != null) { + cql = wfsUrlComponents.getQueryParams().get("CQL_FILTER").get(0) + " AND "; + } + } + List m = this.getDownloadableFields(uuid, request); List target = m.stream() .filter(value -> "dateTime".equalsIgnoreCase(value.getType())) @@ -93,15 +104,15 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { String guess1 = target.get(0).getName(); String guess2 = target.get(1).getName(); if ((guess1.contains("start") || guess1.contains("min")) && (guess2.contains("end") || guess2.contains("max"))) { - return String.format("CQL_FILTER=%s >= %s AND %s <= %s", guess1, d[0], guess2, d[1]); + return String.format("CQL_FILTER=%s%s >= %s AND %s <= %s", cql, guess1, d[0], guess2, d[1]); } if ((guess2.contains("start") || guess2.contains("min")) && (guess1.contains("end") || guess1.contains("max"))) { - return String.format("CQL_FILTER=%s >= %s AND %s <= %s", guess2, d[0], guess2, d[1]); + return String.format("CQL_FILTER=%s%s >= %s AND %s <= %s", cql, guess2, d[0], guess2, d[1]); } } else { // Only 1 field so use it. log.debug("Map datetime field to name to [{}]", target.get(0).getName()); - return String.format("CQL_FILTER=%s DURING %s", target.get(0).getName(), request.getDatetime()); + return String.format("CQL_FILTER=%s%s DURING %s", cql, target.get(0).getName(), request.getDatetime()); } } log.error("No date time field found from query for uuid {}, result will not be bounded by date time", uuid); @@ -112,7 +123,13 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { } return ""; } - + /** + * Create the full WMS url to fetch the tiles image + * @param url - The url from the metadata, it may point to the wms server only without specifying the remain details, this function will do a smart lookup + * @param uuid - The UUID of the metadata which use to find the WFS links + * @param request - The request like bbox and other param say datetime, layerName (where layerName is not reliable and need lookup internally) + * @return - The final URl to do the query + */ protected List createMapQueryUrl(String url, String uuid, FeatureRequest request) { try { UriComponents components = UriComponentsBuilder.fromUriString(url).build(); @@ -157,7 +174,7 @@ protected List createMapQueryUrl(String url, String uuid, FeatureRequest // This is the normal route UriComponentsBuilder builder = UriComponentsBuilder .newInstance() - .scheme(components.getScheme()) + .scheme("https") .port(components.getPort()) .host(components.getHost()) .path(components.getPath()); @@ -346,7 +363,7 @@ protected Optional getMapServerUrl(String collectionId, FeatureRequest r return Optional.empty(); } - Optional matchedUrl = Optional.empty(); + Optional matchedUrl; if (layerName != null && !layerName.isEmpty()) { // If layer name provided, try to match by layer name @@ -452,7 +469,12 @@ public byte[] getMapTile(String collectionId, FeatureRequest request) throws URI } return null; } - + /** + * Query the field using WMS's DescriberLayer function to find out the associated WFS layer and fields + * @param collectionId - The uuid of the metadata that hold this WMS link + * @param request - Request item for this WMS layer, usually layer name, size, etc. + * @return - The fields contained in this WMS layer, we are particular interest in the date time field for subsetting + */ public List getDownloadableFields(String collectionId, FeatureRequest request) { DescribeLayerResponse response = this.describeLayer(collectionId, request); @@ -465,9 +487,8 @@ public List getDownloadableFields(String collectionId, F return wfsServer.getDownloadableFields(collectionId, request, null); } } - /** - * Fetch raw layers from WMS GetCapabilities - cached by URL + * Fetch raw layers from WMS GetCapabilities - cached by URL, that is query all layer supported by this WMS server. * This allows multiple collections sharing the same WMS server to use cached results * * @param wmsServerUrl - The WMS server base URL @@ -527,7 +548,6 @@ public List fetchCapabilitiesLayersByUrl(String wmsServerUrl) { return Collections.emptyList(); } - /** * Get filtered layers from WMS GetCapabilities for a specific collection * First fetches all layers (cached by URL), then filters by WFS links (cached by UUID) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/ConstructUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/ConstructUtils.java index 475036a7..f4059c5e 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/ConstructUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/ConstructUtils.java @@ -10,7 +10,7 @@ public class ConstructUtils { @Setter - private static ObjectMapper objectMapper; + private static ObjectMapper objectMapper = new ObjectMapper(); // Give default public static Optional constructByJsonString(String jsonString, Class clazz) { try { 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 b444c1bd..1d848e44 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 @@ -270,14 +270,12 @@ public void testPrepareWfsRequestUrl_NoWfsServerUrl() { String layerName = "test:layer"; when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(null); - when(wfsServer.getFeatureServerUrl(eq(uuid), eq(layerName))).thenReturn(java.util.Optional.empty()); + when(wfsServer.getFeatureServerUrlByTitle(eq(uuid), eq(layerName))).thenReturn(java.util.Optional.empty()); // Test with no WFS server URL available - Exception exception = assertThrows(IllegalArgumentException.class, () -> { - downloadWfsDataService.prepareWfsRequestUrl( - uuid, null, null, null, null, layerName - ); - }); + Exception exception = assertThrows(IllegalArgumentException.class, () -> downloadWfsDataService.prepareWfsRequestUrl( + uuid, null, null, null, null, layerName + )); assertTrue(exception.getMessage().contains("No WFS server URL found")); } diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/WfsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/WfsServerTest.java new file mode 100644 index 00000000..58cac516 --- /dev/null +++ b/server/src/test/java/au/org/aodn/ogcapi/server/service/wfs/WfsServerTest.java @@ -0,0 +1,132 @@ +package au.org.aodn.ogcapi.server.service.wfs; + +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.wms.LayerInfo; +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.service.wfs.DownloadableFieldsService; +import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; +import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WfsServerTest { + + @Mock + Search mockSearch; + + @Mock + DownloadableFieldsService downloadableFieldsService; + + @Mock + RestTemplate restTemplate; + + AutoCloseable closeableMock; + + @BeforeEach + public void setUp() { + closeableMock = MockitoAnnotations.openMocks(this); + } + + @AfterEach + void cleanUp() throws Exception { + closeableMock.close(); + } + /** + * Test null case where the dataset have the collection id not found + */ + @Test + void noCollection_returnsEmptyLayers() { + ElasticSearchBase.SearchResult result = new ElasticSearchBase.SearchResult<>(); + result.setCollections(Collections.emptyList()); + when(mockSearch.searchCollections(anyString())).thenReturn(result); + + WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate)); + + List layers = Collections.singletonList(LayerInfo.builder().build()); + assertEquals(Collections.emptyList(), server.filterLayersByWfsLinks("id", layers)); + } + + @Test + void noWfsLinks_returnsEmptyLayers() { + StacCollectionModel model = mock(StacCollectionModel.class); + when(model.getLinks()).thenReturn(Collections.emptyList()); + + ElasticSearchBase.SearchResult result = new ElasticSearchBase.SearchResult<>(); + result.setCollections(List.of(model)); + + when(mockSearch.searchCollections(anyString())).thenReturn(result); + + WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate)); + + List layers = Collections.singletonList(LayerInfo.builder().build()); + assertEquals(Collections.emptyList(), server.filterLayersByWfsLinks("id", layers)); + } + /** + * The function should fine one because title name matches + */ + @Test + void primaryTitleMatch_filtersMatchingLayers() { + LinkModel wfsLink = LinkModel.builder() + .title("test_layer") + .aiGroup("Data Access > wfs") + .href("http://example.com?wfs").build(); + + StacCollectionModel model = StacCollectionModel.builder().links(List.of(wfsLink)).build(); + var layers = List.of( + LayerInfo.builder().title("test_layer").name("").build(), + LayerInfo.builder().title("other").build() + ); + + ElasticSearchBase.SearchResult result = new ElasticSearchBase.SearchResult<>(); + result.setCollections(List.of(model)); + when(mockSearch.searchCollections(anyString())).thenReturn(result); + + WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate)); + + List info = server.filterLayersByWfsLinks("id", layers); + assertEquals(1, info.size(), "Layer count match"); + assertEquals(layers.get(0), info.get(0), "Layer test_layer found"); + } + /** + * The function will scan the layer that match if there exist layers where name ends with _aodn_map, then + * only return those, otherwise return layers found without _aodn_map sufix. This make the portal works like + * old portal where they setup layer for portal with sufix _aodn_map + */ + @Test + void primaryTitleMatch_filtersPreferAodnMapLayers() { + LinkModel wfsLink = LinkModel.builder() + .title("test_layer") + .aiGroup("Data Access > wfs") + .href("http://example.com?wfs").build(); + + StacCollectionModel model = StacCollectionModel.builder().links(List.of(wfsLink)).build(); + var layers = List.of( + LayerInfo.builder().title("test_layer").name("").build(), + LayerInfo.builder().title("layer:test_layer_aodn_map").name("").build() + ); + + ElasticSearchBase.SearchResult result = new ElasticSearchBase.SearchResult<>(); + result.setCollections(List.of(model)); + when(mockSearch.searchCollections(anyString())).thenReturn(result); + + WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate)); + + List info = server.filterLayersByWfsLinks("id", layers); + assertEquals(1, info.size(), "Layer count match"); + assertEquals(layers.get(1), info.get(0), "Layer layer:test_layer_aodn_map found"); + } +}