From dd9c33c5fd99ef0104b398c244af4f01ef33fb8c Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Fri, 22 May 2026 15:45:43 +0200 Subject: [PATCH] Commit remaining tracked non-log changes --- pom.xml | 4 + ...eventhub-esper-poc.postman_collection.json | 127 ++++++++++++++++++ .../procon/eventhub/config/JacksonConfig.java | 8 +- .../EventHubEventReadRepository.java | 22 ++- ...TachographFileSessionExceptionHandler.java | 11 +- src/main/resources/application.yml | 1 + src/main/resources/reference/nation.csv | 120 ++++++++--------- ...nifiedRuntimeProcessingControllerTest.java | 42 +++++- 8 files changed, 266 insertions(+), 69 deletions(-) diff --git a/pom.xml b/pom.xml index 19ce112..39447fa 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,10 @@ org.apache.camel.springboot camel-jackson-starter + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + org.flywaydb diff --git a/postman/eventhub-esper-poc.postman_collection.json b/postman/eventhub-esper-poc.postman_collection.json index 784e338..ee30532 100644 --- a/postman/eventhub-esper-poc.postman_collection.json +++ b/postman/eventhub-esper-poc.postman_collection.json @@ -631,6 +631,121 @@ } } ] + }, + { + "name": "Tachograph composite sessions", + "item": [ + { + "name": "Create tachograph composite session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{additionalSessionId}}\"\n ],\n \"label\": \"{{compositeSessionLabel}}\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/tachograph-composite-sessions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "tachograph-composite-sessions" + ] + } + } + }, + { + "name": "Get tachograph composite session", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/eventhub/tachograph-composite-sessions/{{compositeSessionId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "tachograph-composite-sessions", + "{{compositeSessionId}}" + ] + } + } + }, + { + "name": "List tachograph composite session drivers", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/eventhub/tachograph-composite-sessions/{{compositeSessionId}}/drivers", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "tachograph-composite-sessions", + "{{compositeSessionId}}", + "drivers" + ] + } + } + }, + { + "name": "Load tachograph composite driver events", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/eventhub/tachograph-composite-sessions/{{compositeSessionId}}/drivers/{{driverKey}}/events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "tachograph-composite-sessions", + "{{compositeSessionId}}", + "drivers", + "{{driverKey}}", + "events" + ] + } + } + }, + { + "name": "Load tachograph composite driver timeline", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/eventhub/tachograph-composite-sessions/{{compositeSessionId}}/drivers/{{driverKey}}/timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "tachograph-composite-sessions", + "{{compositeSessionId}}", + "drivers", + "{{driverKey}}", + "timeline" + ] + } + } + } + ] } ], "variable": [ @@ -670,6 +785,18 @@ "key": "sessionId", "value": "00000000-0000-0000-0000-000000000000" }, + { + "key": "additionalSessionId", + "value": "00000000-0000-0000-0000-000000000001" + }, + { + "key": "compositeSessionId", + "value": "00000000-0000-0000-0000-000000000100" + }, + { + "key": "compositeSessionLabel", + "value": "driver-multi-file sample" + }, { "key": "driverKey", "value": "12:12345678901200" diff --git a/src/main/java/at/procon/eventhub/config/JacksonConfig.java b/src/main/java/at/procon/eventhub/config/JacksonConfig.java index ce6a3ad..6f08fec 100644 --- a/src/main/java/at/procon/eventhub/config/JacksonConfig.java +++ b/src/main/java/at/procon/eventhub/config/JacksonConfig.java @@ -1,6 +1,8 @@ package at.procon.eventhub.config; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -11,7 +13,9 @@ public class JacksonConfig { @Bean @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper objectMapper() { - return new ObjectMapper().findAndRegisterModules(); + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .findAndRegisterModules() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } } - diff --git a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java index c4595fc..7fee1b5 100644 --- a/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java +++ b/src/main/java/at/procon/eventhub/persistence/EventHubEventReadRepository.java @@ -442,16 +442,34 @@ public class EventHubEventReadRepository { return Integer.parseInt(value.toString()); } - private JsonNode json(String value) { + static JsonNode parseJsonColumn(ObjectMapper objectMapper, String value) { try { - return value == null || value.isBlank() + JsonNode node = value == null || value.isBlank() ? objectMapper.createObjectNode() : objectMapper.readTree(value); + while (node != null + && node.isTextual() + && looksLikeEmbeddedJson(node.textValue())) { + node = objectMapper.readTree(node.textValue()); + } + return node; } catch (JsonProcessingException e) { throw new IllegalArgumentException("Failed to parse JSON column.", e); } } + private JsonNode json(String value) { + return parseJsonColumn(objectMapper, value); + } + + private static boolean looksLikeEmbeddedJson(String value) { + if (value == null) { + return false; + } + String trimmed = value.trim(); + return trimmed.startsWith("{") || trimmed.startsWith("["); + } + private String syntheticId(String prefix, UUID id) { return id == null ? null : prefix + ":" + id; } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionExceptionHandler.java b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionExceptionHandler.java index 34e6962..abe140c 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionExceptionHandler.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionExceptionHandler.java @@ -1,8 +1,10 @@ package at.procon.eventhub.tachographfilesession.api; import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException; +import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInCompositeSessionException; import at.procon.eventhub.tachographfilesession.service.LegalRequirementsUploadException; import at.procon.eventhub.tachographfilesession.service.LegalRequirementsXmlDownloadException; +import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException; import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException; import at.procon.eventhub.tachographfilesession.service.TachographXmlValidationException; import at.procon.eventhub.tachographfilesession.service.UnsupportedTachographFileTypeException; @@ -13,12 +15,17 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -@RestControllerAdvice(basePackageClasses = TachographFileSessionController.class) +@RestControllerAdvice(basePackageClasses = { + TachographFileSessionController.class, + TachographCompositeSessionController.class +}) public class TachographFileSessionExceptionHandler { @ExceptionHandler({ TachographFileSessionNotFoundException.class, - DriverNotFoundInSessionException.class + TachographCompositeSessionNotFoundException.class, + DriverNotFoundInSessionException.class, + DriverNotFoundInCompositeSessionException.class }) public ResponseEntity> notFound(RuntimeException exception) { return error(HttpStatus.NOT_FOUND, exception); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 04f9d27..da6cf33 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -130,6 +130,7 @@ eventhub: significant-driving-minutes: 3 merge-gap-seconds: 0 gap-detection-tolerance-seconds: 0 + timeline-input-mode: events legal-requirements: base-url: ${LEGAL_REQUIREMENTS_BASE_URL:https://legalrequirements.services.bytebar.eu/ODataV4/LR} username: ${LEGAL_REQUIREMENTS_USERNAME:} diff --git a/src/main/resources/reference/nation.csv b/src/main/resources/reference/nation.csv index cb10f4b..af8d040 100644 --- a/src/main/resources/reference/nation.csv +++ b/src/main/resources/reference/nation.csv @@ -1,60 +1,60 @@ -ID;Name;AlphaCode;NumericCode;DefaultLanguageCode;ID_Certificate;ID_FileLog -0;Unknown 0;0;0;NULL;NULL;NULL -1;Austria;A;1;de-AT;NULL;NULL -2;Albania;AL;2;sq-AL;NULL;NULL -3;Andorra;AND;3;ca;NULL;NULL -4;Armenia;ARM;4;hy-AM;NULL;NULL -5;Azerbaijan;AZ;5;az;NULL;NULL -6;Belgium;B;6;NULL;NULL;NULL -7;Bulgaria;BG;7;bg-BG;NULL;NULL -8;Bosnia Herzegovina;BIH;8;NULL;NULL;NULL -9;Belarus;BY;9;be-BY;NULL;NULL -10;Switzerland;CH;10;de-CH;NULL;NULL -11;Cyprus;CY;11;NULL;NULL;NULL -12;Czech Republic;CZ;12;cs-CZ;NULL;NULL -13;Germany;D;13;de-DE;NULL;NULL -14;Denmark;DK;14;da-DK;NULL;NULL -15;Spain;E;15;es-ES;NULL;NULL -16;Estonia;EST;16;et-EE;NULL;NULL -17;France;F;17;fr-FR;NULL;NULL -18;Finland;FIN;18;fi-FI;NULL;NULL -19;Liechtenstein;FL;19;de-LI;NULL;NULL -20;Faroe Islands;FR;20;fo-FO;NULL;NULL -21;United Kingdom;UK;21;en-GB;NULL;NULL -22;Georgia;GE;22;ka-GE;NULL;NULL -23;Greece;GR;23;el-GR;NULL;NULL -24;Hungary;H;24;hu-HU;NULL;NULL -25;Croatia;HR;25;hr-HR;NULL;NULL -26;Italy;I;26;it-IT;NULL;NULL -27;Ireland;IRL;27;en-IE;NULL;NULL -28;Iceland;IS;28;is-IS;NULL;NULL -29;Kazakhstan;KZ;29;kk-KZ;NULL;NULL -30;Luxembourg;L;30;de-LU;NULL;NULL -31;Lithuania;LT;31;lt-LT;NULL;NULL -32;Latvia;LV;32;lv-LV;NULL;NULL -33;Malta;M;33;NULL;NULL;NULL -34;Monaco;MC;34;fr-MC;NULL;NULL -35;Moldova;MD;35;ro;NULL;NULL -36;North Macedonia;MK;36;mk-MK;NULL;NULL -37;Norway;N;37;no;NULL;NULL -38;Netherlands;NL;38;nl-NL;NULL;NULL -39;Portugal;P;39;pt-PT;NULL;NULL -40;Poland;PL;40;pl-PL;NULL;NULL -41;Romania;RO;41;ro-RO;NULL;NULL -42;San Marino;RSM;42;it-IT;NULL;NULL -43;Russia;RUS;43;ru-RU;NULL;NULL -44;Sweden;S;44;sv-SE;NULL;NULL -45;Slovakia;SK;45;sk-SK;NULL;NULL -46;Slovenia;SLO;46;sl-SI;NULL;NULL -47;Turkmenistan;TM;47;NULL;NULL;NULL -48;Turkey;TR;48;tr-TR;NULL;NULL -49;Ukraine;UA;49;uk-UA;NULL;NULL -50;Vatican City;V;50;it-IT;NULL;NULL -51;Yugoslavia;YU;51;NULL;NULL;NULL -52;Montenegro;MNE;52;NULL;NULL;763087 -53;Serbia;SRB;53;NULL;NULL;272576 -54;Uzbekistan;UZ;54;NULL;NULL;308001 -55;Tajikistan;TJ;55;NULL;NULL;NULL -253;European Community;EC;253;NULL;NULL;NULL -254;Rest of Europe;EUR;254;NULL;NULL;NULL -255;Rest of the world;WLD;255;NULL;NULL;NULL +ID;Name;AlphaCode;NumericCode +0;Unknown 0;0;0 +1;Austria;A;1 +2;Albania;AL;2 +3;Andorra;AND;3 +4;Armenia;ARM;4 +5;Azerbaijan;AZ;5 +6;Belgium;B;6 +7;Bulgaria;BG;7 +8;Bosnia Herzegovina;BIH;8 +9;Belarus;BY;9 +10;Switzerland;CH;10 +11;Cyprus;CY;11 +12;Czech Republic;CZ;12 +13;Germany;D;13 +14;Denmark;DK;14 +15;Spain;E;15 +16;Estonia;EST;16 +17;France;F;17 +18;Finland;FIN;18 +19;Liechtenstein;FL;19 +20;Faroe Islands;FR;20 +21;United Kingdom;UK;21 +22;Georgia;GE;22 +23;Greece;GR;23 +24;Hungary;H;24 +25;Croatia;HR;25 +26;Italy;I;26 +27;Ireland;IRL;27 +28;Iceland;IS;28 +29;Kazakhstan;KZ;29 +30;Luxembourg;L;30 +31;Lithuania;LT;31 +32;Latvia;LV;32 +33;Malta;M;33 +34;Monaco;MC;34 +35;Moldova;MD;35 +36;North Macedonia;MK;36 +37;Norway;N;37 +38;Netherlands;NL;38 +39;Portugal;P;39 +40;Poland;PL;40 +41;Romania;RO;41 +42;San Marino;RSM;42 +43;Russia;RUS;43 +44;Sweden;S;44 +45;Slovakia;SK;45 +46;Slovenia;SLO;46 +47;Turkmenistan;TM;47 +48;Turkey;TR;48 +49;Ukraine;UA;49 +50;Vatican City;V;50 +51;Yugoslavia;YU;51 +52;Montenegro;MNE;52 +53;Serbia;SRB;53 +54;Uzbekistan;UZ;54 +55;Tajikistan;TJ;55 +253;European Community;EC;253 +254;Rest of Europe;EUR;254 +255;Rest of the world;WLD;255 diff --git a/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java b/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java index 62799ec..2d777f7 100644 --- a/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java +++ b/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java @@ -6,6 +6,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import at.procon.eventhub.dto.EventDetailsDto; import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.dto.EventDomain; import at.procon.eventhub.dto.EventHubEventDto; @@ -23,21 +24,32 @@ import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService; import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; import java.time.OffsetDateTime; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.Test; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; class UnifiedRuntimeProcessingControllerTest { + private final ObjectMapper objectMapper = testObjectMapper(); + @Test void loadsDriverEventsViaRuntimeApi() throws Exception { UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService)) + .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) .build(); @@ -77,7 +89,9 @@ class UnifiedRuntimeProcessingControllerTest { .andExpect(jsonPath("$.request.sessionId").value(sessionId.toString())) .andExpect(jsonPath("$.request.driverKey").value("12:123")) .andExpect(jsonPath("$.discoveredVehicles[0].vin").value("VIN-1")) - .andExpect(jsonPath("$.mergedEvents[0].externalSourceEventId").value("EV-1")); + .andExpect(jsonPath("$.mergedEvents[0].externalSourceEventId").value("EV-1")) + .andExpect(jsonPath("$.mergedEvents[0].payload.raw.driverKey").value("12:123")) + .andExpect(jsonPath("$.mergedEvents[0].eventDetails.attributes.cardSlot").value("DRIVER")); } @Test @@ -85,6 +99,7 @@ class UnifiedRuntimeProcessingControllerTest { UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService)) + .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) .build(); @@ -152,6 +167,7 @@ class UnifiedRuntimeProcessingControllerTest { UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class); UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService)) + .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) .build(); @@ -182,11 +198,31 @@ class UnifiedRuntimeProcessingControllerTest { EventLifecycle.START, null, null, + new EventDetailsDto("DRIVER_ACTIVITY", objectMapper.valueToTree(Map.of("cardSlot", "DRIVER"))), null, - null, - null, + objectMapper.valueToTree(Map.of("raw", Map.of("driverKey", "12:123", "intervalId", "ACT-1"))), false, null ); } + + private ObjectMapper testObjectMapper() { + SimpleModule module = new SimpleModule(); + module.addSerializer(OffsetDateTime.class, new StdScalarSerializer<>(OffsetDateTime.class) { + @Override + public void serialize(OffsetDateTime value, JsonGenerator gen, com.fasterxml.jackson.databind.SerializerProvider provider) + throws java.io.IOException { + gen.writeString(value == null ? null : value.toString()); + } + }); + module.addDeserializer(OffsetDateTime.class, new StdScalarDeserializer<>(OffsetDateTime.class) { + @Override + public OffsetDateTime deserialize(JsonParser p, com.fasterxml.jackson.databind.DeserializationContext ctxt) + throws java.io.IOException { + String value = p.getValueAsString(); + return value == null || value.isBlank() ? null : OffsetDateTime.parse(value); + } + }); + return new ObjectMapper().registerModule(module); + } }