From 21e4c1fe11e37687b1f6faa3601349aaf88a718e Mon Sep 17 00:00:00 2001 From: amber Date: Fri, 31 Oct 2025 12:28:22 +1100 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=90=9B=20include=20collection=20title?= =?UTF-8?q?,=20metadata=20link=20and=20citation=20in=20download=20emails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enumeration/DatasetDownloadEnums.java | 5 +- .../aodn/ogcapi/server/processes/RestApi.java | 7 +- .../ogcapi/server/processes/RestServices.java | 19 ++- .../src/main/resources/job-started-email.html | 110 ++++++------------ .../ogcapi/server/processes/RestApiTest.java | 4 +- .../server/processes/RestServicesTest.java | 4 +- 6 files changed, 61 insertions(+), 88 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java index a4e4eb34..c49d1be8 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/DatasetDownloadEnums.java @@ -17,6 +17,9 @@ public enum Parameter { TYPE("type"), FIELDS("fields"), LAYER_NAME("layer_name"), + COLLECTION_TITLE("collection_title"), + FULL_METADATA_LINK("full_metadata_link"), + SUGGESTED_CITATION("suggested_citation"), ; private final String value; @@ -38,7 +41,7 @@ public enum Type { @Getter public enum JobDefinition { - GENERATE_CSV_DATA_FILE("generate-csv-data-file"); + GENERATE_CSV_DATA_FILE("generate-csv-data-file-dev"); private final String value; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java index d17ee4a4..402f5951 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestApi.java @@ -56,11 +56,14 @@ public ResponseEntity execute( var endDate = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.END_DATE.getValue()); var multiPolygon = body.getInputs().get(DatasetDownloadEnums.Parameter.MULTI_POLYGON.getValue()); var recipient = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.RECIPIENT.getValue()); + var collectionTitle = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.COLLECTION_TITLE.getValue()); + var fullMetadataLink = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.FULL_METADATA_LINK.getValue()); + var suggestedCitation = (String) body.getInputs().get(DatasetDownloadEnums.Parameter.SUGGESTED_CITATION.getValue()); // move the notify user email from data-access-service to here to make the first email faster - restServices.notifyUser(recipient, uuid, startDate, endDate, multiPolygon); + restServices.notifyUser(recipient, uuid, startDate, endDate, multiPolygon, collectionTitle, fullMetadataLink, suggestedCitation); - var response = restServices.downloadData(uuid, startDate, endDate, multiPolygon, recipient); + var response = restServices.downloadData(uuid, startDate, endDate, multiPolygon, recipient, collectionTitle, fullMetadataLink, suggestedCitation); var value = new InlineValue(response.getBody()); var status = new InlineValue(Integer.toString(HttpStatus.OK.value())); diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java index 464b1225..5f7d7948 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java @@ -41,13 +41,15 @@ public RestServices(BatchClient batchClient, ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - public void notifyUser(String recipient, String uuid, String startDate, String endDate, Object multiPolygon) { + public void notifyUser(String recipient, String uuid, String startDate, String endDate, Object multiPolygon, String collectionTitle, + String fullMetadataLink, + String suggestedCitation) { String aodnInfoSender = "no.reply@aodn.org.au"; try (SesClient ses = SesClient.builder().build()) { var subject = Content.builder().data("Start processing data file whose uuid is: " + uuid).build(); - var content = Content.builder().data(generateStartedEmailContent(uuid, startDate, endDate, multiPolygon)).build(); + var content = Content.builder().data(generateStartedEmailContent(uuid, startDate, endDate, multiPolygon, collectionTitle, fullMetadataLink, suggestedCitation)).build(); var destination = Destination.builder().toAddresses(recipient).build(); var body = Body.builder().html(content).build(); @@ -74,7 +76,10 @@ public ResponseEntity downloadData( String startDate, String endDate, Object polygons, - String recipient + String recipient, + String collectionTitle, + String fullMetadataLink, + String suggestedCitation ) throws JsonProcessingException { Map parameters = new HashMap<>(); @@ -83,6 +88,9 @@ public ResponseEntity downloadData( parameters.put(DatasetDownloadEnums.Parameter.END_DATE.getValue(), endDate); parameters.put(DatasetDownloadEnums.Parameter.MULTI_POLYGON.getValue(), objectMapper.writeValueAsString(polygons)); parameters.put(DatasetDownloadEnums.Parameter.RECIPIENT.getValue(), recipient); + parameters.put(DatasetDownloadEnums.Parameter.COLLECTION_TITLE.getValue(), collectionTitle); + parameters.put(DatasetDownloadEnums.Parameter.FULL_METADATA_LINK.getValue(), fullMetadataLink); + parameters.put(DatasetDownloadEnums.Parameter.SUGGESTED_CITATION.getValue(), suggestedCitation); parameters.put( DatasetDownloadEnums.Parameter.TYPE.getValue(), @@ -111,7 +119,7 @@ private String submitJob(String jobName, String jobQueue, String jobDefinition, return submitJobResponse.jobId(); } - private String generateStartedEmailContent(String uuid, String startDate, String endDate, Object multipolygon) { + private String generateStartedEmailContent(String uuid, String startDate, String endDate, Object multipolygon, String collectionTitle, String fullMetadataLink, String suggestedCitation) { try (InputStream inputStream = getClass().getResourceAsStream("/job-started-email.html")) { if (inputStream == null) { @@ -134,6 +142,9 @@ private String generateStartedEmailContent(String uuid, String startDate, String .replace("{{startDate}}", displayStartDate) .replace("{{endDate}}", displayEndDate) .replace("{{bboxContent}}", bboxHtml) + .replace("{{collectionTitle}}", collectionTitle != null ? collectionTitle : "") + .replace("{{fullMetadataLink}}", fullMetadataLink != null ? fullMetadataLink : "") + .replace("{{suggestedCitation}}", suggestedCitation != null ? suggestedCitation : "") .replace("{{HEADER_IMG}}", EmailUtils.readBase64Image("header.txt")) .replace("{{DOWNLOAD_ICON}}", EmailUtils.readBase64Image("download.txt")) .replace("{{BBOX_IMG}}", EmailUtils.readBase64Image("bbox.txt")) diff --git a/server/src/main/resources/job-started-email.html b/server/src/main/resources/job-started-email.html index 4281edba..3a85d644 100644 --- a/server/src/main/resources/job-started-email.html +++ b/server/src/main/resources/job-started-email.html @@ -219,7 +219,7 @@ - +
+
@@ -239,38 +239,6 @@
-
- - - - - - -
- -
- - - - - - -
- - - - - - -
-
-
- -
-
-
@@ -283,7 +251,7 @@ @@ -509,44 +477,6 @@
- +

