Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ public enum FeatureId {
wfs_fields("wfs_fields"), // Query field based on pure wfs and given layer
wfs_field_value("wfs_field_value"),
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"),
wave_buoy_all("wave_buoy_all"),
wave_buoys_between_dates("wave_buoys_between_dates"),
wave_buoys_latest_available_date("wave_buoys_latest_available_date"),
wave_buoy_details_between_dates("wave_buoy_details_between_dates"),
moorings_between_dates("moorings_between_dates"),
moorings_latest_available_date("moorings_latest_available_date"),
mooring_details_between_dates("mooring_details_between_dates"),
wms_map_tile("wms_map_tile"),
wms_map_feature("wms_map_feature"),
wms_layers("wms_layers"), // Get all available layers from WMS GetCapabilities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public String toString() {
@Schema(description = "Wave buoy name")
private String waveBuoy;

@Schema(description = "Mooring name")
private String mooring;

@Schema(description = "Enable or disable geoserver whitelist")
@Builder.Default
private Boolean enableGeoServerWhiteList = Boolean.TRUE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import jakarta.annotation.PostConstruct;

import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -35,54 +34,58 @@ public void init() {
httpEntity = new HttpEntity<>(headers);
}

public byte[] getWaveBuoys(String from, String to){
String waveBuoysUrlTemplate = UriComponentsBuilder.fromUriString(dasConfig.host + "/api/v1/das/data/feature-collection/wave-buoy")
.queryParam("start_date","{start_date}")
.queryParam("end_date","{end_date}")
.encode()
.toUriString();
Map<String,String> params = new HashMap<>();
params.put("start_date", from);
params.put("end_date",to);
/**
* GET a feature-collection from the DAS, optionally bounded by start/end date. Only the date
* query params that are non-null are added, so a null value is never passed to URI template
* expansion (which would throw). Any path variables in {@code path} are supplied via
* {@code pathVariables}.
*/
private byte[] getFeatureCollection(String path, String start, String end, Map<String, String> pathVariables) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(dasConfig.host + path);
Map<String, String> params = new HashMap<>(pathVariables);

if (start != null) {
builder.queryParam("start_date", "{start_date}");
params.put("start_date", start);
}
if (end != null) {
builder.queryParam("end_date", "{end_date}");
params.put("end_date", end);
}

String url = builder.encode().toUriString();
return httpClient.exchange(url, HttpMethod.GET, httpEntity, byte[].class, params).getBody();
}

return httpClient.exchange(waveBuoysUrlTemplate, HttpMethod.GET,httpEntity,byte[].class,params).getBody();
public byte[] getWaveBuoysBetweenDates(String start, String end) {
return getFeatureCollection("/api/v1/das/data/feature-collection/wave-buoy", start, end, Map.of());
}

public byte[] getWaveBuoysLatestDate(){
public byte[] getWaveBuoysLatestAvailableDate() {
String waveBuoysUrlTemplate = UriComponentsBuilder.fromUriString(dasConfig.host + "/api/v1/das/data/feature-collection/wave-buoy/latest")
.encode()
.toUriString();

return httpClient.exchange(waveBuoysUrlTemplate, HttpMethod.GET,httpEntity,byte[].class).getBody();
}

public byte[] getWaveBuoyData(String from, String to, String buoy){
String encodedBuoy = URLEncoder.encode(buoy, java.nio.charset.StandardCharsets.UTF_8);

String waveBuoyDataUrlTemplate = UriComponentsBuilder.fromUriString(dasConfig.host + "/api/v1/das/data/feature-collection/wave-buoy/" + encodedBuoy)
.queryParam("start_date","{start_date}")
.queryParam("end_date","{end_date}")
.encode()
.toUriString();
Map<String,String> params = new HashMap<>();
params.put("start_date", from);
params.put("end_date",to);
public byte[] getWaveBuoyDetailsBetweenDates(String startDateTime, String endDateTime, String buoy) {
return getFeatureCollection("/api/v1/das/data/feature-collection/wave-buoy/{buoy}", startDateTime, endDateTime, Map.of("buoy", buoy));
}

return httpClient.exchange(waveBuoyDataUrlTemplate, HttpMethod.GET,httpEntity,byte[].class,params).getBody();
public byte[] getMooringsBetweenDates(String start, String end) {
return getFeatureCollection("/api/v1/das/data/feature-collection/mooring", start, end, Map.of());
}

public byte[] getLatestWaveBuoySites(){
String waveBuoysUrlTemplate = UriComponentsBuilder.fromUriString(dasConfig.host + "/api/v1/das/data/feature-collection/wave-buoy/all")
public byte[] getMooringsLatestAvailableDate() {
String mooringsUrlTemplate = UriComponentsBuilder.fromUriString(dasConfig.host + "/api/v1/das/data/feature-collection/mooring/latest")
.encode()
.toUriString();

return httpClient.exchange(waveBuoysUrlTemplate, HttpMethod.GET,httpEntity,byte[].class).getBody();
return httpClient.exchange(mooringsUrlTemplate, HttpMethod.GET,httpEntity,byte[].class).getBody();
}

public boolean isCollectionSupported(String collectionId){
final String waveBuoyRealtimeCollectionID = "b299cdcd-3dee-48aa-abdd-e0fcdbb9cadc";
return waveBuoyRealtimeCollectionID.contentEquals(collectionId);
public byte[] getMooringDetailsBetweenDates(String startDateTime, String endDateTime, String mooring) {
return getFeatureCollection("/api/v1/das/data/feature-collection/mooring/{mooring}", startDateTime, endDateTime, Map.of("mooring", mooring));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,25 @@ public static String validateAndFormatDate(String dateInput, boolean isStartDate
throw new IllegalArgumentException("Date must be in MM-YYYY or YYYY-MM-DD format: " + dateInput);
}
}

/**
* Parse an ISO-8601 date-time and verify it is in UTC (offset 'Z').
*
* @param dateTime ISO-8601 date-time string, e.g. "2026-06-16T00:00:00Z"
* @return the parsed date-time
* @throws IllegalArgumentException if the value is not a valid ISO-8601 date-time or is not in UTC
*/
public static java.time.OffsetDateTime parseUtcDateTime(String dateTime) {
java.time.OffsetDateTime parsed;
try {
parsed = java.time.OffsetDateTime.parse(dateTime);
} catch (java.time.format.DateTimeParseException e) {
throw new IllegalArgumentException("Date-time must be a valid ISO-8601 UTC value (e.g. '2026-06-16T00:00:00Z'): " + dateTime);
}

if (!parsed.getOffset().equals(java.time.ZoneOffset.UTC)) {
throw new IllegalArgumentException("Date-time must be in UTC (offset 'Z'): " + dateTime);
}
return parsed;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,23 @@ public ResponseEntity<?> getFeature(
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
case wave_buoy_first_data_available -> {
return featuresService.getWaveBuoys(collectionId, request.getDatetime());
case wave_buoys_between_dates -> {
return featuresService.getWaveBuoysBetweenDates( request.getStartDateTime(), request.getEndDateTime());
}
case wave_buoy_latest_date -> {
return featuresService.getWaveBuoysLatestDate(collectionId);
case wave_buoys_latest_available_date -> {
return featuresService.getWaveBuoysLatestAvailableDate();
}
case wave_buoy_timeseries -> {
return featuresService.getWaveBuoyData(collectionId, request.getDatetime(), request.getWaveBuoy());
case wave_buoy_details_between_dates -> {
return featuresService.getWaveBuoyDetailsBetweenDates(request.getStartDateTime(), request.getEndDateTime(), request.getWaveBuoy());
}
case wave_buoy_all -> {
return featuresService.getLatestWaveBuoySites(collectionId);
case moorings_between_dates -> {
return featuresService.getMooringsBetweenDates(request.getStartDateTime(), request.getEndDateTime());
}
case moorings_latest_available_date -> {
return featuresService.getMooringsLatestAvailableDate();
}
case mooring_details_between_dates -> {
return featuresService.getMooringDetailsBetweenDates(request.getStartDateTime(), request.getEndDateTime(), request.getMooring());
}
case wfs_fields -> {
return featuresService.getWfsFields(collectionId, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import au.org.aodn.ogcapi.server.core.service.geoserver.wms.WmsDefaultParam;
import au.org.aodn.ogcapi.server.core.service.geoserver.wms.WmsServer;
import au.org.aodn.ogcapi.server.core.util.CommonUtils;
import au.org.aodn.ogcapi.server.core.util.DatetimeUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.geotools.feature.FeatureCollection;
Expand Down Expand Up @@ -224,99 +225,160 @@ public ResponseEntity<?> getWfsLayers(String collectionId, FeatureRequest reques
}
}


/**
* @param collectionID - uuid
* @param from -
* @return -
* Validate that startDateTime/endDateTime (when provided) are UTC ISO-8601 values and that the
* range is ordered.
*
* @return a bad-request response describing the problem, or empty if the range is valid
*/
public ResponseEntity<?> getWaveBuoys(String collectionID, String from) {
if (!dasService.isCollectionSupported(collectionID)) {
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build();
private Optional<ResponseEntity<?>> validateUtcDateRange(String startDateTime, String endDateTime) {
java.time.OffsetDateTime start;
java.time.OffsetDateTime end;
try {
start = startDateTime == null ? null : DatetimeUtils.parseUtcDateTime(startDateTime);
end = endDateTime == null ? null : DatetimeUtils.parseUtcDateTime(endDateTime);
} catch (IllegalArgumentException e) {
return Optional.of(ResponseEntity.badRequest().body(e.getMessage()));
}
if (from == null) {
return ResponseEntity.badRequest().body("Parameter 'datetime' is required and must be in 'from/to' format");

if (start != null && end != null && start.isAfter(end)) {
return Optional.of(ResponseEntity.badRequest().body("Parameter 'startDateTime' must not be after 'endDateTime'"));
}
return Optional.empty();
}

/**
* Returns wave buoy sites recorded within the given UTC date range.
*
* @param startDateTime,endDateTime must be UTC or null, since the underlying DAS service expects UTC only.
*/
public ResponseEntity<?> getWaveBuoysBetweenDates(String startDateTime, String endDateTime) {
Optional<ResponseEntity<?>> validationError = validateUtcDateRange(startDateTime, endDateTime);
if (validationError.isPresent()) {
return validationError.get();
}

java.time.ZonedDateTime fromDateTime = java.time.ZonedDateTime.parse(from);
String to = fromDateTime.plusDays(1)
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'"));
try {
return ResponseEntity
.ok()
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(dasService.getWaveBuoys(from, to));
.body(dasService.getWaveBuoysBetweenDates(startDateTime, endDateTime));

} catch (Exception e) {
log.error("Error fetching wave buoys data: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}

}

public ResponseEntity<?> getWaveBuoysLatestDate(String collectionID) {
if (!dasService.isCollectionSupported(collectionID)) {
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build();

/**
* Returns detailed data for a single wave buoy within the given UTC date range.
*
* @param startDateTime,endDateTime must be UTC or null, since the underlying DAS service expects UTC only.
* @param buoy the wave buoy identifier; required.
*/
public ResponseEntity<?> getWaveBuoyDetailsBetweenDates(String startDateTime, String endDateTime, String buoy) {
Optional<ResponseEntity<?>> validationError = validateUtcDateRange(startDateTime, endDateTime);
if (validationError.isPresent()) {
return validationError.get();
}
if (buoy == null) {
return ResponseEntity.badRequest().body("Parameter 'waveBuoy' is required");
}

try {
return ResponseEntity
.ok()
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(dasService.getWaveBuoyDetailsBetweenDates(startDateTime, endDateTime, buoy));

} catch (Exception e) {
log.error("Error fetching wave buoy historical data: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
}


/**
* Returns the latest date for which wave buoy data is available.
*/
public ResponseEntity<?> getWaveBuoysLatestAvailableDate() {
try {
return ResponseEntity
.ok()
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(dasService.getWaveBuoysLatestDate());
.body(dasService.getWaveBuoysLatestAvailableDate());

} catch (Exception e) {
log.error("Error fetching wave buoys latest date: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
}

public ResponseEntity<?> getWaveBuoyData(String collectionID, String datetime, String buoy) {
if (!dasService.isCollectionSupported(collectionID)) {
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build();
/**
* Returns mooring observations recorded within the given UTC date range.
*
* @param startDateTime,endDateTime must be UTC or null, since the underlying DAS service expects UTC only.
*/
public ResponseEntity<?> getMooringsBetweenDates(String startDateTime, String endDateTime) {
Optional<ResponseEntity<?>> validationError = validateUtcDateRange(startDateTime, endDateTime);
if (validationError.isPresent()) {
return validationError.get();
}
if (datetime == null) {
return ResponseEntity.badRequest().body("Parameter 'datetime' is required and must be in 'from/to' format");

try {
return ResponseEntity
.ok()
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(dasService.getMooringsBetweenDates(startDateTime, endDateTime));

} catch (Exception e) {
log.error("Error fetching moorings data: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
if (!datetime.contains("/")) {
return ResponseEntity.badRequest().body("Parameter 'datetime' must be in 'from/to' format");
}

/**
* Returns detailed observations for a single mooring within the given UTC date range.
*
* @param startDateTime,endDateTime must be UTC or null, since the underlying DAS service expects UTC only.
* @param mooring the mooring identifier; required.
*/
public ResponseEntity<?> getMooringDetailsBetweenDates(String startDateTime, String endDateTime, String mooring) {
Optional<ResponseEntity<?>> validationError = validateUtcDateRange(startDateTime, endDateTime);
if (validationError.isPresent()) {
return validationError.get();
}
if (buoy == null) {
return ResponseEntity.badRequest().body("Parameter 'waveBuoy' is required");
if (mooring == null) {
return ResponseEntity.badRequest().body("Parameter 'mooring' is required");
}

try {
String[] parts = datetime.split("/", 2);
String from = parts[0];
String to = parts[1];

return ResponseEntity
.ok()
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(dasService.getWaveBuoyData(from, to, buoy));
.body(dasService.getMooringDetailsBetweenDates(startDateTime, endDateTime, mooring));

} catch (Exception e) {
log.error("Error fetching wave buoy historical data: {}", e.getMessage());
log.error("Error fetching mooring historical data: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
}

/**
* This is to get all buoy sites with their latest available observation
*
* @param collectionID - uuid
* @return -
* Returns the latest date for which mooring data is available.
*/
public ResponseEntity<?> getLatestWaveBuoySites(String collectionID) {
if (!dasService.isCollectionSupported(collectionID)) {
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build();
}
public ResponseEntity<?> getMooringsLatestAvailableDate() {
try {
return ResponseEntity
.ok()
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.body(dasService.getLatestWaveBuoySites());
.body(dasService.getMooringsLatestAvailableDate());

} catch (Exception e) {
log.error("Error fetching wave buoy all unique sites date: {}", e.getMessage());
log.error("Error fetching moorings latest date: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
}
Expand Down
Loading
Loading