diff --git a/.gitignore b/.gitignore index d1c9a67..cd1ed3a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ replay_pid* /memex/.idea/ /SAMPLE_DATA/VOSA/ /memex/target/ +/memex/.gradle/ /DataGen/target/ diff --git a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/controller/VehicleInspectionController.java b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/controller/VehicleInspectionController.java index f51ec20..6153289 100644 --- a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/controller/VehicleInspectionController.java +++ b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/controller/VehicleInspectionController.java @@ -16,7 +16,9 @@ import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.Date; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Iterator; import java.util.List; import java.util.stream.Stream; @@ -251,8 +253,9 @@ public ResponseEntity streamJsonNative() { @GetMapping(value = "/inspections/asOf", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity dataAtDate( - @RequestParam(name = "asOfDate") @DateTimeFormat(pattern = "yyyyMMddHHmmss") Date asOfDate, + @RequestParam(name = "asOfDate") @DateTimeFormat(pattern = "yyyyMMddHHmmss") LocalDateTime asOfDateParam, @RequestParam(name = "id") Long id) { + Instant asOfDate = asOfDateParam.atZone(ZoneOffset.UTC).toInstant(); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body( diff --git a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java index 1d70dfc..63c4ed2 100644 --- a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java +++ b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java @@ -16,7 +16,7 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; -import java.util.Date; +import java.time.LocalDate; import java.util.HashMap; import java.util.Map; @@ -45,7 +45,7 @@ public class VehicleInspection { @Version Long lockVersion; - Date testdate; + LocalDate testdate; String testclass; String testtype; String testresult; @@ -58,7 +58,7 @@ public class VehicleInspection { Long capacity; - Date firstusedate; + LocalDate firstusedate; /* Use this to flag from the JSON we want to remove the record */ @Transient @DeleteFlag diff --git a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/service/VehicleInspectionHistoryService.java b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/service/VehicleInspectionHistoryService.java index 0d413cb..2051967 100644 --- a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/service/VehicleInspectionHistoryService.java +++ b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/service/VehicleInspectionHistoryService.java @@ -3,7 +3,7 @@ import com.johnlpage.memex.VehicleInspection.model.VehicleInspection; import com.johnlpage.memex.VehicleInspection.repository.VehicleInspectionRepository; -import java.util.Date; +import java.time.Instant; import java.util.stream.Stream; import org.springframework.stereotype.Service; @@ -19,7 +19,7 @@ public VehicleInspectionHistoryService(VehicleInspectionRepository repository) { this.repository = repository; } - public Stream asOfDate(Long id, Date asOfDate) { + public Stream asOfDate(Long id, Instant asOfDate) { return repository.GetRecordByIdAsOfDate(id, asOfDate, VehicleInspection.class); } } diff --git a/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepository.java b/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepository.java index 16cff7f..79ddf9b 100644 --- a/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepository.java +++ b/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepository.java @@ -1,13 +1,13 @@ package com.johnlpage.memex.generics.repository; -import java.util.Date; +import java.time.Instant; import java.util.stream.Stream; import org.springframework.data.mongodb.core.query.Criteria; public interface MongoHistoryRepository { - Stream GetRecordByIdAsOfDate(I recordId, Date asOf, Class clazz); + Stream GetRecordByIdAsOfDate(I recordId, Instant asOf, Class clazz); - Stream GetRecordsAsOfDate(Criteria criteria, Date asOf, Class clazz); + Stream GetRecordsAsOfDate(Criteria criteria, Instant asOf, Class clazz); } diff --git a/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepositoryImpl.java b/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepositoryImpl.java index 5b13adf..0c9a059 100644 --- a/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepositoryImpl.java +++ b/memex/src/main/java/com/johnlpage/memex/generics/repository/MongoHistoryRepositoryImpl.java @@ -13,6 +13,7 @@ import org.springframework.data.mongodb.core.aggregation.*; import org.springframework.data.mongodb.core.query.Criteria; +import java.time.Instant; import java.util.*; import java.util.stream.Stream; @@ -29,12 +30,12 @@ public MongoHistoryRepositoryImpl(MongoTemplate mongoTemplate, MongoVersionBean this.mongoVersion = mongoVersion; } - public Stream GetRecordByIdAsOfDate(I recordId, Date asOf, Class clazz) { + public Stream GetRecordByIdAsOfDate(I recordId, Instant asOf, Class clazz) { Criteria criteria = Criteria.where("_id").is(recordId); return GetRecordsAsOfDate(criteria, asOf, clazz); } - public Stream GetRecordsAsOfDate(Criteria criteria, Date asOf, Class clazz) { + public Stream GetRecordsAsOfDate(Criteria criteria, Instant asOf, Class clazz) { List stages = new ArrayList<>(); String collectionName = AnnotationExtractor.getCollectionName(clazz); diff --git a/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java b/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java index 3084467..c0b5015 100644 --- a/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java +++ b/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.*; -import java.util.Date; +import java.time.Instant; import java.util.Map; import lombok.EqualsAndHashCode; @@ -33,7 +33,7 @@ public class DocumentHistory { @EqualsAndHashCode.Include ObjectId historyId; Object recordId; - Date timestamp; + Instant timestamp; String type; // TOOO enum? Map changes; } diff --git a/memex/src/main/java/com/johnlpage/memex/generics/service/HistoryTriggerService.java b/memex/src/main/java/com/johnlpage/memex/generics/service/HistoryTriggerService.java index 2bc8b9c..86085fb 100644 --- a/memex/src/main/java/com/johnlpage/memex/generics/service/HistoryTriggerService.java +++ b/memex/src/main/java/com/johnlpage/memex/generics/service/HistoryTriggerService.java @@ -11,6 +11,7 @@ import com.mongodb.bulk.BulkWriteUpsert; import com.mongodb.client.ClientSession; +import java.time.Instant; import java.util.*; import org.bson.Document; @@ -60,7 +61,7 @@ public void postWriteTrigger( DocumentHistory vih = new DocumentHistory(); vih.setRecordId(v.getId()); vih.setType("insert"); - vih.setTimestamp(new Date()); + vih.setTimestamp(Instant.now()); history.add(vih); // Add this history records to the history list } } @@ -103,7 +104,7 @@ public void postWriteTrigger( previousValues.remove(OptimizedMongoLoadRepositoryImpl.LAST_UPDATE_DATE); vih.setChanges(previousValues); - vih.setTimestamp(new Date()); + vih.setTimestamp(Instant.now()); history.add(vih); // Add this history records to the history list } } @@ -123,7 +124,7 @@ public void postWriteTrigger( }); vih.setChanges(finalState); vih.setType("delete"); - vih.setTimestamp(new Date()); + vih.setTimestamp(Instant.now()); history.add(vih); // Add this history records to the history list } } diff --git a/memex/src/main/java/com/johnlpage/memex/util/MongoSchemaGenerator.java b/memex/src/main/java/com/johnlpage/memex/util/MongoSchemaGenerator.java index 2fb4353..5d8d580 100644 --- a/memex/src/main/java/com/johnlpage/memex/util/MongoSchemaGenerator.java +++ b/memex/src/main/java/com/johnlpage/memex/util/MongoSchemaGenerator.java @@ -143,7 +143,13 @@ private static String mapJavaToBson(Class type) { if (type == Boolean.class || type == boolean.class) return "bool"; if (Date.class.isAssignableFrom(type)) return "date"; if (type == java.time.Instant.class) - return "date"; // Works as logn as we have a typemapper + return "date"; // Works as long as we have a typemapper + if (type == java.time.LocalDate.class) + return "date"; // Stored as BSON Date via native java.time codecs + if (type == java.time.LocalTime.class) + return "date"; // Stored as BSON Date via native java.time codecs + if (type == java.time.LocalDateTime.class) + return "date"; // Stored as BSON Date via native java.time codecs if (UUID.class.isAssignableFrom(type)) return "string"; // Native BSON types (preferred for performance) @@ -208,7 +214,8 @@ private static String createErrorMessage(Class type) { if (pkg.startsWith("java.time")) { return "Unsupported type: " + type.getName() - + ". Convert to java.util.Date or store as ISO-8601 String or epoch Long."; + + ". Use Instant for timestamps, LocalDate for date-only values, LocalTime for time-only values," + + " LocalDateTime for combined date/time, or store as ISO-8601 String or epoch Long."; } if (pkg.startsWith("java.sql")) { diff --git a/memex/src/main/java/com/johnlpage/memex/util/ObjectConverter.java b/memex/src/main/java/com/johnlpage/memex/util/ObjectConverter.java index cbd81ec..9c9a217 100644 --- a/memex/src/main/java/com/johnlpage/memex/util/ObjectConverter.java +++ b/memex/src/main/java/com/johnlpage/memex/util/ObjectConverter.java @@ -2,10 +2,14 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class ObjectConverter { @@ -26,24 +30,18 @@ public static Object convertObject(Object input) { return ((Number) input).doubleValue(); } if (input instanceof String str) { - if (!str.isEmpty() && str.length() >= 8 && Character.isDigit(str.charAt(0))) { + if (str.length() >= 8 && Character.isDigit(str.charAt(0))) { for (DateTimeFormatter formatter : DATE_FORMATTERS) { try { // Try parsing as different date/time types if (str.contains("T")) { - if (str.contains("Z") || str.contains("+") || str.contains("-")) { - return Date.from(ZonedDateTime.parse(str, formatter).toInstant()); + if (str.endsWith("Z") || str.matches(".*[+-]\\d{2}(:\\d{2})?$")) { + return ZonedDateTime.parse(str, formatter).toInstant(); } else { - return Date.from( - LocalDateTime.parse(str, formatter) - .atZone(java.time.ZoneOffset.UTC) - .toInstant()); + return LocalDateTime.parse(str, formatter).toInstant(ZoneOffset.UTC); } } else { - return Date.from( - LocalDate.parse(str, formatter) - .atStartOfDay(java.time.ZoneOffset.UTC) - .toInstant()); + return LocalDate.parse(str, formatter); } } catch (DateTimeParseException e) { // Continue to next formatter diff --git a/memex/src/test/java/com/johnlpage/memex/cucumber/steps/TimeManagementSteps.java b/memex/src/test/java/com/johnlpage/memex/cucumber/steps/TimeManagementSteps.java index a70bb84..b5e88f3 100644 --- a/memex/src/test/java/com/johnlpage/memex/cucumber/steps/TimeManagementSteps.java +++ b/memex/src/test/java/com/johnlpage/memex/cucumber/steps/TimeManagementSteps.java @@ -4,6 +4,7 @@ import io.cucumber.java.en.Given; import org.springframework.beans.factory.annotation.Autowired; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.TimeUnit; @@ -15,7 +16,7 @@ public class TimeManagementSteps { @Given("I capture the current timestamp to {string} with {string} pattern") public void iCaptureTheCurrentTimestamp(String macroName, String datePattern) { - ZonedDateTime capturedTimestamp = ZonedDateTime.now(); + ZonedDateTime capturedTimestamp = ZonedDateTime.now(ZoneOffset.UTC); macroRegister.registerMacro(macroName, capturedTimestamp.format(DateTimeFormatter.ofPattern(datePattern))); } diff --git a/memex/src/test/resources/features/inspections.rest.asof.feature b/memex/src/test/resources/features/inspections.rest.asof.feature index 83a871b..ec5a856 100644 --- a/memex/src/test/resources/features/inspections.rest.asof.feature +++ b/memex/src/test/resources/features/inspections.rest.asof.feature @@ -16,7 +16,7 @@ Feature: Vehicle Inspection REST API - Point-in-Time History (As Of) [ { "testid": 10001, - "testdate": "2025-10-27T11:00:00Z", + "testdate": "2025-10-27", "testclass": "Class 2", "testtype": "Interim", "testresult": "PASS", @@ -24,7 +24,7 @@ Feature: Vehicle Inspection REST API - Point-in-Time History (As Of) "postcode": "SW1A 0AB", "fuel": "Diesel", "capacity": 90, - "firstusedate": "2019-03-20T00:00:00Z", + "firstusedate": "2019-03-20", "faileditems": ["Brakes", "Lights"], "vehicle": { "make": "Ford", diff --git a/memex/src/test/resources/features/inspections.rest.streams.feature b/memex/src/test/resources/features/inspections.rest.streams.feature index aa84979..8e96b78 100644 --- a/memex/src/test/resources/features/inspections.rest.streams.feature +++ b/memex/src/test/resources/features/inspections.rest.streams.feature @@ -11,7 +11,7 @@ Feature: Vehicle Inspection REST API - Data Streaming Capabilities [ { "testid": , - "testdate": "2023-10-26T10:00:00Z", + "testdate": "2023-10-26", "testclass": "Class 1", "testtype": "Annual", "testresult": "PASS", @@ -19,7 +19,7 @@ Feature: Vehicle Inspection REST API - Data Streaming Capabilities "postcode": "SW1A 0AA", "fuel": "Petrol", "capacity": 56, - "firstusedate": "2018-01-15T00:00:00Z", + "firstusedate": "2018-01-15", "faileditems": [], "vehicle": { "make": "Toyota", @@ -30,7 +30,7 @@ Feature: Vehicle Inspection REST API - Data Streaming Capabilities }, { "testid": , - "testdate": "2023-10-27T11:00:00Z", + "testdate": "2023-10-27", "testclass": "Class 2", "testtype": "Interim", "testresult": "FAIL", @@ -38,7 +38,7 @@ Feature: Vehicle Inspection REST API - Data Streaming Capabilities "postcode": "SW1A 0AB", "fuel": "Diesel", "capacity": 76, - "firstusedate": "2019-03-20T00:00:00Z", + "firstusedate": "2019-03-20", "faileditems": ["Brakes", "Lights"], "vehicle": { "make": "Ford", @@ -88,11 +88,11 @@ Feature: Vehicle Inspection REST API - Data Streaming Capabilities [ { "testid": 10001, - "testdate": "2023-10-26T10:00:00Z" + "testdate": "2023-10-26" }, { // Malformed object, missing closing brace "testid": 10002, - "testdate": "2023-10-27T11:00:00Z" + "testdate": "2023-10-27" ] """ Then the response status code should be 207 diff --git a/memex/templates/controller/Controller.java.template b/memex/templates/controller/Controller.java.template index 1263168..1145f72 100644 --- a/memex/templates/controller/Controller.java.template +++ b/memex/templates/controller/Controller.java.template @@ -13,7 +13,9 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.Date; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Iterator; import java.util.List; import java.util.stream.Stream; @@ -209,8 +211,9 @@ public class __className__Controller { */ @GetMapping(value = "/__apiPath__/asOf", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity dataAtDate( - @RequestParam(name = "asOfDate") @DateTimeFormat(pattern = "yyyyMMddHHmmss") Date asOfDate, + @RequestParam(name = "asOfDate") @DateTimeFormat(pattern = "yyyyMMddHHmmss") LocalDateTime asOfDateParam, @RequestParam(name = "id") __idType__ id) { + Instant asOfDate = asOfDateParam.atZone(ZoneOffset.UTC).toInstant(); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body( diff --git a/memex/templates/scripts/generate-models-from-json.groovy b/memex/templates/scripts/generate-models-from-json.groovy index 0ae83fd..ef5cb4b 100644 --- a/memex/templates/scripts/generate-models-from-json.groovy +++ b/memex/templates/scripts/generate-models-from-json.groovy @@ -384,8 +384,11 @@ determineType = { String fieldName, Object value, String parentClassName -> if (value instanceof String) { String strVal = (String) value - if (strVal ==~ /^\d{4}-\d{2}-\d{2}.*/ || strVal ==~ /.*T\d{2}:\d{2}:\d{2}.*/) { - return [type: 'Date', isComplex: false, imports: ['java.util.Date']] + if (strVal ==~ /.*T\d{2}:\d{2}:\d{2}.*/) { + return [type: 'Instant', isComplex: false, imports: ['java.time.Instant']] + } + if (strVal ==~ /^\d{4}-\d{2}-\d{2}$/) { + return [type: 'LocalDate', isComplex: false, imports: ['java.time.LocalDate']] } return [type: 'String', isComplex: false] } diff --git a/memex/templates/service/HistoryService.java.template b/memex/templates/service/HistoryService.java.template index ec59572..b42307b 100644 --- a/memex/templates/service/HistoryService.java.template +++ b/memex/templates/service/HistoryService.java.template @@ -2,7 +2,7 @@ package __package__.__className__.service; import __package__.__className__.model.__className__; import __package__.__className__.repository.__className__Repository; -import java.util.Date; +import java.time.Instant; import java.util.stream.Stream; import org.springframework.stereotype.Service; __idImport__ @@ -18,7 +18,7 @@ public class __className__HistoryService { this.repository = repository; } - public Stream<__className__> asOfDate(__idType__ id, Date asOfDate) { + public Stream<__className__> asOfDate(__idType__ id, Instant asOfDate) { return repository.GetRecordByIdAsOfDate(id, asOfDate, __className__.class); } }