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 d07ee883..9074611f 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,7 +1,7 @@ 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.WfsDefaultParam; 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; @@ -16,7 +16,7 @@ @Configuration public class WfsWmsConfig { - @ConditionalOnMissingBean(name="pretendUserEntity") + @ConditionalOnMissingBean(name = "pretendUserEntity") @Bean("pretendUserEntity") public HttpEntity createPretendUserEntity() { // Some server do not allow program to scrap the content, so we need to pretend to be a client @@ -28,11 +28,11 @@ public HttpEntity createPretendUserEntity() { @Bean public WfsServer createWfsServer(Search search, - DownloadableFieldsService downloadableFieldsService, RestTemplate restTemplate, RestTemplateUtils restTemplateUtils, - @Qualifier("pretendUserEntity") HttpEntity entity) { - return new WfsServer(search, downloadableFieldsService, restTemplate, restTemplateUtils, entity); + @Qualifier("pretendUserEntity") HttpEntity entity, + WfsDefaultParam wfsDefaultParam) { + return new WfsServer(search, restTemplate, restTemplateUtils, entity, wfsDefaultParam); } @Bean diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/DownloadableFieldsNotFoundException.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GeoserverFieldsNotFoundException.java similarity index 61% rename from server/src/main/java/au/org/aodn/ogcapi/server/core/exception/DownloadableFieldsNotFoundException.java rename to server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GeoserverFieldsNotFoundException.java index 293f7e7d..d7a00b52 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/DownloadableFieldsNotFoundException.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GeoserverFieldsNotFoundException.java @@ -4,8 +4,8 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.NOT_FOUND) -public class DownloadableFieldsNotFoundException extends RuntimeException { - public DownloadableFieldsNotFoundException(String message) { +public class GeoserverFieldsNotFoundException extends RuntimeException { + public GeoserverFieldsNotFoundException(String message) { super(message); } } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GeoserverLayersNotFoundException.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GeoserverLayersNotFoundException.java new file mode 100644 index 00000000..40cbcbdc --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GeoserverLayersNotFoundException.java @@ -0,0 +1,11 @@ +package au.org.aodn.ogcapi.server.core.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class GeoserverLayersNotFoundException extends RuntimeException { + public GeoserverLayersNotFoundException(String message) { + super(message); + } +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GlobalExceptionHandler.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GlobalExceptionHandler.java index 47bedba7..5c5b2741 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/GlobalExceptionHandler.java @@ -40,8 +40,8 @@ public ResponseEntity handleCustomException(Exception ex, WebRequ return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } - @ExceptionHandler(DownloadableFieldsNotFoundException.class) - public ResponseEntity handleDownloadableFieldsNotFoundException(DownloadableFieldsNotFoundException ex, WebRequest request) { + @ExceptionHandler(GeoserverFieldsNotFoundException.class) + public ResponseEntity handleDownloadableFieldsNotFoundException(GeoserverFieldsNotFoundException ex, WebRequest request) { ErrorResponse errorResponse = ErrorResponse .builder() .timestamp(LocalDateTime.now()) @@ -64,7 +64,17 @@ public ResponseEntity handleUnauthorizedServerException(Unauthori return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); } + @ExceptionHandler(GeoserverLayersNotFoundException.class) + public ResponseEntity handleGeoserverLayersNotFoundException(GeoserverLayersNotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = ErrorResponse + .builder() + .timestamp(LocalDateTime.now()) + .message(ex.getMessage()) + .details(request.getDescription(false)) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } @ExceptionHandler(Exception.class) public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java index 84ec1832..7af55e9d 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/exception/wfs/WfsErrorHandler.java @@ -1,6 +1,6 @@ package au.org.aodn.ogcapi.server.core.exception.wfs; -import au.org.aodn.ogcapi.server.core.exception.DownloadableFieldsNotFoundException; +import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; import au.org.aodn.ogcapi.server.core.exception.UnauthorizedServerException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @@ -120,7 +120,7 @@ private static ErrorType categorizeError(Exception e) { } // Downloadable fields not found error - if (e instanceof DownloadableFieldsNotFoundException) { + if (e instanceof GeoserverFieldsNotFoundException) { return ErrorType.DOWNLOADABLE_FIELDS_ERROR; } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java index 650c59b6..820e8428 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/FeatureId.java @@ -2,11 +2,11 @@ public enum FeatureId { summary("summary"), + wfs_fields("wfs_fields"), // Query field based on pure wfs and given layer + wms_fields("wms_fields"), // Query field based on value from wms describe layer query wave_buoy_first_data_available("wave_buoy_first_data_available"), wave_buoy_latest_date("wave_buoy_latest_date"), wave_buoy_timeseries("wave_buoy_timeseries"), - wfs_downloadable_fields("wfs_downloadable_fields"), // Query field based on pure wfs and given layer - wms_downloadable_fields("wms_downloadable_fields"), // Query field based on value from wms describe layer query wms_map_tile("wms_map_tile"), wms_map_feature("wms_map_feature"), wms_layers("wms_layers"), // Get all available layers from WMS GetCapabilities diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/DownloadableFieldModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/DownloadableFieldModel.java deleted file mode 100644 index a8cd6201..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/DownloadableFieldModel.java +++ /dev/null @@ -1,18 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model.ogc.wfs; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class DownloadableFieldModel { - @JsonProperty("label") - private String label; - - @JsonProperty("type") - private String type; - - @JsonProperty("name") - private String name; -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java new file mode 100644 index 00000000..e02ae4be --- /dev/null +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WFSFieldModel.java @@ -0,0 +1,31 @@ +package au.org.aodn.ogcapi.server.core.model.ogc.wfs; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class WFSFieldModel { + + @JsonProperty("typename") + private String typename; + + @JsonProperty("fields") + private List fields; + + @Data + @Builder + public static class Field { + @JsonProperty("label") + private String label; + + @JsonProperty("name") + private String name; + + @JsonProperty("type") + private String type; + } +} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsDescribeFeatureTypeResponse.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsDescribeFeatureTypeResponse.java index 8c6f171f..3a9415a6 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsDescribeFeatureTypeResponse.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ogc/wfs/WfsDescribeFeatureTypeResponse.java @@ -17,6 +17,11 @@ public class WfsDescribeFeatureTypeResponse { @JacksonXmlElementWrapper(useWrapping = false) private List complexTypes; + // Top-level element like + @JacksonXmlProperty(localName = "element") + @JacksonXmlElementWrapper(useWrapping = false) + private List topLevelElements; + @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class ComplexType { @@ -58,4 +63,18 @@ public static class Element { @JacksonXmlProperty(isAttribute = true) private String type; } + + // Top-level element: + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class TopLevelElement { + @JacksonXmlProperty(isAttribute = true) + private String name; + + @JacksonXmlProperty(isAttribute = true) + private String type; + + @JacksonXmlProperty(isAttribute = true) + private String substitutionGroup; + } } 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 b4292fb3..eac4b474 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataService.java @@ -1,7 +1,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.DownloadableFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; 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; @@ -46,11 +46,17 @@ public DownloadWfsDataService( /** * Build CQL filter for temporal and spatial constraints */ - private String buildCqlFilter(String startDate, String endDate, Object multiPolygon, List downloadableFields) { + private String buildCqlFilter(String startDate, String endDate, Object multiPolygon, WFSFieldModel wfsFieldModel) { StringBuilder cqlFilter = new StringBuilder(); + if (wfsFieldModel == null || wfsFieldModel.getFields() == null) { + return cqlFilter.toString(); + } + + List fields = wfsFieldModel.getFields(); + // Find temporal field - Optional temporalField = downloadableFields.stream() + Optional temporalField = fields.stream() .filter(field -> "dateTime".equals(field.getType()) || "date".equals(field.getType())) .findFirst(); @@ -64,7 +70,7 @@ private String buildCqlFilter(String startDate, String endDate, Object multiPoly } // Find geometry field - Optional geometryField = downloadableFields.stream() + Optional geometryField = fields.stream() .filter(field -> "geometrypropertytype".equals(field.getType())) .findFirst(); @@ -131,23 +137,23 @@ public String prepareWfsRequestUrl( String wfsServerUrl; String wfsTypeName; - List downloadableFields; + WFSFieldModel 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(); - downloadableFields = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); - log.info("DownloadableFields by describeLayer: {}", downloadableFields); + wfsFieldModel = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); + log.info("WFSFieldModel by describeLayer: {}", wfsFieldModel); } else { Optional featureServerUrl = wfsServer.getFeatureServerUrlByTitle(uuid, layerName); if (featureServerUrl.isPresent()) { wfsServerUrl = featureServerUrl.get(); wfsTypeName = layerName; - downloadableFields = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); - log.info("DownloadableFields by wfs typename: {}", downloadableFields); + wfsFieldModel = wfsServer.getDownloadableFields(uuid, FeatureRequest.builder().layerName(wfsTypeName).build(), wfsServerUrl); + log.info("WFSFieldModel by wfs typename: {}", wfsFieldModel); } else { throw new IllegalArgumentException("No WFS server URL found for the given UUID and layer name"); } @@ -158,7 +164,7 @@ public String prepareWfsRequestUrl( String validEndDate = DatetimeUtils.validateAndFormatDate(endDate, false); // Build CQL filter - String cqlFilter = buildCqlFilter(validStartDate, validEndDate, multiPolygon, downloadableFields); + String cqlFilter = buildCqlFilter(validStartDate, validEndDate, multiPolygon, wfsFieldModel); // Build final WFS request URL String wfsRequestUrl = buildWfsRequestUrl(wfsServerUrl, wfsTypeName, cqlFilter); diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsService.java deleted file mode 100644 index 26f72608..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsService.java +++ /dev/null @@ -1,109 +0,0 @@ -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.DownloadableFieldModel; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsDescribeFeatureTypeResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@Slf4j -@Service -public class DownloadableFieldsService { - - @Autowired - protected WfsDefaultParam wfsDefaultParam; - - protected String createFeatureFieldQueryUrl(String url, FeatureRequest request) { - UriComponents components = UriComponentsBuilder.fromUriString(url).build(); - if(components.getPath() != null) { - // Now depends on the service, we need to have different arguments - List pathSegments = components.getPathSegments(); - if (!pathSegments.isEmpty()) { - Map param = new HashMap<>(wfsDefaultParam.getFields()); - - // Now we add the missing argument from the request - param.put("TYPENAME", request.getLayerName()); - - // This is the normal route - UriComponentsBuilder builder = UriComponentsBuilder - .newInstance() - .scheme(components.getScheme()) - .port(components.getPort()) - .host(components.getHost()) - .path(components.getPath()); - - param.forEach((key, value) -> { - if(value != null) { - builder.queryParam(key, value); - } - }); - String target = builder.build().toUriString(); - log.debug("Url query support field in wfs {}", target); - - return target; - } - } - return null; - } - /** - * Convert WFS response to downloadable fields (geometry and date/time fields) - */ - protected static List convertWfsResponseToDownloadableFields(WfsDescribeFeatureTypeResponse wfsResponse) { - return wfsResponse.getComplexTypes() != null ? - wfsResponse.getComplexTypes().stream() - .filter(complexType -> complexType.getComplexContent() != null) - .filter(complexType -> complexType.getComplexContent().getExtension() != null) - .filter(complexType -> complexType.getComplexContent().getExtension().getSequence() != null) - .flatMap(complexType -> { - List elements = - complexType.getComplexContent().getExtension().getSequence().getElements(); - return elements != null ? elements.stream() : Stream.empty(); - }) - .filter(element -> element.getName() != null && element.getType() != null) - .map(DownloadableFieldsService::createDownloadableField) - .filter(Objects::nonNull) - .collect(Collectors.toList()) : new ArrayList<>(); - } - /** - * Create a downloadable field based on the element type - */ - protected static DownloadableFieldModel createDownloadableField(WfsDescribeFeatureTypeResponse.Element element) { - String elementType = element.getType(); - if (elementType == null) { - return null; - } - - DownloadableFieldModel field = DownloadableFieldModel - .builder() - .label(element.getName()) - .name(element.getName()) - .build(); - - return switch (elementType) { - case "gml:GeometryPropertyType" -> { - field.setType("geometrypropertytype"); - yield field; - } - case "xsd:dateTime" -> { - field.setType("dateTime"); - yield field; - } - case "xsd:date" -> { - field.setType("date"); - yield field; - } - case "xsd:time" -> { - field.setType("time"); - yield field; - } - default -> null; // Ignore other types - }; - } -} 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 c1ac75ae..e2c201d0 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 @@ -1,13 +1,14 @@ package au.org.aodn.ogcapi.server.core.service.wfs; -import au.org.aodn.ogcapi.server.core.exception.DownloadableFieldsNotFoundException; +import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; +import au.org.aodn.ogcapi.server.core.exception.GeoserverLayersNotFoundException; import au.org.aodn.ogcapi.server.core.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsDescribeFeatureTypeResponse; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsGetCapabilitiesResponse; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.DownloadableFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import au.org.aodn.ogcapi.server.core.service.Search; import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils; @@ -28,36 +29,35 @@ import org.springframework.web.util.UriComponentsBuilder; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static au.org.aodn.ogcapi.server.core.configuration.CacheConfig.DOWNLOADABLE_FIELDS; import static au.org.aodn.ogcapi.server.core.configuration.CacheConfig.GET_CAPABILITIES_WFS_FEATURE_TYPES; import static au.org.aodn.ogcapi.server.core.service.wfs.WfsDefaultParam.WFS_LINK_MARKER; -import static au.org.aodn.ogcapi.server.core.util.GeoserverUtils.extractLayernameOrTypenameFromUrl; -import static au.org.aodn.ogcapi.server.core.util.GeoserverUtils.roughlyMatch; +import static au.org.aodn.ogcapi.server.core.service.wms.WmsDefaultParam.WMS_LINK_MARKER; +import static au.org.aodn.ogcapi.server.core.util.GeoserverUtils.*; @Slf4j public class WfsServer { // Cannot use singleton bean as it impacted other dependency protected final XmlMapper xmlMapper; - protected DownloadableFieldsService downloadableFieldsService; protected RestTemplateUtils restTemplateUtils; protected RestTemplate restTemplate; protected Search search; protected HttpEntity pretendUserEntity; + protected WfsDefaultParam wfsDefaultParam; @Lazy @Autowired protected WfsServer self; public WfsServer(Search search, - DownloadableFieldsService downloadableFieldsService, RestTemplate restTemplate, RestTemplateUtils restTemplateUtils, - HttpEntity entity) { + HttpEntity entity, + WfsDefaultParam wfsDefaultParam) { xmlMapper = new XmlMapper(); xmlMapper.registerModule(new JavaTimeModule()); // Add JavaTimeModule @@ -66,8 +66,101 @@ public WfsServer(Search search, this.search = search; this.restTemplate = restTemplate; this.restTemplateUtils = restTemplateUtils; - this.downloadableFieldsService = downloadableFieldsService; this.pretendUserEntity = entity; + this.wfsDefaultParam = wfsDefaultParam; + } + + /** + * Get all WFS links from a collection. + * + * @param collectionId - The uuid + * @return - List of WFS LinkModel objects from the collection + */ + protected List getWfsLinks(String collectionId) { + ElasticSearchBase.SearchResult result = search.searchCollections(collectionId); + + if (result.getCollections().isEmpty()) { + log.info("No collection found for collectionId: {}", collectionId); + return Collections.emptyList(); + } + + StacCollectionModel model = result.getCollections().get(0); + + // Filter WMS links where ai:group contains WMS_LINK_MARKER + return model.getLinks() + .stream() + .filter(link -> link.getAiGroup() != null) + .filter(link -> link.getAiGroup().contains(WFS_LINK_MARKER)) + .toList(); + } + + + protected String createFeatureFieldQueryUrl(String url, FeatureRequest request) { + UriComponents components = UriComponentsBuilder.fromUriString(url).build(); + if (components.getPath() != null) { + // Now depends on the service, we need to have different arguments + List pathSegments = components.getPathSegments(); + if (!pathSegments.isEmpty()) { + Map param = new HashMap<>(wfsDefaultParam.getFields()); + + // Now we add the missing argument from the request + param.put("TYPENAME", request.getLayerName()); + + // This is the normal route + UriComponentsBuilder builder = UriComponentsBuilder + .newInstance() + .scheme(components.getScheme()) + .port(components.getPort()) + .host(components.getHost()) + .path(components.getPath()); + + param.forEach((key, value) -> { + if (value != null) { + builder.queryParam(key, value); + } + }); + String target = builder.build().toUriString(); + log.debug("Url query support field in wfs {}", target); + + return target; + } + } + return null; + } + + /** + * Convert WFS response to WFSFieldModel. + * The typename is extracted from the top-level xsd:element (e.g., ) + */ + protected static WFSFieldModel convertWfsResponseToDownloadableFields(WfsDescribeFeatureTypeResponse wfsResponse) { + String typename = null; + if (wfsResponse.getTopLevelElements() != null && !wfsResponse.getTopLevelElements().isEmpty()) { + typename = wfsResponse.getTopLevelElements().get(0).getName(); + } + + List fields = wfsResponse.getComplexTypes() != null ? + wfsResponse.getComplexTypes().stream() + .filter(complexType -> complexType.getComplexContent() != null) + .filter(complexType -> complexType.getComplexContent().getExtension() != null) + .filter(complexType -> complexType.getComplexContent().getExtension().getSequence() != null) + .flatMap(complexType -> { + List elements = + complexType.getComplexContent().getExtension().getSequence().getElements(); + return elements != null ? elements.stream() : Stream.empty(); + }) + .filter(element -> element.getName() != null && element.getType() != null) + .map(element -> WFSFieldModel.Field.builder() + .label(element.getName()) + .name(element.getName()) + // The type can be in format of "xsd:date", we only want the actual type name "date" + .type(element.getType().contains(":") ? element.getType().split(":")[1] : element.getType()) + .build()) + .collect(Collectors.toList()) : new ArrayList<>(); + + return WFSFieldModel.builder() + .typename(typename) + .fields(fields) + .build(); } /** @@ -76,10 +169,10 @@ public WfsServer(Search search, * @param collectionId - The uuid of the collection * @param request - The feature request containing the layer name * @param assumedWfsServer - An optional wfs server url to use instead of searching for one - * @return - A list of downloadable fields + * @return - WFSFieldModel containing typename and fields */ @Cacheable(value = DOWNLOADABLE_FIELDS) - public List getDownloadableFields(String collectionId, FeatureRequest request, String assumedWfsServer) { + public WFSFieldModel getDownloadableFields(String collectionId, FeatureRequest request, String assumedWfsServer) { Optional> mapFeatureUrl = assumedWfsServer != null ? Optional.of(List.of(assumedWfsServer)) : @@ -88,7 +181,7 @@ public List getDownloadableFields(String collectionId, F if (mapFeatureUrl.isPresent()) { // Keep trying all possible url until one get response for (String url : mapFeatureUrl.get()) { - String uri = downloadableFieldsService.createFeatureFieldQueryUrl(url, request); + String uri = createFeatureFieldQueryUrl(url, request); try { if (uri != null) { log.debug("Try Url to wfs {}", uri); @@ -100,7 +193,7 @@ public List getDownloadableFields(String collectionId, F ); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { - return DownloadableFieldsService.convertWfsResponseToDownloadableFields( + return convertWfsResponseToDownloadableFields( xmlMapper.readValue(response.getBody(), WfsDescribeFeatureTypeResponse.class) ); } @@ -110,9 +203,49 @@ public List getDownloadableFields(String collectionId, F } } } else { - return List.of(); + return null; } - throw new DownloadableFieldsNotFoundException("No downloadable fields found for all url"); + throw new GeoserverFieldsNotFoundException("No downloadable fields found for all url"); + } + + public List getWFSFields(String collectionId, FeatureRequest request) { + List wfsFields = new ArrayList<>(); + + // If typename is provided, use it directly + // If no typename provided, get fields for all layers from collection WFS links + if (request.getLayerName() != null && !request.getLayerName().isEmpty()) { + wfsFields.add(self.getDownloadableFields(collectionId, request, null)); + } else { + log.debug("No layer name provided in request, get fields for all WFS links"); + List typeNamesToProcess = new ArrayList<>(); + + // Get all wfs links and extract typename from the link + List wfsLinks = this.getWfsLinks(collectionId); + for (LinkModel wfsLink : wfsLinks) { + extractLayernameOrTypenameFromLink(wfsLink).ifPresent(typeNamesToProcess::add); + } + // fetch downloadable fields for each typename + for (String typeName : typeNamesToProcess) { + FeatureRequest requestModified = FeatureRequest.builder() + .layerName(typeName) + .build(); + + try { + WFSFieldModel fields = self.getDownloadableFields(collectionId, requestModified, null); + if (fields != null) { + wfsFields.add(fields); + } + } catch (GeoserverFieldsNotFoundException e) { + log.debug("No fields found for typename {}, continue with other typename", typeName); + } + } + } + + if (wfsFields.isEmpty()) { + throw new GeoserverFieldsNotFoundException("No downloadable fields found for uuid " + collectionId); + } + + return wfsFields; } /** @@ -341,11 +474,15 @@ public List getCapabilitiesFeatureTypes(String collectionId, Fe // Filter feature types based on WFS link matching List filteredFeatureTypes = filterFeatureTypesByWfsLinks(collectionId, allFeatureTypes); + if (filteredFeatureTypes.isEmpty()) { + throw new GeoserverLayersNotFoundException("No WFS layer is found for uuid " + collectionId); + } + log.debug("Returning feature types {}", filteredFeatureTypes); return filteredFeatureTypes; } } - return Collections.emptyList(); + throw new GeoserverLayersNotFoundException("No valid WFS server url is found for uuid " + collectionId); } } 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 e52411c6..ecb324af 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 @@ -1,10 +1,11 @@ package au.org.aodn.ogcapi.server.core.service.wms; -import au.org.aodn.ogcapi.server.core.exception.DownloadableFieldsNotFoundException; +import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; +import au.org.aodn.ogcapi.server.core.exception.GeoserverLayersNotFoundException; import au.org.aodn.ogcapi.server.core.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.DownloadableFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; import au.org.aodn.ogcapi.server.core.model.ogc.wms.*; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; import au.org.aodn.ogcapi.server.core.service.Search; @@ -103,14 +104,20 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { // Special handle for date time field, the field name will be diff across dataset. So we need // to look it up try { - List m = this.getDownloadableFields(uuid, request); - List target = m.stream() + List wfsFieldModels = this.getWMSFields(uuid, request); + // Flatten all fields from all WFSFieldModels + List allFields = wfsFieldModels.stream() + .filter(m -> m.getFields() != null) + .flatMap(m -> m.getFields().stream()) + .toList(); + + List target = allFields.stream() .filter(value -> "dateTime".equalsIgnoreCase(value.getType())) .toList(); if (!target.isEmpty()) { - List range; + List range; if (target.size() > 2) { // Try to find possible fields where it contains start end min max range = target.stream() @@ -135,7 +142,7 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { } else { // There are more than 1 dateTime field, it is not range type, so we try to guess the individual one // based on some common name. Add more if needed - List individual = target.stream() + List individual = target.stream() .filter(v -> Stream.of("juld", "time").anyMatch(k -> v.getName().equalsIgnoreCase(k))) .toList(); @@ -152,7 +159,7 @@ protected String createCQLFilter(String uuid, FeatureRequest request) { } } log.error("No date time field found for uuid {}, result will not be bounded by date time even specified", uuid); - } catch (DownloadableFieldsNotFoundException dfnf) { + } catch (GeoserverFieldsNotFoundException dfnf) { // Without field, we cannot create a valid CQL filte targeting a dateTime, so just return existing CQL if exist } } @@ -516,30 +523,6 @@ public FeatureInfoResponse getMapFeatures(String collectionId, FeatureRequest re return null; } - public DescribeLayerResponse describeLayer(String collectionId, FeatureRequest request) { - Optional mapServerUrl = getMapServerUrl(collectionId, request); - - if (mapServerUrl.isPresent()) { - List urls = createMapDescribeUrl(mapServerUrl.get(), collectionId, request); - // Try one by one, we exit when any works - for (String url : urls) { - try { - ResponseEntity response = restTemplateUtils.handleRedirect(url, restTemplate.exchange(url, HttpMethod.GET, pretendUserEntity, String.class), String.class, pretendUserEntity); - if (response.getStatusCode().is2xxSuccessful()) { - DescribeLayerResponse layer = xmlMapper.readValue(response.getBody(), DescribeLayerResponse.class); - if (layer.getLayerDescription() != null) { - return layer; - } - } - } catch (RestClientException | URISyntaxException | JsonProcessingException pe) { - log.debug("Exception ignored it as we will retry", pe); - throw new RuntimeException(pe); - } - } - } - return null; - } - /** * Get the wms image/png tile * @@ -572,24 +555,97 @@ public byte[] getMapTile(String collectionId, FeatureRequest request) throws URI } /** - * Query the field using WMS's DescriberLayer function to find out the associated WFS layer and fields + * Query by 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 + * @param collectionId - The uuid + * @param request - The request containing layer name + * @return - DescribeLayerResponse containing the layer and associated wfs info, this is used to find the real layer name and wfs server url for the field query + */ + public DescribeLayerResponse describeLayer(String collectionId, FeatureRequest request) { + Optional mapServerUrl = getMapServerUrl(collectionId, request); + if (mapServerUrl.isPresent()) { + List urls = createMapDescribeUrl(mapServerUrl.get(), collectionId, request); + // Try one by one, we exit when any works + for (String url : urls) { + try { + ResponseEntity response = restTemplateUtils.handleRedirect(url, restTemplate.exchange(url, HttpMethod.GET, pretendUserEntity, String.class), String.class, pretendUserEntity); + if (response.getStatusCode().is2xxSuccessful()) { + DescribeLayerResponse layer = xmlMapper.readValue(response.getBody(), DescribeLayerResponse.class); + if (layer.getLayerDescription() != null) { + return layer; + } + } + } catch (RestClientException | URISyntaxException | JsonProcessingException pe) { + log.debug("Exception ignored it as we will retry, failed url {}", url, pe); + } + } + // All URLs tried, none worked + log.debug("No describe layer is found after trying all URLs for collectionId: {}", collectionId); + throw new GeoserverFieldsNotFoundException("No describe layer found for uuid " + collectionId); + } + return null; + } + + /** + * Fetch fields for a single layer using WFS.getDownloadableFields + * + * @param collectionId - The uuid of the metadata + * @param layerName - The layer name to fetch fields for + * @return - WFSFieldModel containing typename and fields, or null if not found */ - public List getDownloadableFields(String collectionId, FeatureRequest request) { + private WFSFieldModel fetchFieldsForLayer(String collectionId, String layerName) { + FeatureRequest layerRequest = FeatureRequest.builder().layerName(layerName).build(); - DescribeLayerResponse response = this.describeLayer(collectionId, request); + DescribeLayerResponse response = this.describeLayer(collectionId, layerRequest); if (response != null && response.getLayerDescription().getWfs() != null) { - // If we are able to find the wfs server and real layer name based on wms layer, then use it - FeatureRequest modified = FeatureRequest.builder().layerName(response.getLayerDescription().getQuery().getTypeName()).build(); - return wfsServer.getDownloadableFields(collectionId, modified, response.getLayerDescription().getWfs()); + // Use describe layer to find the real layer name and wfs server for fields + FeatureRequest requestWithDescribeLayer = FeatureRequest.builder() + .layerName(response.getLayerDescription().getQuery().getTypeName()) + .build(); + return wfsServer.getDownloadableFields(collectionId, requestWithDescribeLayer, response.getLayerDescription().getWfs()); + } else { + // Fallback: trust what is found inside the elastic search metadata + return wfsServer.getDownloadableFields(collectionId, layerRequest, null); + } + } + + /** + * Query by 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 + * @return - List of WFSFieldModel containing typename and fields for each WMS layer + */ + public List getWMSFields(String collectionId, FeatureRequest request) { + List wmsFields = new ArrayList<>(); + + List layerNamesToProcess = new ArrayList<>(); + // If layer name is provided, use it directly + // No layer name provided, get fields for all layer from collection WMS links + if (request.getLayerName() != null && !request.getLayerName().isEmpty()) { + layerNamesToProcess.add(request.getLayerName()); } else { - // We trust what is found inside the elastic search metadata - return wfsServer.getDownloadableFields(collectionId, request, null); + log.debug("No layer name provided in request, get fields for all WMS links"); + List wmsLinks = this.getWmsLinks(collectionId); + for (LinkModel wmsLink : wmsLinks) { + extractLayernameOrTypenameFromLink(wmsLink).ifPresent(layerNamesToProcess::add); + } + } + + // Fetch fields for each layer name + for (String layerName : layerNamesToProcess) { + WFSFieldModel fieldModel = fetchFieldsForLayer(collectionId, layerName); + if (fieldModel != null) { + wmsFields.add(fieldModel); + } } + + if (wmsFields.isEmpty()) { + throw new GeoserverFieldsNotFoundException("No fields found for uuid " + collectionId); + } + + return wmsFields; } /** @@ -795,6 +851,7 @@ public List getCapabilitiesLayers(String collectionId, FeatureRequest // Fetch all layers from GetCapabilities (this call is cached by URL) // Special rewrite to speed up query String url = rewriteUrlWithWorkSpace(mapServerUrl.get(), request); + List allLayers = self.fetchCapabilitiesLayersByUrl(url); List filteredLayers = filterLayers(collectionId, request, allLayers); @@ -833,11 +890,15 @@ public List getCapabilitiesLayers(String collectionId, FeatureRequest }); } - log.debug("Returning layers {}", filteredLayers); + if (filteredLayers.isEmpty()) { + throw new GeoserverLayersNotFoundException("No WMS layer is found for uuid " + collectionId); + } + + log.debug("Returning layers {}", filteredLayers); return filteredLayers; } - return Collections.emptyList(); + throw new GeoserverLayersNotFoundException("No valid WMS server url is found for uuid " + collectionId); } } 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 1b4f874b..5a19a6c0 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 @@ -85,27 +85,26 @@ public static Optional extractLayernameOrTypenameFromUrl(String url) { /** * Extract layer name or typename from a LinkModel. - * First tries link.title, then falls back to extracting from link.href URL query parameters. + * If link title does not match the typename/layername extracted from URL, return the extracted typename/layername. * * @param link - The LinkModel object - * @return layer name/typename if found, empty otherwise + * @return layer name/typename if found */ public static Optional extractLayernameOrTypenameFromLink(LinkModel link) { if (link == null) { return Optional.empty(); } - // Try to get layer name from link title first + Optional extractedName = extractLayernameOrTypenameFromUrl(link.getHref()); + if (link.getTitle() != null && !link.getTitle().isEmpty()) { + if (extractedName.isPresent() && !roughlyMatch(link.getTitle(), extractedName.get())) { + log.debug("Link title '{}' does not match type/layer name extracted from URL, return the extracted layer/type name {}", link.getTitle(), extractedName.get()); + return extractedName; + } return Optional.of(link.getTitle()); } - // Fallback: extract layer name from URL query parameters - if (link.getHref() != null) { - return extractLayernameOrTypenameFromUrl(link.getHref()); - } - - return Optional.empty(); + return extractedName; } - } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java index 9137160b..384f468b 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestApi.java @@ -122,17 +122,13 @@ public ResponseEntity getFeature( return featuresService.getWaveBuoysLatestDate(collectionId); } case wave_buoy_timeseries -> { - return featuresService.getWaveBuoyData(collectionId, request.getDatetime(), request.getWaveBuoy()); + return featuresService.getWaveBuoyData(collectionId, request.getDatetime(), request.getWaveBuoy()); } - case wfs_downloadable_fields -> { - return (request.getLayerName() == null || request.getLayerName().isEmpty()) ? - ResponseEntity.badRequest().build() : - featuresService.getWfsDownloadableFields(collectionId, request); + case wfs_fields -> { + return featuresService.getWfsFields(collectionId, request); } - case wms_downloadable_fields -> { - return (request.getLayerName() == null || request.getLayerName().isEmpty()) ? - ResponseEntity.badRequest().build() : - featuresService.getWmsDownloadableFields(collectionId, request); + case wms_fields -> { + return featuresService.getWmsFields(collectionId, request); } case wms_map_tile -> { return featuresService.getWmsMapTile(collectionId, request); @@ -151,6 +147,7 @@ public ResponseEntity getFeature( } } } + /** * @param collectionId - The collection id * @param limit - Limit of result return diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java index e577067d..f30a9d51 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java @@ -1,7 +1,6 @@ package au.org.aodn.ogcapi.server.features; import au.org.aodn.ogcapi.features.model.Collection; -import au.org.aodn.ogcapi.server.core.exception.DownloadableFieldsNotFoundException; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; import au.org.aodn.ogcapi.server.core.model.ogc.wms.FeatureInfoResponse; @@ -9,7 +8,7 @@ import au.org.aodn.ogcapi.server.core.service.DasService; import au.org.aodn.ogcapi.server.core.mapper.StacToCollection; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.DownloadableFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.OGCApiService; import au.org.aodn.ogcapi.server.core.service.wfs.WfsServer; @@ -87,56 +86,40 @@ public ResponseEntity getWmsMapTile(String collectionId, FeatureRequest } /** - * This is used to get the downloadable fields from wfs where layer name is not mentioned in wms + * This is used to get the WFS fields given a WFS layer * * @param collectionId - The uuid of dataset - * @param request - Request to get field given a layer name - * @return - The downloadable field name + * @param request -Request to get field given a WFS layer name; if no layer name provided, it will return fields for all WFS links in the collection + * @return - The WFS fields */ - public ResponseEntity getWfsDownloadableFields(String collectionId, FeatureRequest request) { + public ResponseEntity getWfsFields(String collectionId, FeatureRequest request) { + List result = wfsServer.getWFSFields(collectionId, request); - if (request.getLayerName() == null || request.getLayerName().isEmpty()) { - return ResponseEntity.badRequest().body("Layer name cannot be null or empty"); - } - - List result = wfsServer.getDownloadableFields(collectionId, request, null); - - return result.isEmpty() ? + return result == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(result); } /** - * This is used to get the downloadable fields from wfs given a wms layer + * This is used to get the WMS fields from the describe wfs layer given a wms layer * * @param collectionId - The uuid of dataset - * @param request - Request to get field given a WMS layer name - * @return - The downloadable field name, or UNAUTHORIZED if it is not in white list + * @param request - Request to get field given a WMS layer name; if no layer name provided, it will return fields for all WMS links in the collection + * @return - The WMS fields, or UNAUTHORIZED if it is not in white list */ - public ResponseEntity getWmsDownloadableFields(String collectionId, FeatureRequest request) { - - if (request.getLayerName() == null || request.getLayerName().isEmpty()) { - log.info("Layer name cannot be null or empty"); - return ResponseEntity.badRequest().body("Layer name cannot be null or empty"); - } - + public ResponseEntity getWmsFields(String collectionId, FeatureRequest request) { // Temp block and show only white list uuid, the other uuid need QA check before release. if (request.getEnableGeoServerWhiteList() && wmsDefaultParam.getAllowId() != null && !wmsDefaultParam.getAllowId().contains(collectionId)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - else { - try { - List result = wmsServer.getDownloadableFields(collectionId, request); + } else { + List result = wmsServer.getWMSFields(collectionId, request); - return result.isEmpty() ? - ResponseEntity.notFound().build() : - ResponseEntity.ok(result); - } - catch(DownloadableFieldsNotFoundException nfe) { - return ResponseEntity.notFound().build(); - } + return result.isEmpty() ? + ResponseEntity.notFound().build() : + ResponseEntity.ok(result); } } + /** * This is used to get all available layers from WMS GetCapabilities * @@ -160,15 +143,21 @@ public ResponseEntity getWmsLayers(String collectionId, FeatureRequest reques * @return - List of available feature types with name, title, abstract, etc. */ public ResponseEntity getWfsLayers(String collectionId, FeatureRequest request) { - List result = wfsServer.getCapabilitiesFeatureTypes(collectionId, request); + if (request.getEnableGeoServerWhiteList() && wmsDefaultParam.getAllowId() != null && !wmsDefaultParam.getAllowId().contains(collectionId)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } else { + List result = wfsServer.getCapabilitiesFeatureTypes(collectionId, request); + + return result.isEmpty() ? + ResponseEntity.notFound().build() : + ResponseEntity.ok(result); + } - return result.isEmpty() ? - ResponseEntity.notFound().build() : - ResponseEntity.ok(result); } + /** * @param collectionID - uuid - * @param from - + * @param from - * @return - */ public ResponseEntity getWaveBuoys(String collectionID, String from) { 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 82112a6b..2e7a796c 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadWfsDataServiceTest.java @@ -1,7 +1,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.DownloadableFieldModel; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; import au.org.aodn.ogcapi.server.core.model.ogc.wms.DescribeLayerResponse; import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; import org.junit.jupiter.api.BeforeEach; @@ -52,26 +52,29 @@ public void setUp() { } /** - * Helper method to create a list of downloadable fields for testing + * Helper method to create a WFSFieldModel for testing */ - private List createTestDownloadableFields() { - List fields = new ArrayList<>(); + private WFSFieldModel createTestWFSFieldModel() { + List fields = new ArrayList<>(); // Add geometry field - fields.add(DownloadableFieldModel.builder() + fields.add(WFSFieldModel.Field.builder() .name("geom") .label("geom") .type("geometrypropertytype") .build()); // Add datetime field - fields.add(DownloadableFieldModel.builder() + fields.add(WFSFieldModel.Field.builder() .name("timestamp") .label("timestamp") .type("dateTime") .build()); - return fields; + return WFSFieldModel.builder() + .typename("testLayer") + .fields(fields) + .build(); } @Test @@ -79,7 +82,7 @@ public void testPrepareWfsRequestUrl_WithNullDates() { // Setup String uuid = "test-uuid"; String layerName = "test:layer"; - List fields = createTestDownloadableFields(); + WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -91,7 +94,7 @@ public void testPrepareWfsRequestUrl_WithNullDates() { when(query.getTypeName()).thenReturn(layerName); when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); - when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(wfsFieldModel); Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); @@ -115,7 +118,7 @@ public void testPrepareWfsRequestUrl_WithEmptyDates() { // Setup String uuid = "test-uuid"; String layerName = "test:layer"; - List fields = createTestDownloadableFields(); + WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -127,7 +130,7 @@ public void testPrepareWfsRequestUrl_WithEmptyDates() { when(query.getTypeName()).thenReturn(layerName); when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); - when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(wfsFieldModel); Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); @@ -153,7 +156,7 @@ public void testPrepareWfsRequestUrl_WithValidDates() { String layerName = "test:layer"; String startDate = "2023-01-01"; String endDate = "2023-12-31"; - List fields = createTestDownloadableFields(); + WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -165,7 +168,7 @@ public void testPrepareWfsRequestUrl_WithValidDates() { when(query.getTypeName()).thenReturn(layerName); when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); - when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(wfsFieldModel); Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); @@ -193,7 +196,7 @@ public void testPrepareWfsRequestUrl_WithOnlyStartDate() { String uuid = "test-uuid"; String layerName = "test:layer"; String startDate = "2023-01-01"; - List fields = createTestDownloadableFields(); + WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -205,7 +208,7 @@ public void testPrepareWfsRequestUrl_WithOnlyStartDate() { when(query.getTypeName()).thenReturn(layerName); when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); - when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(wfsFieldModel); Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); @@ -231,7 +234,7 @@ public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { String layerName = "test:layer"; String startDate = "01-2023"; // MM-YYYY format String endDate = "12-2023"; // MM-YYYY format - List fields = createTestDownloadableFields(); + WFSFieldModel wfsFieldModel = createTestWFSFieldModel(); DescribeLayerResponse describeLayerResponse = mock(DescribeLayerResponse.class); DescribeLayerResponse.LayerDescription layerDescription = mock(DescribeLayerResponse.LayerDescription.class); @@ -243,7 +246,7 @@ public void testPrepareWfsRequestUrl_WithMMYYYYFormat() { when(query.getTypeName()).thenReturn(layerName); when(wmsServer.describeLayer(eq(uuid), any(FeatureRequest.class))).thenReturn(describeLayerResponse); - when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(fields); + when(wfsServer.getDownloadableFields(eq(uuid), any(FeatureRequest.class), anyString())).thenReturn(wfsFieldModel); Map defaultParams = new HashMap<>(); defaultParams.put("service", "WFS"); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java deleted file mode 100644 index e491373b..00000000 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wfs/DownloadableFieldsServiceTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package au.org.aodn.ogcapi.server.core.service.wfs; - -import au.org.aodn.ogcapi.server.core.configuration.Config; -import au.org.aodn.ogcapi.server.core.configuration.TestConfig; -import au.org.aodn.ogcapi.server.core.exception.DownloadableFieldsNotFoundException; -import au.org.aodn.ogcapi.server.core.mapper.StacToCollection; -import au.org.aodn.ogcapi.server.core.model.LinkModel; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; -import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; -import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureId; -import au.org.aodn.ogcapi.server.core.model.ogc.wfs.DownloadableFieldModel; -import au.org.aodn.ogcapi.server.core.service.DasService; -import au.org.aodn.ogcapi.server.core.service.ElasticSearch; -import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; -import au.org.aodn.ogcapi.server.core.service.wms.WmsDefaultParam; -import au.org.aodn.ogcapi.server.core.service.wms.WmsServer; -import au.org.aodn.ogcapi.server.features.RestApi; -import au.org.aodn.ogcapi.server.features.RestServices; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.web.client.RestTemplate; - -import java.util.ArrayList; -import java.util.List; - -import static au.org.aodn.ogcapi.server.core.service.wfs.WfsDefaultParam.WFS_LINK_MARKER; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -@SpringBootTest(classes = { - TestConfig.class, - Config.class, - RestApi.class, - RestServices.class, - WfsServer.class, - WfsDefaultParam.class, - WmsDefaultParam.class, - DownloadableFieldsService.class, - JacksonAutoConfiguration.class, - CacheAutoConfiguration.class -}) -public class DownloadableFieldsServiceTest { - - @Autowired - private RestTemplate restTemplate; - - @Autowired - private WfsServer wfsServerConfig; - - @Autowired - private DownloadableFieldsService downloadableFieldsService; - - @Autowired - private RestApi restApi; - - @Autowired - private WfsServer wfsServer; - - @Autowired - @Qualifier("pretendUserEntity") - private HttpEntity entity; - - @MockitoBean - private DasService dasService; - - @MockitoBean - private StacToCollection stacToCollection; - - @MockitoBean - private WmsServer wmsServer; - - @MockitoBean - private ElasticSearch search; - - private static final String AUTHORIZED_SERVER = "https://geoserver-123.aodn.org.au/geoserver/wfs"; - private static final String UNAUTHORIZED_SERVER = "https://unauthorized-server.com/wfs"; - - // Helper method to create FeatureRequest for testing - private FeatureRequest createDownloadableFieldsRequest(String serverUrl, String layerName) { - return FeatureRequest.builder() - .layerName(layerName) - .build(); - } - - @BeforeEach - public void resetMock() { - Mockito.reset(search); - } - - @Test - public void testGetDownloadableFieldsSuccess() { - // Mock successful WFS response with geometry and datetime fields - String mockWfsResponse = """ - - - - - - - - - - - - - - - """; - - FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); - - ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); - stac.setCollections(new ArrayList<>()); - stac.getCollections().add( - StacCollectionModel - .builder() - .links(List.of( - LinkModel.builder() - .href("http://geoserver-123.aodn.org.au/geoserver/ows") - .title(request.getLayerName()) - .aiGroup(WFS_LINK_MARKER) - .build()) - ) - .build() - ); - - String id = "id"; - - when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) - .thenReturn(new ResponseEntity<>(mockWfsResponse, HttpStatus.OK)); - - when(search.searchCollections(eq(id))) - .thenReturn(stac); - - List result = wfsServer.getDownloadableFields(id, request, null); - - assertNotNull(result); - assertEquals(2, result.size()); - - // Check geometry field - DownloadableFieldModel geomField = result.stream() - .filter(f -> "geom".equals(f.getName())) - .findFirst() - .orElse(null); - assertNotNull(geomField); - assertEquals("geom", geomField.getLabel()); - assertEquals("geometrypropertytype", geomField.getType()); - - // Check datetime field - DownloadableFieldModel timeField = result.stream() - .filter(f -> "timestamp".equals(f.getName())) - .findFirst() - .orElse(null); - assertNotNull(timeField); - assertEquals("timestamp", timeField.getLabel()); - assertEquals("dateTime", timeField.getType()); - } - - @Test - public void testGetDownloadableFieldsEmptyResponse() { - // Mock WFS response with no geometry or datetime fields - String mockWfsResponse = """ - - - - - - - - - - - - - - """; - FeatureRequest request = FeatureRequest.builder().layerName("test:layer2").build(); - - ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); - stac.setCollections(new ArrayList<>()); - stac.getCollections().add( - StacCollectionModel - .builder() - .links(List.of( - LinkModel.builder() - .href("http://geoserver-123.aodn.org.au/geoserver/ows") - .title(request.getLayerName()) - .aiGroup(WFS_LINK_MARKER) - .build()) - ) - .build() - ); - - String id = "id2"; - - when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) - .thenReturn(new ResponseEntity<>(mockWfsResponse, HttpStatus.NOT_FOUND)); - - when(search.searchCollections(eq(id))) - .thenReturn(stac); - - DownloadableFieldsNotFoundException exception = assertThrows( - DownloadableFieldsNotFoundException.class, - () -> wfsServer.getDownloadableFields(id, request, null) - ); - - assertEquals("No downloadable fields found for all url", - exception.getMessage(), - "Exception not match" - ); - } - - @Test - public void testGetDownloadableFieldsWfsError() { - FeatureRequest request = FeatureRequest.builder().layerName("invalid:layer").build(); - - ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); - stac.setCollections(new ArrayList<>()); - stac.getCollections().add( - StacCollectionModel - .builder() - .links(List.of( - LinkModel.builder() - .href("http://geoserver-123.aodn.org.au/geoserver/ows") - .title(request.getLayerName()) - .aiGroup(WFS_LINK_MARKER) - .build()) - ) - .build() - ); - - String id = "id3"; - - when(search.searchCollections(eq(id))) - .thenReturn(stac); - - when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) - .thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); - - DownloadableFieldsNotFoundException exception = assertThrows( - DownloadableFieldsNotFoundException.class, - () -> wfsServer.getDownloadableFields(id, request, null) - ); - - assertTrue(exception.getMessage().contains("No downloadable fields found")); - } - - @Test - public void testGetDownloadableFieldsNetworkError() { - FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); - - ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); - stac.setCollections(new ArrayList<>()); - stac.getCollections().add( - StacCollectionModel - .builder() - .links(List.of( - LinkModel.builder() - .href("http://geoserver-123.aodn.org.au/geoserver/ows") - .title(request.getLayerName()) - .aiGroup(WFS_LINK_MARKER) - .build()) - ) - .build() - ); - - String id = "id4"; - - when(search.searchCollections(eq(id))) - .thenReturn(stac); - - // Mock network error - when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) - .thenThrow(new RuntimeException("Connection timeout")); - - RuntimeException exception = assertThrows( - RuntimeException.class, - () -> wfsServer.getDownloadableFields(id, request, null) - ); - - assertTrue(exception.getMessage().contains("Connection timeout")); - } - - @Test - public void testRestApiDownloadableFieldsMissingWongUuid() { - FeatureRequest request = createDownloadableFieldsRequest(null, "test:layer"); - - ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); - stac.setCollections(new ArrayList<>()); - String id = "id5"; - - when(search.searchCollections(eq(id))) - .thenReturn(stac); - - ResponseEntity response = restApi.getFeature( - id, - FeatureId.wfs_downloadable_fields.name(), - request - ); - - assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode(), "Should return 400 for missing wong uuid"); - } - - @Test - public void testRestApiDownloadableFieldsMissingLayerName() { - FeatureRequest request = createDownloadableFieldsRequest("https://test.com/wfs", null); - - ResponseEntity response = restApi.getFeature( - "test-collection", - FeatureId.wfs_downloadable_fields.name(), - request - ); - - assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode(), "Should return 400 for missing layerName"); - } -} diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java index 6fe78f58..d0aa5df4 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/wms/WmsServerTest.java @@ -3,13 +3,13 @@ import au.org.aodn.ogcapi.server.core.configuration.Config; import au.org.aodn.ogcapi.server.core.configuration.TestConfig; import au.org.aodn.ogcapi.server.core.configuration.WfsWmsConfig; +import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; import au.org.aodn.ogcapi.server.core.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wms.DescribeLayerResponse; 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.WfsDefaultParam; import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.*; @@ -33,8 +33,7 @@ import static au.org.aodn.ogcapi.server.core.service.wfs.WfsDefaultParam.WFS_LINK_MARKER; import static au.org.aodn.ogcapi.server.core.service.wms.WmsDefaultParam.WMS_LINK_MARKER; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; @@ -45,7 +44,6 @@ WfsWmsConfig.class, WmsDefaultParam.class, WfsDefaultParam.class, - DownloadableFieldsService.class, JacksonAutoConfiguration.class, CacheAutoConfiguration.class} ) @@ -268,9 +266,10 @@ public void verifyHandleServiceExceptionReportCorrect() { when(search.searchCollections(eq(id))) .thenReturn(stac); - DescribeLayerResponse response = wmsServer.describeLayer(id, request); - assertNull(response, "Null expected as Service Exception ignored"); + // When layer is not found on the server, DownloadableFieldsNotFoundException is thrown + assertThrows(GeoserverFieldsNotFoundException.class, () -> wmsServer.describeLayer(id, request), + "DownloadableFieldsNotFoundException expected when layer is not found on server"); } @Test @@ -289,6 +288,7 @@ public void verifyParseCorrect() throws JsonProcessingException { assertEquals("https://geoserver-123.aodn.org.au/geoserver/wfs?", value.getLayerDescription().getWfs()); assertEquals("imos:srs_ghrsst_l4_gamssa_url", value.getLayerDescription().getQuery().getTypeName()); } + /** * Test with only one dateTime field in the describe layer */ @@ -350,6 +350,7 @@ public void verifyCreateCQLSingleDateTime() { assertEquals("CQL_FILTER=time DURING 2023-01-01/2023-12-31", result); } + /** * Test with only one dateTime field in the describe layer with predefined cql */ @@ -411,6 +412,7 @@ public void verifyCreateCQLSingleDateTimeWithCQL() { assertEquals("CQL_FILTER=time DURING 2023-01-01/2023-12-31 AND set_code=1234", result); } + /** * Test with only two or more dateTime field in the describe layer with predefined cql */ @@ -505,6 +507,7 @@ public void verifyCreateCQLMultiDateTimeWithCQL() { assertEquals("CQL_FILTER=juld DURING 2023-01-01/2023-12-31 AND set_code=1234", result); } + /** * Test where dateTime is a range start_xxx end_xxx */ 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 index 9e1ed002..185b0cb2 100644 --- 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 @@ -1,11 +1,14 @@ package au.org.aodn.ogcapi.server.service.wfs; +import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; import au.org.aodn.ogcapi.server.core.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; +import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WFSFieldModel; 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.WfsDefaultParam; 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; @@ -13,17 +16,19 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.List; +import java.util.Map; import static au.org.aodn.ogcapi.server.core.service.wfs.WfsDefaultParam.WFS_LINK_MARKER; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.anyString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,20 +38,25 @@ public class WfsServerTest { Search mockSearch; @Mock - DownloadableFieldsService downloadableFieldsService; + RestTemplate restTemplate; @Mock - RestTemplate restTemplate; + HttpEntity entity; - @Autowired - @Qualifier("pretendUserEntity") - private HttpEntity entity; + @Mock + WfsDefaultParam wfsDefaultParam; AutoCloseable closeableMock; @BeforeEach public void setUp() { closeableMock = MockitoAnnotations.openMocks(this); + // Setup default param mock + when(wfsDefaultParam.getFields()).thenReturn(Map.of( + "service", "WFS", + "version", "2.0.0", + "request", "DescribeFeatureType" + )); } @AfterEach @@ -63,7 +73,7 @@ void noCollection_returnsEmptyFeatureTypes() { result.setCollections(Collections.emptyList()); when(mockSearch.searchCollections(anyString())).thenReturn(result); - WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate), entity); + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); List featureTypes = Collections.singletonList(FeatureTypeInfo.builder().build()); assertEquals(Collections.emptyList(), server.filterFeatureTypesByWfsLinks("id", featureTypes)); @@ -79,7 +89,7 @@ void noWfsLinks_returnsEmptyFeatureTypes() { when(mockSearch.searchCollections(anyString())).thenReturn(result); - WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate), entity); + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); List featureTypes = Collections.singletonList(FeatureTypeInfo.builder().build()); assertEquals(Collections.emptyList(), server.filterFeatureTypesByWfsLinks("id", featureTypes)); @@ -105,10 +115,221 @@ void primaryTitleMatch_filtersMatchingFeatureTypes() { result.setCollections(List.of(model)); when(mockSearch.searchCollections(anyString())).thenReturn(result); - WfsServer server = new WfsServer(mockSearch, downloadableFieldsService, restTemplate, new RestTemplateUtils(restTemplate), entity); + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); List info = server.filterFeatureTypesByWfsLinks("id", featureTypes); assertEquals(1, info.size(), "FeatureType count match"); assertEquals(featureTypes.get(0), info.get(0), "FeatureType test_feature_type found"); } + + @Test + public void testGetDownloadableFieldsSuccess() { + // Mock successful WFS response with geometry and datetime fields + String mockWfsResponse = """ + + + + + + + + + + + + + + + + """; + + FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); + + ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); + stac.setCollections(List.of( + StacCollectionModel.builder() + .links(List.of( + LinkModel.builder() + .href("http://geoserver-123.aodn.org.au/geoserver/ows") + .title(request.getLayerName()) + .aiGroup(WFS_LINK_MARKER) + .build()) + ) + .build() + )); + + String id = "id"; + + when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) + .thenReturn(new ResponseEntity<>(mockWfsResponse, HttpStatus.OK)); + + when(mockSearch.searchCollections(eq(id))) + .thenReturn(stac); + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + WFSFieldModel result = server.getDownloadableFields(id, request, null); + + assertNotNull(result); + assertNotNull(result.getFields()); + assertEquals("testLayer", result.getTypename()); + assertEquals(3, result.getFields().size()); + + // Check geometry field + WFSFieldModel.Field geomField = result.getFields().stream() + .filter(f -> "geom".equals(f.getName())) + .findFirst() + .orElse(null); + assertNotNull(geomField); + assertEquals("geom", geomField.getLabel()); + assertEquals("GeometryPropertyType", geomField.getType()); + + // Check datetime field + WFSFieldModel.Field timeField = result.getFields().stream() + .filter(f -> "timestamp".equals(f.getName())) + .findFirst() + .orElse(null); + assertNotNull(timeField); + assertEquals("timestamp", timeField.getLabel()); + assertEquals("dateTime", timeField.getType()); + + // Check string field + WFSFieldModel.Field nameField = result.getFields().stream() + .filter(f -> "name".equals(f.getName())) + .findFirst() + .orElse(null); + assertNotNull(nameField); + assertEquals("name", nameField.getLabel()); + assertEquals("string", nameField.getType()); + } + + @Test + public void testGetDownloadableFieldsNotFoundResponse() { + // Mock WFS response with NOT_FOUND status + FeatureRequest request = FeatureRequest.builder().layerName("test:layer2").build(); + + ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); + stac.setCollections(List.of( + StacCollectionModel.builder() + .links(List.of( + LinkModel.builder() + .href("http://geoserver-123.aodn.org.au/geoserver/ows") + .title(request.getLayerName()) + .aiGroup(WFS_LINK_MARKER) + .build()) + ) + .build() + )); + + String id = "id2"; + + when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.NOT_FOUND)); + + when(mockSearch.searchCollections(eq(id))) + .thenReturn(stac); + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + + GeoserverFieldsNotFoundException exception = assertThrows( + GeoserverFieldsNotFoundException.class, + () -> server.getDownloadableFields(id, request, null) + ); + + assertEquals("No downloadable fields found for all url", + exception.getMessage(), + "Exception not match" + ); + } + + @Test + public void testGetDownloadableFieldsWfsError() { + FeatureRequest request = FeatureRequest.builder().layerName("invalid:layer").build(); + + ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); + stac.setCollections(List.of( + StacCollectionModel.builder() + .links(List.of( + LinkModel.builder() + .href("http://geoserver-123.aodn.org.au/geoserver/ows") + .title(request.getLayerName()) + .aiGroup(WFS_LINK_MARKER) + .build()) + ) + .build() + )); + + String id = "id3"; + + when(mockSearch.searchCollections(eq(id))) + .thenReturn(stac); + + when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) + .thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + + GeoserverFieldsNotFoundException exception = assertThrows( + GeoserverFieldsNotFoundException.class, + () -> server.getDownloadableFields(id, request, null) + ); + + assertTrue(exception.getMessage().contains("No downloadable fields found")); + } + + @Test + public void testGetDownloadableFieldsNetworkError() { + FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); + + ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); + stac.setCollections(List.of( + StacCollectionModel.builder() + .links(List.of( + LinkModel.builder() + .href("http://geoserver-123.aodn.org.au/geoserver/ows") + .title(request.getLayerName()) + .aiGroup(WFS_LINK_MARKER) + .build()) + ) + .build() + )); + + String id = "id4"; + + when(mockSearch.searchCollections(eq(id))) + .thenReturn(stac); + + // Mock network error + when(restTemplate.exchange(any(String.class), eq(HttpMethod.GET), eq(entity), eq(String.class))) + .thenThrow(new RuntimeException("Connection timeout")); + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + + RuntimeException exception = assertThrows( + RuntimeException.class, + () -> server.getDownloadableFields(id, request, null) + ); + + assertTrue(exception.getMessage().contains("Connection timeout")); + } + + @Test + public void testGetDownloadableFieldsNoCollection() { + FeatureRequest request = FeatureRequest.builder().layerName("test:layer").build(); + + ElasticSearchBase.SearchResult stac = new ElasticSearchBase.SearchResult<>(); + stac.setCollections(Collections.emptyList()); + String id = "id5"; + + when(mockSearch.searchCollections(eq(id))) + .thenReturn(stac); + + WfsServer server = new WfsServer(mockSearch, restTemplate, new RestTemplateUtils(restTemplate), entity, wfsDefaultParam); + + WFSFieldModel result = server.getDownloadableFields(id, request, null); + + assertNull(result, "Should return null when no collection found"); + } }