Collection:

Collection: {{collectionTitle}}

-
- - - - - - -
- -
- - - - - - - - - - - - -
- - -

Attributes

-
-
-
- - -

Instrument: DALEC

-
-
- -
-
-
@@ -591,7 +521,7 @@ @@ -601,10 +531,36 @@ - -
- +

uuid is: {{uuid}}

Metadata Link: {{fullMetadataLink}}

-
- +
+ + +
+ + + + + + +
+ +
+ + + + + + +
+
+

{{suggestedCitation}}

+
+
+
+ +
+
+
diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestApiTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestApiTest.java index a0ea52db..97036903 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestApiTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestApiTest.java @@ -50,7 +50,7 @@ public void setUp() { @Test public void testExecuteDownloadDatasetSuccess() throws JsonProcessingException { - when(restServices.downloadData(any(), any(), any(), any(), any())) + when(restServices.downloadData(any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(ResponseEntity.ok("Job submitted with ID: test-job-id")); ResponseEntity response = restApi.execute(ProcessIdEnum.DOWNLOAD_DATASET.getValue(), executeRequest); @@ -65,7 +65,7 @@ public void testExecuteDownloadDatasetSuccess() throws JsonProcessingException { @Test public void testExecuteDownloadDatasetError() throws JsonProcessingException { - when(restServices.downloadData(any(), any(), any(), any(), any())) + when(restServices.downloadData(any(), any(), any(), any(), any(), any(), any(), any())) .thenThrow(new RuntimeException("Error while getting dataset")); ResponseEntity response = restApi.execute(ProcessIdEnum.DOWNLOAD_DATASET.getValue(), executeRequest); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestServicesTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestServicesTest.java index 43275fa6..07c5f483 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestServicesTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/processes/RestServicesTest.java @@ -49,7 +49,7 @@ public void testDownloadDataSuccess() throws JsonProcessingException { // Act ResponseEntity response = restServices.downloadData( - "test-uuid", "2023-01-01", "2023-01-31", "test-multipolygon", "test@example.com"); + "test-uuid", "2023-01-01", "2023-01-31", "test-multipolygon", "test@example.com", "Test Ocean Data Collection", "https://metadata.imas.utas.edu.au/.../test-uuid-123", "Cite data as: Mazor, T., Watermeyer, K., Hobley, T., Grinter, V., Holden, R., MacDonald, K. and Ferns, L. (2023)."); // Assert assertEquals(ResponseEntity.ok("Job submitted with ID: " + jobId), response); @@ -63,7 +63,7 @@ public void testDownloadDataJsonProcessingException() throws JsonProcessingExcep // Act & Assert try { - restServices.downloadData("test-uuid", "2023-01-01", "2023-01-31", "test-multipolygon", "test@example.com"); + restServices.downloadData("test-uuid", "2023-01-01", "2023-01-31", "test-multipolygon", "test@example.com","Test Ocean Data Collection", "https://metadata.imas.utas.edu.au/.../test-uuid-123", "Cite data as: Mazor, T., Watermeyer, K., Hobley, T., Grinter, V., Holden, R., MacDonald, K. and Ferns, L. (2023)."); } catch (JsonProcessingException e) { assertEquals("Error", e.getMessage()); } From 5c85f5152eb0f86e01441280983f8b0cb085546d Mon Sep 17 00:00:00 2001 From: amber Date: Fri, 31 Oct 2025 14:51:54 +1100 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=90=9B=20hide=20empty=20subsetting=20?= =?UTF-8?q?sections=20in=20download=20emails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ogcapi/server/core/util/EmailUtils.java | 233 +++++++++++++++++- .../ogcapi/server/processes/RestServices.java | 27 +- .../src/main/resources/job-started-email.html | 157 +----------- .../server/core/util/EmailUtilsTest.java | 42 ++++ 4 files changed, 292 insertions(+), 167 deletions(-) diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java index 96b61472..ab81835b 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/EmailUtils.java @@ -34,22 +34,78 @@ public static String readBase64Image(String filename) throws IOException { } /** - * Generate HTML content for bounding box section in email + * Generate the complete subsetting section HTML (header + bbox + time range) + * Returns empty string if no subsetting is applied + */ + public static String generateSubsettingSection( + String startDate, + String endDate, + Object multipolygon, + ObjectMapper objectMapper + ) { + try { + // Format dates + String displayStartDate = (startDate != null && !startDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) + ? startDate.replace("-", "/") : ""; + String displayEndDate = (endDate != null && !endDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) + ? endDate.replace("-", "/") : ""; + + // Check if dates are specified + boolean hasDateSubsetting = !displayStartDate.isEmpty() || !displayEndDate.isEmpty(); + + // Check if bbox is specified + boolean hasBboxSubsetting = multipolygon != null && !isEmptyMultiPolygon(multipolygon); + + // If no subsetting at all, return empty string + if (!hasDateSubsetting && !hasBboxSubsetting) { + return ""; + } + + StringBuilder html = new StringBuilder(); + + // Add subsetting header + html.append(buildSubsettingHeader()); + + // Add bbox section if present + if (hasBboxSubsetting) { + html.append(buildBboxWrapper(generateBboxHtml(multipolygon, objectMapper))); + } + + // Add spacing between bbox and time range if both exist + if (hasBboxSubsetting && hasDateSubsetting) { + html.append(buildSpacerSection()); + } + + // Add time range section if present + if (hasDateSubsetting) { + html.append(buildTimeRangeWrapper(displayStartDate, displayEndDate)); + } + + return html.toString(); + + } catch (Exception e) { + log.error("Error generating subsetting section", e); + return ""; + } + } + + /** + * Generate HTML content for bounding box data only (without wrapper) * @param multipolygon - the multipolygon object * @param objectMapper - Jackson ObjectMapper for JSON processing - * @return HTML string for bbox section + * @return HTML string for bbox data rows */ public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMapper) { try { if (multipolygon == null) { - return buildBboxSection("0", "0", "0", "0", 0); + return ""; } // Extract coordinates directly from the object List>>> coordinates = extractCoordinates(multipolygon, objectMapper); if (coordinates == null || coordinates.isEmpty()) { - return buildBboxSection("0", "0", "0", "0", 0); + return ""; } StringBuilder html = new StringBuilder(); @@ -106,7 +162,79 @@ public static String generateBboxHtml(Object multipolygon, ObjectMapper objectMa } catch (Exception e) { log.error("Error generating bbox HTML", e); - return buildBboxSection("0", "0", "0", "0", 0); + return ""; + } + } + + /** + * Check if multipolygon is empty or represents the full world + */ + private static boolean isEmptyMultiPolygon(Object multipolygon) { + try { + if (multipolygon instanceof Map) { + Map map = (Map) multipolygon; + Object coords = map.get("coordinates"); + if (coords instanceof List) { + List coordsList = (List) coords; + // Empty coordinates + if (coordsList.isEmpty()) { + return true; + } + // Full world bbox + if (isFullWorldBbox(coordsList)) { + return true; + } + } + } + return false; + } catch (Exception e) { + return true; + } + } + + /** + * Check if coordinates represent the full world (±180 longitude, ±90 latitude) + */ + private static boolean isFullWorldBbox(List coordinates) { + try { + // Check each polygon in the MultiPolygon + for (Object polygonObj : coordinates) { + if (!(polygonObj instanceof List)) continue; + List polygon = (List) polygonObj; + + // Check each ring in the polygon + for (Object ringObj : polygon) { + if (!(ringObj instanceof List)) continue; + List ring = (List) ringObj; + + // Count how many world boundary points we find + boolean hasMaxLon = false; // 180 or -180 + boolean hasMaxLat = false; // 90 + boolean hasMinLat = false; // -90 + + for (Object pointObj : ring) { + if (!(pointObj instanceof List)) continue; + List point = (List) pointObj; + + if (point.size() >= 2) { + double lon = ((Number) point.get(0)).doubleValue(); + double lat = ((Number) point.get(1)).doubleValue(); + + if (Math.abs(lon) == 180.0) hasMaxLon = true; + if (lat == 90.0) hasMaxLat = true; + if (lat == -90.0) hasMinLat = true; + } + } + + // If we found all world boundaries, it's the full world + if (hasMaxLon && hasMaxLat && hasMinLat) { + return true; + } + } + } + return false; + } catch (Exception e) { + return false; } } @@ -135,6 +263,101 @@ private static List>>> extractCoordinates(Object mult return null; } + /** + * Build the subsetting section header + */ + private static String buildSubsettingHeader() { + return "" + + "
" + + "
" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "
" + + "
" + + "
" + + "

Subsetting for this collection:

" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
"; + } + + /** + * Build bbox wrapper with table structure + */ + private static String buildBboxWrapper(String bboxContent) { + return "" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "" + bboxContent + "
" + + "
"; + } + + /** + * Build spacer section between bbox and time range + */ + private static String buildSpacerSection() { + return "" + + "
" + + "
" + + "" + + "
" + + "
" + + "" + + "" + + "
" + + "
"; + } + + /** + * Build time range wrapper with content + */ + private static String buildTimeRangeWrapper(String startDate, String endDate) { + return "" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "
" + + "
" + + "
" + + "
" + + "
" + + "

Time Range

" + + "" + + "" + + "
" + + "
" + + "
" + + "

"+startDate+" - "+endDate+"

" + + "
"; + } + protected static String buildBboxSection(String north, String south, String west, String east, int index) { String title = index > 0 ? "Bounding Box " + index : "Bounding Box Selection"; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java index 5f7d7948..e32aaf27 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/processes/RestServices.java @@ -119,7 +119,15 @@ private String submitJob(String jobName, String jobQueue, String jobDefinition, return submitJobResponse.jobId(); } - private String generateStartedEmailContent(String uuid, String startDate, String endDate, Object multipolygon, String collectionTitle, String fullMetadataLink, String suggestedCitation) { + private String generateStartedEmailContent( + String uuid, + String startDate, + String endDate, + Object multipolygon, + String collectionTitle, + String fullMetadataLink, + String suggestedCitation + ) { try (InputStream inputStream = getClass().getResourceAsStream("/job-started-email.html")) { if (inputStream == null) { @@ -129,22 +137,21 @@ private String generateStartedEmailContent(String uuid, String startDate, String String template = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - // Handle dates - only show if not "non-specified" - String displayStartDate = (startDate != null && !startDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) ? startDate.replace("-", "/") : ""; - String displayEndDate = (endDate != null && !endDate.equals(DatetimeUtils.NON_SPECIFIED_DATE)) ? endDate.replace("-", "/") : ""; - - // Generate dynamic bbox HTML - String bboxHtml = EmailUtils.generateBboxHtml(multipolygon, objectMapper); + // Generate subsetting section (returns empty string if no subsetting) + String subsettingSection = EmailUtils.generateSubsettingSection( + startDate, + endDate, + multipolygon, + objectMapper + ); // Replace all variables in one chain return template .replace("{{uuid}}", uuid) - .replace("{{startDate}}", displayStartDate) - .replace("{{endDate}}", displayEndDate) - .replace("{{bboxContent}}", bboxHtml) .replace("{{collectionTitle}}", collectionTitle != null ? collectionTitle : "") .replace("{{fullMetadataLink}}", fullMetadataLink != null ? fullMetadataLink : "") .replace("{{suggestedCitation}}", suggestedCitation != null ? suggestedCitation : "") + .replace("{{subsettingSection}}", subsettingSection) .replace("{{HEADER_IMG}}", EmailUtils.readBase64Image("header.txt")) .replace("{{DOWNLOAD_ICON}}", EmailUtils.readBase64Image("download.txt")) .replace("{{BBOX_IMG}}", EmailUtils.readBase64Image("bbox.txt")) diff --git a/server/src/main/resources/job-started-email.html b/server/src/main/resources/job-started-email.html index 3a85d644..755bf7e7 100644 --- a/server/src/main/resources/job-started-email.html +++ b/server/src/main/resources/job-started-email.html @@ -121,12 +121,12 @@