From db6e22a92208f32232945df51b345221d89ebfc5 Mon Sep 17 00:00:00 2001 From: rng Date: Fri, 9 Oct 2026 10:45:17 +1100 Subject: [PATCH 1/2] Minor fix checkpoint --- .../ogcapi/server/core/configuration/CacheConfig.java | 7 +++++-- .../server/core/exception/GlobalExceptionHandler.java | 11 +++++++++++ .../server/core/service/geoserver/wms/WmsServer.java | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java index 2fb96081..e7350d07 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/configuration/CacheConfig.java @@ -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))) 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 8bb92b94..cbd29162 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 @@ -170,6 +170,17 @@ public ResponseEntity handleGeoserverLayersNotFoundException(Geos @ExceptionHandler(Exception.class) public ResponseEntity 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()) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java index 84ffef86..f7f694d0 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java @@ -555,7 +555,7 @@ protected static String truncateForLog(String body) { @Cacheable(value = CACHE_WMS_MAP_TILE) public byte[] getMapTile(String collectionId, FeatureRequest request) throws URISyntaxException { Optional 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 urls = createMapQueryUrl(mapServerUrl.get(), collectionId, request); // Try one by one, we exit when any works @@ -563,7 +563,7 @@ public byte[] getMapTile(String collectionId, FeatureRequest request) throws URI log.debug("map tile request for layer name {} url {} ", request.getLayerName(), url); ResponseEntity 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 From ad5459e5d4b859cd0f65f561301ee2fd6d46c779 Mon Sep 17 00:00:00 2001 From: rng Date: Fri, 9 Oct 2026 11:08:11 +1100 Subject: [PATCH 2/2] Fix bug with cache tiler due to end date keep changing on request --- .../server/core/util/DatetimeUtils.java | 34 ++++++++++++++++++ .../aodn/ogcapi/server/features/RestApi.java | 4 +++ .../server/core/util/DatetimeUtilsTest.java | 35 +++++++++++-------- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java index f0f5924b..32b13d6a 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtils.java @@ -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 = ".."; 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 236f630c..14f248ce 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 @@ -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; @@ -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 -> { diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java index 03162fbf..db57902c 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/DatetimeUtilsTest.java @@ -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 @@ -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")); }