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 @@ -56,14 +56,17 @@ public JCacheCacheManager cacheManager() throws IOException {
File storagePath = tempDir.toFile();
storagePath.deleteOnExit(); // Mark the directory for deletion on JVM exit

ResourcePoolsBuilder.newResourcePoolsBuilder();
org.ehcache.config.Configuration config = ConfigurationBuilder
.newConfigurationBuilder()
.withService(new DefaultPersistenceConfiguration(storagePath))
.withCache(CACHE_WMS_MAP_TILE,
// Entry-counted heap. A byte-sized heap asks Ehcache to measure each PNG,
// and a failed measurement rejects the put, so the tile is fetched again.
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Object.class, byte[].class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(100, MemoryUnit.MB)
ResourcePoolsBuilder
.heap(2000) // 2000 entries
.disk(10, MemoryUnit.GB, true)
)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofHours(24)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ public ResponseEntity<ErrorResponse> handleGeoserverLayersNotFoundException(Geos

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex, WebRequest request) {
// Log before building the body. A later failure to write ErrorResponse (for example
// when the request content type is image/png) replaces this exception in the log.
log.error("Unhandled exception on {} parameters {}",
request.getDescription(false),
request.getParameterMap()
.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + Arrays.toString(entry.getValue()))
.collect(Collectors.joining(", ", "{", "}")),
ex);

ErrorResponse errorResponse = ErrorResponse
.builder()
.timestamp(LocalDateTime.now())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,15 +555,15 @@ protected static String truncateForLog(String body) {
@Cacheable(value = CACHE_WMS_MAP_TILE)
public byte[] getMapTile(String collectionId, FeatureRequest request) throws URISyntaxException {
Optional<String> mapServerUrl = getMapServerUrl(collectionId, request);
log.debug("map tile request for uuid {} layername {}", collectionId, request.getLayerName());
log.debug("Geoserver tile request for uuid {} request -> {}", collectionId, request);
if (mapServerUrl.isPresent()) {
List<String> urls = createMapQueryUrl(mapServerUrl.get(), collectionId, request);
// Try one by one, we exit when any works
for (String url : urls) {
log.debug("map tile request for layer name {} url {} ", request.getLayerName(), url);
ResponseEntity<byte[]> response = restTemplateUtils.handleRedirect(url, restTemplate.exchange(url, HttpMethod.GET, pretendUserEntity, byte[].class), byte[].class, pretendUserEntity);
if (response.getStatusCode().is2xxSuccessful()) {
if (response.getHeaders().getContentType() != null && response.getHeaders().getContentType().getType().equals("image")) {
if (response.getHeaders().getContentType() != null && response.getHeaders().getContentType().isCompatibleWith(MediaType.IMAGE_PNG)) {
return response.getBody();
} else {
// Something wrong from the server likely syntax error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,40 @@ public static String toDisplayUtcDownloadBoundary(String value, boolean endBound
}
}

/**
* Snap an OGC interval to whole UTC days so the same calendar day is one cache key.
* The start becomes 00:00:00Z and the end becomes 23:59:59Z on their own days.
* A value that is not an interval is returned unchanged.
*
* @param datetime - An OGC datetime, for example 1970-01-01T00:00:00Z/2026-10-08T23:46:31Z
* @return - The same interval with both ends on day boundaries, or the original value
*/
public static String coverWholeUtcDays(String datetime) {
if (datetime == null || !datetime.contains("/")) {
return datetime;
}
String[] parts = datetime.split("/", -1);
if (parts.length != 2) {
return datetime;
}
return snapToUtcDay(parts[0], false) + "/" + snapToUtcDay(parts[1], true);
}

private static String snapToUtcDay(String value, boolean endOfDay) {
if (value == null || value.isBlank() || "..".equals(value.trim())) {
return value == null ? "" : value;
}
try {
LocalDate day = OffsetDateTime.parse(value.trim()).withOffsetSameInstant(ZoneOffset.UTC).toLocalDate();
OffsetDateTime bound = endOfDay
? day.atTime(23, 59, 59).atOffset(ZoneOffset.UTC)
: day.atStartOfDay().atOffset(ZoneOffset.UTC);
return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").format(bound);
} catch (DateTimeParseException e) {
return value;
}
}

public static String formatOGCDateTime(String startDate, String endDate) {
if(startDate == null || startDate.trim().isEmpty()) {
startDate = "..";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import au.org.aodn.ogcapi.features.model.Exception;
import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureId;
import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest;
import au.org.aodn.ogcapi.server.core.util.DatetimeUtils;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -122,6 +123,9 @@ public ResponseEntity<?> getFeature(
return featuresService.getWmsFields(collectionId, request);
}
case wms_map_tile -> {
// The client end instant may move every on refresh; therefore, we are never able to cache the image
// due to datetime is part of the key
request.setDatetime(DatetimeUtils.coverWholeUtcDays(request.getDatetime()));
return featuresService.getWmsMapTile(collectionId, request);
}
case wms_legend -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ public void testDisplayUtcDownloadBoundary_OmitsUnspecifiedValues() {
assertEquals("", DatetimeUtils.toDisplayUtcDownloadBoundary(null, true));
}

@Test
public void testCoverWholeUtcDays_SnapsIntervalToDayBounds() {
assertEquals("1970-01-01T00:00:00Z/2026-10-08T23:59:59Z",
DatetimeUtils.coverWholeUtcDays("1970-01-01T00:00:00Z/2026-10-08T23:46:31Z"));
}

@Test
public void testCoverWholeUtcDays_LeavesInstantAndOpenEnds() {
assertEquals("2022-07-13T05:30:00.000Z",
DatetimeUtils.coverWholeUtcDays("2022-07-13T05:30:00.000Z"));
assertEquals("../2026-10-08T23:59:59Z",
DatetimeUtils.coverWholeUtcDays("../2026-10-08T23:46:31Z"));
assertNull(DatetimeUtils.coverWholeUtcDays(null));
}

@Test
public void testValidateAndFormatDate_ValidYYYY_MM_DD() {
// Test valid YYYY-MM-DD format
Expand Down Expand Up @@ -118,45 +133,35 @@ public void testValidateAndFormatDate_NonSpecified() {
@Test
public void testValidateAndFormatDate_InvalidFormat() {
// Test invalid format throws exception
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
DatetimeUtils.validateAndFormatDate("2023/01/15", true);
});
Exception exception = assertThrows(IllegalArgumentException.class, () -> DatetimeUtils.validateAndFormatDate("2023/01/15", true));
assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format"));
}

@Test
public void testValidateAndFormatDate_InvalidMonth() {
// Test invalid month throws exception
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
DatetimeUtils.validateAndFormatDate("13-2023", true);
});
Exception exception = assertThrows(IllegalArgumentException.class, () -> DatetimeUtils.validateAndFormatDate("13-2023", true));
assertTrue(exception.getMessage().contains("Invalid month in date"));
}

@Test
public void testValidateAndFormatDate_InvalidFormat_Slashes() {
// Test date with slashes instead of dashes
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
DatetimeUtils.validateAndFormatDate("2023/02/15", true);
});
Exception exception = assertThrows(IllegalArgumentException.class, () -> DatetimeUtils.validateAndFormatDate("2023/02/15", true));
assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format"));
}

@Test
public void testValidateAndFormatDate_IncompleteDate() {
// Test incomplete date format
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
DatetimeUtils.validateAndFormatDate("2023-01", true);
});
Exception exception = assertThrows(IllegalArgumentException.class, () -> DatetimeUtils.validateAndFormatDate("2023-01", true));
assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format"));
}

@Test
public void testValidateAndFormatDate_RandomString() {
// Test random string throws exception
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
DatetimeUtils.validateAndFormatDate("random-text", true);
});
Exception exception = assertThrows(IllegalArgumentException.class, () -> DatetimeUtils.validateAndFormatDate("random-text", true));
assertTrue(exception.getMessage().contains("Date must be in MM-YYYY or YYYY-MM-DD format"));
}

Expand Down
Loading