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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ENV MAX_HEAP_PERCENTAGE=70
COPY ./server/target/ogcapi-java-server-*-exec.jar app.jar
ENTRYPOINT ["/bin/sh", "-c", "java \
-XX:MaxRAMPercentage=${MAX_HEAP_PERCENTAGE} \
-Duser.timezone=UTC \
-Delasticsearch.index.name=${INDEX_NAME} \
-Delasticsearch.cloud_optimized_index.name=${CO_INDEX_NAME} \
-Delasticsearch.vocabs_index.name=${VOCABS_INDEX_NAME} \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package au.org.aodn.ogcapi.server.core.util;

import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;
import java.util.regex.Pattern;

public class DatetimeUtils {
private static final Pattern MM_YYYY_PATTERN = Pattern.compile("^(\\d{2})-(\\d{4})$");
private static final Pattern YYYY_MM_DD_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$");
private static final DateTimeFormatter ISO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// Email display format (dd MMM yyyy), e.g. 05 Jan 2024; English so the month is stable across locales
private static final DateTimeFormatter DISPLAY_DATE_FORMAT =
DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH);
private static final DateTimeFormatter MONTH_YEAR_FORMAT = DateTimeFormatter.ofPattern("MM-yyyy");
// Email display format with an explicit UTC indicator; English keeps month names stable across locales.
private static final DateTimeFormatter DISPLAY_UTC_DATE_TIME_FORMAT =
DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss 'UTC'", Locale.ENGLISH)
.withZone(ZoneOffset.UTC);
public static final String NON_SPECIFIED_DATE = "non-specified";
// Frontend default lower bound (dateDefault.min); its default upper bound is "now"
public static final String DEFAULT_MIN_DATE = "1970-01-01";
Expand Down Expand Up @@ -39,24 +47,52 @@ public static boolean isOpenUpperBound(String date) {
return true;
}
try {
return !java.time.LocalDate.parse(date.trim(), ISO_DATE_FORMAT).isBefore(java.time.LocalDate.now());
} catch (java.time.format.DateTimeParseException e) {
return !LocalDate.parse(date.trim(), ISO_DATE_FORMAT).isBefore(LocalDate.now());
} catch (DateTimeParseException e) {
return false;
}
}

