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");
+ }
+}