/**
* Format an ISO date (yyyy-MM-dd) for display as dd MMM yyyy, e.g. 05 Jan 2024.
* Returns "" for null/empty/non-specified, and falls back to the raw input if it cannot be parsed.
* Format a download/subsetting boundary in UTC for display in notification emails.
* A date-only value is the inclusive UTC day processed by the backend, displayed
* at whole-second precision from 00:00:00 UTC through 23:59:59 UTC. A month-only
* value uses the equivalent boundaries for its first and last days. An ISO-8601
* timestamp with an offset is converted to the same instant in UTC. This method is
* deliberately specific to download filters; Tiler date keys are opaque calendar
* keys and must not be passed through it.
*
* @return an explicit UTC timestamp, an empty string for an unspecified value, or
* the unmodified input when it is not a supported temporal value
*/
public static String toDisplayDate(String isoDate) {
if (isoDate == null || isoDate.trim().isEmpty() || NON_SPECIFIED_DATE.equalsIgnoreCase(isoDate.trim())) {
public static String toDisplayUtcDownloadBoundary(String value, boolean endBoundary) {
Comment thread
utas-raymondng marked this conversation as resolved.
if (value == null || value.trim().isEmpty() || NON_SPECIFIED_DATE.equalsIgnoreCase(value.trim())) {
return "";
}

String trimmed = value.trim();
try {
return java.time.LocalDate.parse(isoDate.trim(), ISO_DATE_FORMAT).format(DISPLAY_DATE_FORMAT);
} catch (java.time.format.DateTimeParseException e) {
return isoDate;
LocalDate date = LocalDate.parse(trimmed, ISO_DATE_FORMAT);
return DISPLAY_UTC_DATE_TIME_FORMAT.format(endBoundary
? date.atTime(LocalTime.MAX).toInstant(ZoneOffset.UTC)
: date.atStartOfDay().toInstant(ZoneOffset.UTC));
} catch (DateTimeParseException ignored) {
// It may be a month or an ISO-8601 timestamp with an explicit offset.
}

try {
YearMonth month = YearMonth.parse(trimmed, MONTH_YEAR_FORMAT);
return DISPLAY_UTC_DATE_TIME_FORMAT.format(endBoundary
? month.atEndOfMonth().atTime(LocalTime.MAX).toInstant(ZoneOffset.UTC)
: month.atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC));
} catch (DateTimeParseException ignored) {
// It may already be an ISO-8601 timestamp with an explicit offset.
}

try {
return DISPLAY_UTC_DATE_TIME_FORMAT.format(OffsetDateTime.parse(trimmed).toInstant());
} catch (DateTimeParseException ignored) {
return value;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ public static String generateSubsettingSection(
ObjectMapper objectMapper
) {
try {
// Format dates for display as dd MMM yyyy, e.g. 05 Jan 2024
String displayStartDate = DatetimeUtils.toDisplayDate(startDate);
String displayEndDate = DatetimeUtils.toDisplayDate(endDate);
// Download dates are UTC temporal filters (not Tiler calendar keys). Show
// the same inclusive day boundaries that the backend processes.
String displayStartDate = DatetimeUtils.toDisplayUtcDownloadBoundary(startDate, false);
String displayEndDate = DatetimeUtils.toDisplayUtcDownloadBoundary(endDate, true);

// The frontend default full range (1970-01-01 to now) means "no temporal subset" - treat as unspecified
boolean isOpenRange = DatetimeUtils.isDefaultLowerBound(startDate) && DatetimeUtils.isOpenUpperBound(endDate);
Expand Down
25 changes: 25 additions & 0 deletions server/src/test/java/au/org/aodn/ogcapi/server/ServerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package au.org.aodn.ogcapi.server;

import org.junit.jupiter.api.Test;

import java.time.ZoneId;
import java.util.TimeZone;

import static org.junit.jupiter.api.Assertions.assertEquals;

class ServerTest {

@Test
void initSetsUtcAsDefaultTimezone() {
TimeZone originalTimeZone = TimeZone.getDefault();
try {
TimeZone.setDefault(TimeZone.getTimeZone("Australia/Sydney"));

new Server().init();

assertEquals(ZoneId.of("UTC"), ZoneId.systemDefault());
} finally {
TimeZone.setDefault(originalTimeZone);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@
*/
public class DatetimeUtilsTest {

@Test
public void testDisplayUtcDownloadBoundary_DateOnlyUsesInclusiveDayLimits() {
assertEquals("05 Jan 2024 00:00:00 UTC",
DatetimeUtils.toDisplayUtcDownloadBoundary("2024-01-05", false));
assertEquals("05 Jan 2024 23:59:59 UTC",
DatetimeUtils.toDisplayUtcDownloadBoundary("2024-01-05", true));
}

@Test
public void testDisplayUtcDownloadBoundary_MonthUsesInclusiveMonthLimits() {
assertEquals("01 Feb 2024 00:00:00 UTC",
DatetimeUtils.toDisplayUtcDownloadBoundary("02-2024", false));
assertEquals("29 Feb 2024 23:59:59 UTC",
DatetimeUtils.toDisplayUtcDownloadBoundary("02-2024", true));
}

@Test
public void testDisplayUtcDownloadBoundary_NormalizesOffsetTimestamp() {
assertEquals("05 Jan 2024 00:30:00 UTC",
DatetimeUtils.toDisplayUtcDownloadBoundary("2024-01-05T11:30:00+11:00", false));
assertEquals("05 Jan 2024 00:30:00 UTC",
DatetimeUtils.toDisplayUtcDownloadBoundary("2024-01-05T00:30:00.123456789Z", true));
}

@Test
public void testDisplayUtcDownloadBoundary_OmitsUnspecifiedValues() {
assertEquals("", DatetimeUtils.toDisplayUtcDownloadBoundary("non-specified", false));
assertEquals("", DatetimeUtils.toDisplayUtcDownloadBoundary(null, true));
}

@Test
public void testValidateAndFormatDate_ValidYYYY_MM_DD() {
// Test valid YYYY-MM-DD format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,16 @@ void testOnlyDatesShowsSection() {
}

/**
* Test that dates are shown as dd MMM yyyy, e.g. 05 Jan 2024
* Date-only download filters represent inclusive UTC day boundaries.
*/
@Test
void testDatesUseDayMonthYearFormat() {
void testDatesShowInclusiveUtcRange() {
String result = EmailUtils.generateSubsettingSection(
"2024-01-05", "2024-12-31", null, new ObjectMapper()
);

assertTrue(result.contains("05 Jan 2024 - 31 Dec 2024"), "Expected dd MMM yyyy date format");
assertTrue(result.contains(
"05 Jan 2024 00:00:00 UTC - 31 Dec 2024 23:59:59 UTC"));
}

/**
Expand Down Expand Up @@ -298,7 +299,7 @@ void testDefaultLowerBoundWithRealEndShowsSection() {

assertFalse(result.isEmpty());
assertTrue(result.contains("Date Range"));
assertTrue(result.contains("01 Jan 1970 - 31 Dec 2024"));
assertTrue(result.contains("01 Jan 1970 00:00:00 UTC - 31 Dec 2024 23:59:59 UTC"));
}

/**
Expand Down
Loading