Add runtime event scope processing
This commit is contained in:
parent
3bccda20e8
commit
b04b333db7
|
|
@ -0,0 +1,103 @@
|
|||
# Runtime-derived tachograph projections
|
||||
|
||||
Runtime Processing now exposes the tachograph driving-derived Esper bundle through the unified runtime event assembly layer.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/driver-derived-projections
|
||||
```
|
||||
|
||||
The request body uses the same selector fields as the existing runtime endpoints:
|
||||
|
||||
- `sessionId` for one uploaded tachograph file session
|
||||
- `sessionIds` for multiple uploaded tachograph file sessions
|
||||
- `compositeSessionId` for an existing tachograph composite session
|
||||
- `tenantKey` + driver selector for tachograph DB / YellowFox DB runtime sources
|
||||
- `eventBackend` with `SOURCE_DB` or `EVENTHUB_DB` where supported
|
||||
- `sourceFamilies`, for example `TACHOGRAPH_FILE_SESSION`, `TACHOGRAPH_DB`, `YELLOWFOX_DB`
|
||||
- `driverKey`, `driverSourceEntityId`, `driverCardNation`, `driverCardNumber`
|
||||
- `occurredFrom`, `occurredTo`
|
||||
- `expandVehicleEvents`
|
||||
- `vehicleExpansionPaddingMinutes`
|
||||
|
||||
Additional Esper thresholds are optional:
|
||||
|
||||
- `significantDrivingMinutes`
|
||||
- `minimumRestPeriodMinutes`
|
||||
|
||||
When omitted, the defaults are read from:
|
||||
|
||||
```yaml
|
||||
eventhub:
|
||||
tachograph-file-session:
|
||||
processing:
|
||||
significant-driving-minutes: 3
|
||||
minimum-rest-period-minutes: 720
|
||||
```
|
||||
|
||||
## Flow
|
||||
|
||||
```text
|
||||
runtime request
|
||||
-> UnifiedRuntimeEventAssemblyService
|
||||
-> driver seed events
|
||||
-> discovered vehicles
|
||||
-> optional vehicle-expanded events
|
||||
-> merged runtime event stream
|
||||
-> UnifiedEventTimelineReconstructor
|
||||
-> DriverTimelineReusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(...)
|
||||
-> TachographEsperDriverProcessingResultDto
|
||||
```
|
||||
|
||||
The derived part always uses the event-input Esper path. This means the runtime stream is passed as point events to Esper, where activity and card-vehicle-usage intervals are paired and vehicle-usage intervals are merged before the existing driving-derived rules run.
|
||||
|
||||
## Example: composite tachograph file session
|
||||
|
||||
```json
|
||||
{
|
||||
"compositeSessionId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"driverKey": "12:12345678901234",
|
||||
"occurredFrom": "2026-05-01T00:00:00Z",
|
||||
"occurredTo": "2026-05-31T23:59:59Z",
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15,
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
The response contains runtime assembly metadata and the tachograph Esper result:
|
||||
|
||||
```text
|
||||
request
|
||||
driverSeedEventCount
|
||||
discoveredVehicleCount
|
||||
expandedVehicleEventCount
|
||||
mergedEventCount
|
||||
discoveredVehicles
|
||||
projection
|
||||
notes
|
||||
```
|
||||
|
||||
`projection` is the same high-level structure returned by the tachograph file-session Esper endpoint, including:
|
||||
|
||||
- activity interval count/list
|
||||
- driving interval count/list
|
||||
- driving interruption intervals
|
||||
- daily/weekly rest candidate intervals
|
||||
- daily/weekly rest candidate coverage intervals
|
||||
- unclassified rest candidate coverage intervals
|
||||
- potential home overnight stays
|
||||
- potential in-vehicle overnight stays
|
||||
- potential in-vehicle trips
|
||||
- vehicle usage intervals
|
||||
- VU card absent intervals
|
||||
- support geo events
|
||||
|
||||
## Boundary note
|
||||
|
||||
Runtime processing works with point events. If `occurredFrom`/`occurredTo` cuts through a source interval, the matching START/END or INSERT/WITHDRAW point may be outside the selected window. For evaluations near boundaries, request a wider event window or use vehicle expansion padding so Esper receives enough point events to reconstruct the interval.
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
# Runtime Event Processing
|
||||
|
||||
Runtime Processing is now source-neutral. The API receives a runtime scope, selects a processing profile, partitions the normalized EventHub-style events, and delegates execution to the selected profile.
|
||||
|
||||
The tachograph Esper processing is no longer the root concept. It is one profile:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
## Profile discovery endpoint
|
||||
|
||||
```http
|
||||
GET /api/eventhub/runtime-processing/event-processing/profiles
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"displayName": "Tachograph Driver Esper Processing",
|
||||
"description": "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes.",
|
||||
"defaultPartitioningStrategy": "DRIVER",
|
||||
"supportedPartitioningStrategies": ["DRIVER"],
|
||||
"requiredParameters": [],
|
||||
"optionalParameters": [
|
||||
"significantDrivingMinutes",
|
||||
"minimumRestPeriodMinutes",
|
||||
"attachVehicleOnlyEvents",
|
||||
"vehicleEvidencePaddingMinutes"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Clients should prefer this endpoint instead of hardcoding profile metadata.
|
||||
|
||||
## Generic execution endpoint
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
## Request shape
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"scope": {
|
||||
"sessionIds": [
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
"22222222-2222-2222-2222-222222222222"
|
||||
],
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"occurredFrom": "2026-05-01T00:00:00Z",
|
||||
"occurredTo": "2026-05-31T23:59:59Z",
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15
|
||||
},
|
||||
"partitioning": {
|
||||
"strategy": "DRIVER",
|
||||
"includeAllPartitions": true,
|
||||
"attachVehicleEvidence": true,
|
||||
"vehicleEvidencePaddingMinutes": 15
|
||||
},
|
||||
"parameters": {
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720,
|
||||
"attachVehicleOnlyEvents": true,
|
||||
"vehicleEvidencePaddingMinutes": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"partitioningStrategy": "DRIVER",
|
||||
"inputEventCount": 1234,
|
||||
"selectedPartitionCount": 2,
|
||||
"discoveredVehicleCount": 3,
|
||||
"partitionResults": {
|
||||
"12:12345678901234": {
|
||||
"partitionType": "DRIVER",
|
||||
"partitionKey": "12:12345678901234",
|
||||
"resultType": "UnifiedRuntimeDerivedProjectionResultDto",
|
||||
"result": {
|
||||
"projection": {
|
||||
"activityIntervals": [],
|
||||
"drivingIntervals": [],
|
||||
"drivingInterruptionIntervals": [],
|
||||
"dailyWeeklyRestCandidateIntervals": [],
|
||||
"dailyWeeklyRestCandidateCoverageIntervals": [],
|
||||
"unclassifiedDailyWeeklyRestCandidateCoverageIntervals": [],
|
||||
"potentialHomeOvernightStayIntervals": [],
|
||||
"potentialInVehicleOvernightStayIntervals": [],
|
||||
"potentialInVehicleTripIntervals": [],
|
||||
"vehicleUsageIntervals": [],
|
||||
"vuCardAbsentIntervals": [],
|
||||
"supportGeoEvents": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": [],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Scope
|
||||
|
||||
`scope` is the existing runtime selection model. It can select events from:
|
||||
|
||||
```text
|
||||
TACHOGRAPH_FILE_SESSION
|
||||
TACHOGRAPH_DB
|
||||
YELLOWFOX_DB
|
||||
```
|
||||
|
||||
and can use:
|
||||
|
||||
```text
|
||||
SOURCE_DB
|
||||
EVENTHUB_DB
|
||||
```
|
||||
|
||||
where supported.
|
||||
|
||||
For uploaded tachograph files, the scope can use:
|
||||
|
||||
```text
|
||||
sessionId
|
||||
sessionIds
|
||||
compositeSessionId
|
||||
```
|
||||
|
||||
### Profile
|
||||
|
||||
A profile owns domain-specific processing semantics. It defines:
|
||||
|
||||
```text
|
||||
profile key
|
||||
expected partitioning strategy
|
||||
profile-specific parameters
|
||||
result type
|
||||
```
|
||||
|
||||
Current profile:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
Future profiles can include:
|
||||
|
||||
```text
|
||||
vehicle-stop-detection-v1
|
||||
vehicle-trip-detection-v1
|
||||
telematics-poi-clustering-v1
|
||||
driver-settlement-v1
|
||||
mixed-driver-vehicle-correlation-v1
|
||||
```
|
||||
|
||||
### Partitioning
|
||||
|
||||
The common API supports generic partitioning options:
|
||||
|
||||
```text
|
||||
NONE
|
||||
DRIVER
|
||||
VEHICLE
|
||||
DRIVER_VEHICLE
|
||||
SOURCE_FAMILY
|
||||
CUSTOM_PROFILE
|
||||
```
|
||||
|
||||
The first tachograph profile currently supports `DRIVER` partitioning. The service partitions mixed event scopes in Java before invoking Esper so that existing single-driver EPL windows cannot mix driver states.
|
||||
|
||||
## Compatibility endpoint
|
||||
|
||||
The old tachograph endpoint remains available:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/tachograph/esper-processing
|
||||
```
|
||||
|
||||
It now acts as a compatibility adapter for:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```text
|
||||
profileKey = tachograph-driver-esper-v1
|
||||
partitioning.strategy = DRIVER
|
||||
```
|
||||
|
||||
## Tachograph profile processing flow
|
||||
|
||||
```text
|
||||
runtime event scope
|
||||
-> broad event assembly
|
||||
-> driver partition discovery
|
||||
-> for each driver:
|
||||
direct driver events
|
||||
reconstruct driver vehicle-usage intervals
|
||||
attach only vehicle-scoped events whose vehicle matches and whose timestamp falls inside a usage interval plus configured padding
|
||||
shared event-input Esper projection pipeline
|
||||
-> generic partitionResults map
|
||||
```
|
||||
|
||||
The tachograph profile reuses the same `TachographEsperProcessingCore` used by the file-session endpoint. This prevents the file-session API and runtime-processing API from drifting into separate rule chains.
|
||||
## Vehicle-only evidence attachment
|
||||
|
||||
For driver-partitioned profiles, vehicle-only events are no longer attached only by vehicle identity. They are attached to a driver partition only when there is temporal evidence:
|
||||
|
||||
```text
|
||||
vehicle-only event.vehicleKey/registrationKey matches a reconstructed driver vehicle-usage interval
|
||||
and event.occurredAt is inside [usage.from - padding, usage.to + padding]
|
||||
```
|
||||
|
||||
This prevents unrelated vehicle events from being copied into a driver result simply because the driver used the same vehicle on another day. The tachograph profile currently uses:
|
||||
|
||||
```json
|
||||
{
|
||||
"partitioning": {
|
||||
"attachVehicleEvidence": true,
|
||||
"vehicleEvidencePaddingMinutes": 15
|
||||
},
|
||||
"parameters": {
|
||||
"attachVehicleOnlyEvents": true,
|
||||
"vehicleEvidencePaddingMinutes": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`parameters` take precedence in the tachograph profile. The compatibility endpoint maps these values to `expandVehicleEvents` and `vehicleExpansionPaddingMinutes`.
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Runtime tachograph Esper scope processing
|
||||
|
||||
This document is kept for compatibility with earlier patches.
|
||||
|
||||
The preferred endpoint is now the generic Runtime Event Processing endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
Use the tachograph profile key:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
The old endpoint remains available as an adapter:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/tachograph/esper-processing
|
||||
```
|
||||
|
||||
It delegates to the same profile infrastructure used by the generic endpoint. See:
|
||||
|
||||
```text
|
||||
docs/runtime-event-processing.md
|
||||
```
|
||||
|
||||
Vehicle-only evidence is now attached through the same common runtime event-processing profile. The old compatibility endpoint maps:
|
||||
|
||||
```json
|
||||
{
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15
|
||||
}
|
||||
```
|
||||
|
||||
to the generic profile behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"parameters": {
|
||||
"attachVehicleOnlyEvents": true,
|
||||
"vehicleEvidencePaddingMinutes": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Attachment is temporal: a vehicle-only event must match a reconstructed driver vehicle-usage interval and occur inside the interval plus configured padding.
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"info": {
|
||||
"name": "EventHub Runtime Event Processing",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": "Examples for source-neutral Runtime Event Processing and the tachograph-driver-esper-v1 profile."
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://localhost:8085"
|
||||
},
|
||||
{
|
||||
"key": "sessionId",
|
||||
"value": "11111111-1111-1111-1111-111111111111"
|
||||
},
|
||||
{
|
||||
"key": "sessionId2",
|
||||
"value": "22222222-2222-2222-2222-222222222222"
|
||||
},
|
||||
{
|
||||
"key": "compositeSessionId",
|
||||
"value": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
},
|
||||
{
|
||||
"key": "driverKey",
|
||||
"value": "12:12345678901234"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "List runtime event-processing profiles",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/profiles",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing",
|
||||
"profiles"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute tachograph profile - single session and one driver",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"partitionKeys\": [\n \"{{driverKey}}\"\n ],\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute tachograph profile - multiple sessions all drivers",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute tachograph profile - composite session all drivers",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Compatibility endpoint - tachograph esper processing",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"includeAllDrivers\": true,\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/tachograph/esper-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"tachograph",
|
||||
"esper-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,15 +1,26 @@
|
|||
package at.procon.eventhub.processing.api;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/eventhub/runtime-processing")
|
||||
|
|
@ -17,13 +28,31 @@ public class UnifiedRuntimeProcessingController {
|
|||
|
||||
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
|
||||
private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService;
|
||||
private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService;
|
||||
private final UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService;
|
||||
private final RuntimeEventProcessingService runtimeEventProcessingService;
|
||||
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService
|
||||
) {
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService
|
||||
) {
|
||||
this.eventAssemblyService = eventAssemblyService;
|
||||
this.runtimeDriverTimelineService = runtimeDriverTimelineService;
|
||||
this.runtimeDerivedProjectionService = runtimeDerivedProjectionService;
|
||||
this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService;
|
||||
this.runtimeEventProcessingService = runtimeEventProcessingService;
|
||||
}
|
||||
|
||||
@PostMapping("/driver-events")
|
||||
|
|
@ -39,4 +68,45 @@ public class UnifiedRuntimeProcessingController {
|
|||
) {
|
||||
return ResponseEntity.ok(runtimeDriverTimelineService.loadDriverTimeline(request.toRuntimeRequest()));
|
||||
}
|
||||
|
||||
@PostMapping("/driver-derived-projections")
|
||||
public ResponseEntity<UnifiedRuntimeDerivedProjectionResultDto> loadDriverDerivedProjections(
|
||||
@RequestBody UnifiedRuntimeProcessingApiRequest request
|
||||
) {
|
||||
return ResponseEntity.ok(runtimeDerivedProjectionService.loadDriverDerivedProjections(request));
|
||||
}
|
||||
|
||||
@GetMapping("/event-processing/profiles")
|
||||
public ResponseEntity<List<RuntimeEventProcessingProfileDescriptorDto>> listEventProcessingProfiles() {
|
||||
if (runtimeEventProcessingService == null) {
|
||||
throw new IllegalStateException("Runtime event processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(runtimeEventProcessingService.listProfiles());
|
||||
}
|
||||
|
||||
@PostMapping("/event-processing")
|
||||
public ResponseEntity<RuntimeEventProcessingResultDto> runEventProcessing(
|
||||
@RequestBody RuntimeEventProcessingApiRequest request
|
||||
) {
|
||||
if (runtimeEventProcessingService == null) {
|
||||
throw new IllegalStateException("Runtime event processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(runtimeEventProcessingService.process(request));
|
||||
}
|
||||
|
||||
@PostMapping("/tachograph/esper-processing")
|
||||
public ResponseEntity<UnifiedRuntimeTachographEsperScopeResultDto> runTachographEsperProcessing(
|
||||
@RequestBody UnifiedRuntimeProcessingApiRequest request
|
||||
) {
|
||||
if (runtimeEventProcessingService != null) {
|
||||
RuntimeEventProcessingResultDto genericResult = runtimeEventProcessingService.process(
|
||||
RuntimeEventProcessingApiRequest.tachographDriverEsper(request)
|
||||
);
|
||||
return ResponseEntity.ok(UnifiedRuntimeTachographEsperScopeResultDto.fromGenericRuntimeEventProcessingResult(genericResult));
|
||||
}
|
||||
if (tachographEsperScopeProcessingService == null) {
|
||||
throw new IllegalStateException("Tachograph Esper scope processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(tachographEsperScopeProcessingService.processScope(request));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package at.procon.eventhub.processing.api;
|
||||
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInCompositeSessionException;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
|
|
@ -14,7 +16,9 @@ public class UnifiedRuntimeProcessingExceptionHandler {
|
|||
|
||||
@ExceptionHandler({
|
||||
TachographFileSessionNotFoundException.class,
|
||||
DriverNotFoundInSessionException.class
|
||||
TachographCompositeSessionNotFoundException.class,
|
||||
DriverNotFoundInSessionException.class,
|
||||
DriverNotFoundInCompositeSessionException.class
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> notFound(RuntimeException exception) {
|
||||
return error(HttpStatus.NOT_FOUND, exception);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import java.util.List;
|
||||
|
||||
public record UnifiedRuntimeDerivedProjectionResultDto(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int driverSeedEventCount,
|
||||
int discoveredVehicleCount,
|
||||
int expandedVehicleEventCount,
|
||||
int mergedEventCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
TachographEsperDriverProcessingResultDto projection,
|
||||
List<String> notes
|
||||
) {
|
||||
public UnifiedRuntimeDerivedProjectionResultDto {
|
||||
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,30 +4,45 @@ import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
|||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record UnifiedRuntimeProcessingApiRequest(
|
||||
UUID sessionId,
|
||||
List<UUID> sessionIds,
|
||||
UUID compositeSessionId,
|
||||
String tenantKey,
|
||||
Set<UnifiedEventSourceFamily> sourceFamilies,
|
||||
UnifiedRuntimeEventBackend eventBackend,
|
||||
String driverKey,
|
||||
Set<String> driverKeys,
|
||||
Boolean includeAllDrivers,
|
||||
Set<String> vehicleKeys,
|
||||
Boolean includeAllVehicles,
|
||||
String driverSourceEntityId,
|
||||
String driverCardNation,
|
||||
String driverCardNumber,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
Boolean expandVehicleEvents,
|
||||
Integer vehicleExpansionPaddingMinutes
|
||||
Integer vehicleExpansionPaddingMinutes,
|
||||
Integer significantDrivingMinutes,
|
||||
Integer minimumRestPeriodMinutes
|
||||
) {
|
||||
public UnifiedRuntimeProcessingRequest toRuntimeRequest() {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
sessionIds,
|
||||
compositeSessionId,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
eventBackend,
|
||||
driverKey,
|
||||
driverKeys,
|
||||
includeAllDrivers != null && includeAllDrivers,
|
||||
vehicleKeys,
|
||||
includeAllVehicles != null && includeAllVehicles,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int inputEventCount,
|
||||
int selectedDriverCount,
|
||||
int discoveredVehicleCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto {
|
||||
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
|
||||
driverResults = driverResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(driverResults));
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeTachographEsperScopeResultDto fromGenericRuntimeEventProcessingResult(
|
||||
RuntimeEventProcessingResultDto genericResult
|
||||
) {
|
||||
if (genericResult == null) {
|
||||
throw new IllegalArgumentException("genericResult must not be null");
|
||||
}
|
||||
LinkedHashMap<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : genericResult.partitionResults().entrySet()) {
|
||||
Object value = entry.getValue().result();
|
||||
if (value instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) {
|
||||
driverResults.put(entry.getKey(), projectionResult);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Cannot convert generic partition result for key "
|
||||
+ entry.getKey() + " to UnifiedRuntimeDerivedProjectionResultDto: "
|
||||
+ (value == null ? "null" : value.getClass().getName()));
|
||||
}
|
||||
}
|
||||
return new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
genericResult.request(),
|
||||
genericResult.inputEventCount(),
|
||||
genericResult.selectedPartitionCount(),
|
||||
genericResult.discoveredVehicleCount(),
|
||||
genericResult.discoveredVehicles(),
|
||||
driverResults,
|
||||
genericResult.notes(),
|
||||
genericResult.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package at.procon.eventhub.processing.eventprocessing;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import java.util.List;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.RuntimeEventProcessingProfileRegistry;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RuntimeEventProcessingService {
|
||||
|
||||
private final RuntimeEventProcessingProfileRegistry profileRegistry;
|
||||
|
||||
public RuntimeEventProcessingService(RuntimeEventProcessingProfileRegistry profileRegistry) {
|
||||
this.profileRegistry = profileRegistry;
|
||||
}
|
||||
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
return profileRegistry.require(request.profileKey()).process(request);
|
||||
}
|
||||
|
||||
public List<RuntimeEventProcessingProfileDescriptorDto> listProfiles() {
|
||||
return profileRegistry.profileDescriptors();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import java.util.Set;
|
||||
|
||||
public record RuntimeEventPartitioningApiRequest(
|
||||
RuntimeEventPartitioningStrategy strategy,
|
||||
Set<String> partitionKeys,
|
||||
Boolean includeAllPartitions,
|
||||
Set<String> driverKeys,
|
||||
Boolean includeAllDrivers,
|
||||
Set<String> vehicleKeys,
|
||||
Boolean includeAllVehicles,
|
||||
Boolean attachVehicleEvidence,
|
||||
Integer vehicleEvidencePaddingMinutes
|
||||
) {
|
||||
public RuntimeEventPartitioningApiRequest {
|
||||
strategy = strategy == null ? RuntimeEventPartitioningStrategy.CUSTOM_PROFILE : strategy;
|
||||
partitionKeys = partitionKeys == null ? Set.of() : Set.copyOf(partitionKeys);
|
||||
driverKeys = driverKeys == null ? Set.of() : Set.copyOf(driverKeys);
|
||||
vehicleKeys = vehicleKeys == null ? Set.of() : Set.copyOf(vehicleKeys);
|
||||
if (vehicleEvidencePaddingMinutes != null && vehicleEvidencePaddingMinutes < 0) {
|
||||
throw new IllegalArgumentException("vehicleEvidencePaddingMinutes must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean allPartitions() {
|
||||
return includeAllPartitions != null && includeAllPartitions;
|
||||
}
|
||||
|
||||
public boolean allDrivers() {
|
||||
return includeAllDrivers != null && includeAllDrivers;
|
||||
}
|
||||
|
||||
public boolean allVehicles() {
|
||||
return includeAllVehicles != null && includeAllVehicles;
|
||||
}
|
||||
|
||||
public boolean attachVehicleEvidenceOrDefault() {
|
||||
return attachVehicleEvidence == null || attachVehicleEvidence;
|
||||
}
|
||||
|
||||
public int vehicleEvidencePaddingMinutesOrDefault(int fallback) {
|
||||
return vehicleEvidencePaddingMinutes == null ? Math.max(0, fallback) : Math.max(0, vehicleEvidencePaddingMinutes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record RuntimeEventProcessingApiRequest(
|
||||
String profileKey,
|
||||
UnifiedRuntimeProcessingApiRequest scope,
|
||||
RuntimeEventPartitioningApiRequest partitioning,
|
||||
Map<String, Object> parameters
|
||||
) {
|
||||
public RuntimeEventProcessingApiRequest {
|
||||
if (profileKey == null || profileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("profileKey must not be blank");
|
||||
}
|
||||
profileKey = profileKey.trim();
|
||||
if (scope == null) {
|
||||
throw new IllegalArgumentException("scope must not be null");
|
||||
}
|
||||
partitioning = partitioning == null
|
||||
? new RuntimeEventPartitioningApiRequest(null, null, null, null, null, null, null, null, null)
|
||||
: partitioning;
|
||||
parameters = parameters == null
|
||||
? Map.of()
|
||||
: Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
|
||||
}
|
||||
|
||||
public static RuntimeEventProcessingApiRequest tachographDriverEsper(UnifiedRuntimeProcessingApiRequest scope) {
|
||||
return new RuntimeEventProcessingApiRequest(
|
||||
"tachograph-driver-esper-v1",
|
||||
scope,
|
||||
new RuntimeEventPartitioningApiRequest(
|
||||
at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy.DRIVER,
|
||||
null,
|
||||
scope != null ? scope.includeAllDrivers() : null,
|
||||
scope != null ? scope.driverKeys() : null,
|
||||
scope != null ? scope.includeAllDrivers() : null,
|
||||
scope != null ? scope.vehicleKeys() : null,
|
||||
scope != null ? scope.includeAllVehicles() : null,
|
||||
null,
|
||||
scope != null ? scope.vehicleExpansionPaddingMinutes() : null
|
||||
),
|
||||
Map.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record RuntimeEventProcessingPartitionResultDto(
|
||||
String partitionType,
|
||||
String partitionKey,
|
||||
String resultType,
|
||||
Object result,
|
||||
Map<String, Object> metadata
|
||||
) {
|
||||
public RuntimeEventProcessingPartitionResultDto {
|
||||
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.RuntimeEventProcessingProfile;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public record RuntimeEventProcessingProfileDescriptorDto(
|
||||
String profileKey,
|
||||
String displayName,
|
||||
String description,
|
||||
RuntimeEventPartitioningStrategy defaultPartitioningStrategy,
|
||||
List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies,
|
||||
Set<String> requiredParameters,
|
||||
Set<String> optionalParameters
|
||||
) {
|
||||
public RuntimeEventProcessingProfileDescriptorDto {
|
||||
supportedPartitioningStrategies = supportedPartitioningStrategies == null
|
||||
? List.of()
|
||||
: List.copyOf(supportedPartitioningStrategies);
|
||||
requiredParameters = requiredParameters == null ? Set.of() : Set.copyOf(requiredParameters);
|
||||
optionalParameters = optionalParameters == null ? Set.of() : Set.copyOf(optionalParameters);
|
||||
}
|
||||
|
||||
public static RuntimeEventProcessingProfileDescriptorDto from(RuntimeEventProcessingProfile profile) {
|
||||
if (profile == null) {
|
||||
throw new IllegalArgumentException("profile must not be null");
|
||||
}
|
||||
return new RuntimeEventProcessingProfileDescriptorDto(
|
||||
profile.profileKey(),
|
||||
profile.displayName(),
|
||||
profile.description(),
|
||||
profile.defaultPartitioningStrategy(),
|
||||
profile.supportedPartitioningStrategies(),
|
||||
profile.requiredParameters(),
|
||||
profile.optionalParameters()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record RuntimeEventProcessingResultDto(
|
||||
String profileKey,
|
||||
RuntimeEventPartitioningStrategy partitioningStrategy,
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int inputEventCount,
|
||||
int selectedPartitionCount,
|
||||
int discoveredVehicleCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeEventProcessingResultDto {
|
||||
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
|
||||
partitionResults = partitionResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionResults));
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.partition;
|
||||
|
||||
public enum RuntimeEventPartitioningStrategy {
|
||||
NONE,
|
||||
DRIVER,
|
||||
VEHICLE,
|
||||
DRIVER_VEHICLE,
|
||||
SOURCE_FAMILY,
|
||||
CUSTOM_PROFILE
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.partition;
|
||||
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RuntimeEventScopeClassifier {
|
||||
|
||||
public RuntimeEventScopeType classify(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return RuntimeEventScopeType.UNKNOWN;
|
||||
}
|
||||
boolean hasDriver = hasDriver(event);
|
||||
boolean hasVehicle = hasVehicle(event);
|
||||
if (hasDriver && hasVehicle) {
|
||||
return RuntimeEventScopeType.DRIVER_VEHICLE_SCOPED;
|
||||
}
|
||||
if (hasDriver) {
|
||||
return RuntimeEventScopeType.DRIVER_SCOPED;
|
||||
}
|
||||
if (hasVehicle) {
|
||||
return RuntimeEventScopeType.VEHICLE_SCOPED;
|
||||
}
|
||||
return RuntimeEventScopeType.GLOBAL_SUPPORT;
|
||||
}
|
||||
|
||||
public boolean hasDriver(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
if (text(rawPayload(event), "driverKey") != null) {
|
||||
return true;
|
||||
}
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
return driverRef != null && driverRef.hasAnyReference();
|
||||
}
|
||||
|
||||
public boolean hasVehicle(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
JsonNode raw = rawPayload(event);
|
||||
if (text(raw, "vehicleKey") != null || text(raw, "registrationKey") != null) {
|
||||
return true;
|
||||
}
|
||||
VehicleRefDto vehicleRef = event.vehicleRef();
|
||||
return vehicleRef != null && vehicleRef.hasAnyReference();
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.partition;
|
||||
|
||||
public enum RuntimeEventScopeType {
|
||||
DRIVER_SCOPED,
|
||||
VEHICLE_SCOPED,
|
||||
DRIVER_VEHICLE_SCOPED,
|
||||
GLOBAL_SUPPORT,
|
||||
UNKNOWN
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface RuntimeEventProcessingProfile {
|
||||
|
||||
String profileKey();
|
||||
|
||||
RuntimeEventPartitioningStrategy defaultPartitioningStrategy();
|
||||
|
||||
default String displayName() {
|
||||
return profileKey();
|
||||
}
|
||||
|
||||
default String description() {
|
||||
return "";
|
||||
}
|
||||
|
||||
default List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
|
||||
RuntimeEventPartitioningStrategy defaultStrategy = defaultPartitioningStrategy();
|
||||
return defaultStrategy == null ? List.of() : List.of(defaultStrategy);
|
||||
}
|
||||
|
||||
default Set<String> requiredParameters() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
default Set<String> optionalParameters() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RuntimeEventProcessingProfileRegistry {
|
||||
|
||||
private final Map<String, RuntimeEventProcessingProfile> profilesByKey;
|
||||
|
||||
public RuntimeEventProcessingProfileRegistry(List<RuntimeEventProcessingProfile> profiles) {
|
||||
LinkedHashMap<String, RuntimeEventProcessingProfile> byKey = new LinkedHashMap<>();
|
||||
for (RuntimeEventProcessingProfile profile : profiles == null ? List.<RuntimeEventProcessingProfile>of() : profiles) {
|
||||
String key = profile.profileKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
throw new IllegalStateException("Runtime event processing profile returned a blank profileKey: " + profile.getClass().getName());
|
||||
}
|
||||
RuntimeEventProcessingProfile previous = byKey.putIfAbsent(key, profile);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate runtime event processing profileKey: " + key);
|
||||
}
|
||||
}
|
||||
this.profilesByKey = Collections.unmodifiableMap(byKey);
|
||||
}
|
||||
|
||||
public RuntimeEventProcessingProfile require(String profileKey) {
|
||||
String normalized = profileKey == null ? null : profileKey.trim();
|
||||
RuntimeEventProcessingProfile profile = normalized == null ? null : profilesByKey.get(normalized);
|
||||
if (profile == null) {
|
||||
throw new IllegalArgumentException("Unknown runtime event processing profileKey: " + profileKey
|
||||
+ ". Available profiles: " + profilesByKey.keySet());
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
public Map<String, RuntimeEventProcessingProfile> profilesByKey() {
|
||||
return profilesByKey;
|
||||
}
|
||||
|
||||
public List<RuntimeEventProcessingProfileDescriptorDto> profileDescriptors() {
|
||||
return profilesByKey.values().stream()
|
||||
.map(RuntimeEventProcessingProfileDescriptorDto::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile {
|
||||
|
||||
public static final String PROFILE_KEY = "tachograph-driver-esper-v1";
|
||||
|
||||
private final UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService;
|
||||
|
||||
public TachographDriverEsperRuntimeEventProcessingProfile(
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService
|
||||
) {
|
||||
this.tachographScopeProcessingService = tachographScopeProcessingService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String profileKey() {
|
||||
return PROFILE_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
|
||||
return RuntimeEventPartitioningStrategy.DRIVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String displayName() {
|
||||
return "Tachograph Driver Esper Processing";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes. "
|
||||
+ "The profile partitions mixed runtime events by driver before invoking the event-input EPL pipeline.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
|
||||
return List.of(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> optionalParameters() {
|
||||
return Set.of("significantDrivingMinutes", "minimumRestPeriodMinutes", "attachVehicleOnlyEvents", "vehicleEvidencePaddingMinutes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
UnifiedRuntimeProcessingApiRequest tachographScopeRequest = applyGenericRequest(request.scope(), request.partitioning(), request.parameters());
|
||||
UnifiedRuntimeTachographEsperScopeResultDto tachographResult = tachographScopeProcessingService.processScope(tachographScopeRequest);
|
||||
|
||||
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>();
|
||||
tachographResult.driverResults().forEach((driverKey, driverResult) -> partitionResults.put(
|
||||
driverKey,
|
||||
new RuntimeEventProcessingPartitionResultDto(
|
||||
"DRIVER",
|
||||
driverKey,
|
||||
"UnifiedRuntimeDerivedProjectionResultDto",
|
||||
driverResult,
|
||||
Map.of(
|
||||
"projectionResultType", driverResult.projection() == null ? "NONE" : "TachographEsperDriverProcessingResultDto",
|
||||
"driverSeedEventCount", driverResult.driverSeedEventCount(),
|
||||
"expandedVehicleEventCount", driverResult.expandedVehicleEventCount(),
|
||||
"mergedEventCount", driverResult.mergedEventCount()
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
return new RuntimeEventProcessingResultDto(
|
||||
profileKey(),
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
tachographResult.request(),
|
||||
tachographResult.inputEventCount(),
|
||||
tachographResult.selectedDriverCount(),
|
||||
tachographResult.discoveredVehicleCount(),
|
||||
tachographResult.discoveredVehicles(),
|
||||
partitionResults,
|
||||
tachographResult.notes(),
|
||||
tachographResult.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
private UnifiedRuntimeProcessingApiRequest applyGenericRequest(
|
||||
UnifiedRuntimeProcessingApiRequest scope,
|
||||
RuntimeEventPartitioningApiRequest partitioning,
|
||||
Map<String, Object> parameters
|
||||
) {
|
||||
if (scope == null) {
|
||||
throw new IllegalArgumentException("scope must not be null for profile " + PROFILE_KEY + ".");
|
||||
}
|
||||
boolean includeAllDrivers = scope.includeAllDrivers() != null && scope.includeAllDrivers();
|
||||
java.util.Set<String> driverKeys = scope.driverKeys();
|
||||
boolean includeAllVehicles = scope.includeAllVehicles() != null && scope.includeAllVehicles();
|
||||
java.util.Set<String> vehicleKeys = scope.vehicleKeys();
|
||||
|
||||
if (partitioning != null) {
|
||||
RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null
|
||||
? RuntimeEventPartitioningStrategy.DRIVER
|
||||
: partitioning.strategy();
|
||||
if (strategy != RuntimeEventPartitioningStrategy.DRIVER
|
||||
&& strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) {
|
||||
throw new IllegalArgumentException("Profile " + PROFILE_KEY
|
||||
+ " currently supports DRIVER partitioning only. Requested: " + strategy);
|
||||
}
|
||||
includeAllDrivers = includeAllDrivers || partitioning.allPartitions() || partitioning.allDrivers();
|
||||
if (!partitioning.driverKeys().isEmpty()) {
|
||||
driverKeys = partitioning.driverKeys();
|
||||
} else if (!partitioning.partitionKeys().isEmpty()) {
|
||||
driverKeys = partitioning.partitionKeys();
|
||||
}
|
||||
includeAllVehicles = includeAllVehicles || partitioning.allVehicles();
|
||||
if (!partitioning.vehicleKeys().isEmpty()) {
|
||||
vehicleKeys = partitioning.vehicleKeys();
|
||||
}
|
||||
}
|
||||
|
||||
Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", scope.significantDrivingMinutes());
|
||||
Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", scope.minimumRestPeriodMinutes());
|
||||
boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents",
|
||||
partitioning == null ? scope.expandVehicleEvents() == null || scope.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault());
|
||||
Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes",
|
||||
partitioning == null
|
||||
? scope.vehicleExpansionPaddingMinutes()
|
||||
: partitioning.vehicleEvidencePaddingMinutesOrDefault(scope.vehicleExpansionPaddingMinutes() == null ? 0 : scope.vehicleExpansionPaddingMinutes()));
|
||||
|
||||
return new UnifiedRuntimeProcessingApiRequest(
|
||||
scope.sessionId(),
|
||||
scope.sessionIds(),
|
||||
scope.compositeSessionId(),
|
||||
scope.tenantKey(),
|
||||
scope.sourceFamilies(),
|
||||
scope.eventBackend(),
|
||||
scope.driverKey(),
|
||||
driverKeys,
|
||||
includeAllDrivers,
|
||||
vehicleKeys,
|
||||
includeAllVehicles,
|
||||
scope.driverSourceEntityId(),
|
||||
scope.driverCardNation(),
|
||||
scope.driverCardNumber(),
|
||||
scope.occurredFrom(),
|
||||
scope.occurredTo(),
|
||||
attachVehicleOnlyEvents,
|
||||
vehicleEvidencePaddingMinutes,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
private boolean booleanParameter(Map<String, Object> parameters, String key, boolean fallback) {
|
||||
if (parameters == null || !parameters.containsKey(key)) {
|
||||
return fallback;
|
||||
}
|
||||
Object value = parameters.get(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (value instanceof Boolean booleanValue) {
|
||||
return booleanValue;
|
||||
}
|
||||
String text = value.toString();
|
||||
if (text == null || text.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return Boolean.parseBoolean(text.trim());
|
||||
}
|
||||
|
||||
private Integer nonNegativeIntegerParameter(Map<String, Object> parameters, String key, Integer fallback) {
|
||||
if (parameters == null || !parameters.containsKey(key)) {
|
||||
return fallback;
|
||||
}
|
||||
Object value = parameters.get(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return Math.max(0, number.intValue());
|
||||
}
|
||||
String text = value.toString();
|
||||
if (text == null || text.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Math.max(0, Integer.parseInt(text.trim()));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer integerParameter(Map<String, Object> parameters, String key, Integer fallback) {
|
||||
if (parameters == null || !parameters.containsKey(key)) {
|
||||
return fallback;
|
||||
}
|
||||
Object value = parameters.get(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return Math.max(1, number.intValue());
|
||||
}
|
||||
String text = value.toString();
|
||||
if (text == null || text.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Math.max(1, Integer.parseInt(text.trim()));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package at.procon.eventhub.processing.model;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import java.util.List;
|
||||
|
||||
public record RuntimeDriverVehicleEvidenceAttachmentResult(
|
||||
String driverKey,
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> attachedVehicleEvidenceEvents,
|
||||
List<EventHubEventDto> mergedEvents,
|
||||
int vehicleUsageIntervalCount,
|
||||
int candidateVehicleEvidenceEventCount,
|
||||
int ignoredVehicleEvidenceEventCount,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeDriverVehicleEvidenceAttachmentResult {
|
||||
directDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
|
||||
attachedVehicleEvidenceEvents = attachedVehicleEvidenceEvents == null ? List.of() : List.copyOf(attachedVehicleEvidenceEvents);
|
||||
mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,9 +36,6 @@ public record UnifiedDriverEventsRequest(
|
|||
}
|
||||
if (sourceFamily == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION) {
|
||||
Objects.requireNonNull(sessionId, "sessionId must not be null");
|
||||
if (driverKey == null) {
|
||||
throw new IllegalArgumentException("driverKey must not be blank");
|
||||
}
|
||||
} else {
|
||||
if (tenantKey == null) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
|
|
@ -50,6 +47,10 @@ public record UnifiedDriverEventsRequest(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* File-session requests may omit driverKey when the caller intentionally wants
|
||||
* to load all drivers from the session and partition them later in runtime scope processing.
|
||||
*/
|
||||
public static UnifiedDriverEventsRequest forTachographFileSession(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
|
|
|
|||
|
|
@ -2,15 +2,24 @@ package at.procon.eventhub.processing.model;
|
|||
|
||||
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record UnifiedRuntimeProcessingRequest(
|
||||
UUID sessionId,
|
||||
List<UUID> sessionIds,
|
||||
UUID compositeSessionId,
|
||||
String tenantKey,
|
||||
Set<UnifiedEventSourceFamily> sourceFamilies,
|
||||
UnifiedRuntimeEventBackend eventBackend,
|
||||
String driverKey,
|
||||
Set<String> driverKeys,
|
||||
boolean includeAllDrivers,
|
||||
Set<String> vehicleKeys,
|
||||
boolean includeAllVehicles,
|
||||
String driverSourceEntityId,
|
||||
String driverCardNation,
|
||||
String driverCardNumber,
|
||||
|
|
@ -20,32 +29,64 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
public UnifiedRuntimeProcessingRequest {
|
||||
driverKey = normalize(driverKey);
|
||||
tenantKey = normalize(tenantKey);
|
||||
driverSourceEntityId = normalize(driverSourceEntityId);
|
||||
driverCardNation = normalizeUpper(driverCardNation);
|
||||
driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
|
||||
boolean includesFileSession = sourceFamilies != null && sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
|
||||
boolean includesExternalDb = sourceFamilies != null && sourceFamilies.stream()
|
||||
.anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB);
|
||||
if (tenantKey == null) {
|
||||
if (!includesFileSession || includesExternalDb) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
}
|
||||
}
|
||||
if (sourceFamilies == null || sourceFamilies.isEmpty()) {
|
||||
throw new IllegalArgumentException("sourceFamilies must not be empty");
|
||||
}
|
||||
sourceFamilies = Set.copyOf(sourceFamilies);
|
||||
eventBackend = eventBackend == null ? UnifiedRuntimeEventBackend.SOURCE_DB : eventBackend;
|
||||
if (includesFileSession && sessionId == null) {
|
||||
throw new IllegalArgumentException("sessionId must not be null when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
sessionIds = normalizeSessionIds(sessionId, sessionIds);
|
||||
if (sessionId == null && !sessionIds.isEmpty()) {
|
||||
sessionId = sessionIds.get(0);
|
||||
}
|
||||
if (includesFileSession && driverKey == null) {
|
||||
throw new IllegalArgumentException("driverKey must not be blank when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
if (compositeSessionId != null && !sessionIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("Use either compositeSessionId or sessionId/sessionIds, not both.");
|
||||
}
|
||||
if (includesExternalDb && driverSourceEntityId == null && driverCardNumber == null) {
|
||||
throw new IllegalArgumentException("At least one driver selector must be provided.");
|
||||
driverKey = normalize(driverKey);
|
||||
driverKeys = normalizeStrings(driverKeys);
|
||||
if (driverKey != null) {
|
||||
LinkedHashSet<String> mergedDriverKeys = new LinkedHashSet<>(driverKeys);
|
||||
mergedDriverKeys.add(driverKey);
|
||||
driverKeys = Set.copyOf(mergedDriverKeys);
|
||||
}
|
||||
if (driverKey == null && driverKeys.size() == 1) {
|
||||
driverKey = driverKeys.iterator().next();
|
||||
}
|
||||
vehicleKeys = normalizeStrings(vehicleKeys);
|
||||
tenantKey = normalize(tenantKey);
|
||||
driverSourceEntityId = normalize(driverSourceEntityId);
|
||||
driverCardNation = normalizeUpper(driverCardNation);
|
||||
driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
|
||||
boolean includesFileSession = sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
|
||||
boolean includesExternalDb = sourceFamilies.stream()
|
||||
.anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB);
|
||||
if (includesFileSession && eventBackend == UnifiedRuntimeEventBackend.EVENTHUB_DB) {
|
||||
throw new IllegalArgumentException("TACHOGRAPH_FILE_SESSION runtime processing currently supports SOURCE_DB backend only.");
|
||||
}
|
||||
if (tenantKey == null) {
|
||||
if (!includesFileSession || includesExternalDb) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
}
|
||||
}
|
||||
if (includesFileSession && compositeSessionId == null && sessionIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("sessionId, sessionIds or compositeSessionId must be provided when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
}
|
||||
if (includesFileSession && driverKey == null && driverKeys.isEmpty() && !includeAllDrivers) {
|
||||
throw new IllegalArgumentException("driverKey, driverKeys or includeAllDrivers must be provided when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
}
|
||||
if (includesExternalDb && driverSourceEntityId == null && driverCardNumber == null
|
||||
&& driverKeys.isEmpty() && !includeAllDrivers) {
|
||||
throw new IllegalArgumentException("At least one driver selector, driverKeys or includeAllDrivers must be provided.");
|
||||
}
|
||||
if (includesExternalDb && (includeAllDrivers || !driverKeys.isEmpty())
|
||||
&& (occurredFrom == null || occurredTo == null)) {
|
||||
throw new IllegalArgumentException("occurredFrom and occurredTo are required when loading broad external DB runtime scopes.");
|
||||
}
|
||||
if (eventBackend == UnifiedRuntimeEventBackend.EVENTHUB_DB
|
||||
&& includesExternalDb
|
||||
&& driverSourceEntityId == null
|
||||
&& driverCardNumber == null
|
||||
&& (includeAllDrivers || !driverKeys.isEmpty())) {
|
||||
throw new IllegalArgumentException("Broad multi-driver EVENTHUB_DB runtime scopes are not supported yet; provide a concrete EventHub driver selector or use SOURCE_DB.");
|
||||
}
|
||||
if (occurredFrom != null && occurredTo != null && occurredTo.isBefore(occurredFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom");
|
||||
|
|
@ -116,11 +157,17 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
eventBackend,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
|
|
@ -143,11 +190,17 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
UnifiedRuntimeEventBackend.EVENTHUB_DB,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
|
|
@ -168,10 +221,76 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
driverKey,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
occurredFrom,
|
||||
occurredTo,
|
||||
expandVehicleEvents,
|
||||
vehicleExpansionPaddingMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeProcessingRequest forTachographFileSessions(
|
||||
List<UUID> sessionIds,
|
||||
String driverKey,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
boolean expandVehicleEvents,
|
||||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
sessionIds,
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
driverKey,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
occurredFrom,
|
||||
occurredTo,
|
||||
expandVehicleEvents,
|
||||
vehicleExpansionPaddingMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeProcessingRequest forTachographCompositeSession(
|
||||
UUID compositeSessionId,
|
||||
String driverKey,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
boolean expandVehicleEvents,
|
||||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
compositeSessionId,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
driverKey,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
|
@ -190,6 +309,58 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
return occurredTo == null ? null : occurredTo.plusMinutes(vehicleExpansionPaddingMinutes);
|
||||
}
|
||||
|
||||
private static List<UUID> normalizeSessionIds(UUID sessionId, List<UUID> sessionIds) {
|
||||
LinkedHashSet<UUID> result = new LinkedHashSet<>();
|
||||
if (sessionId != null) {
|
||||
result.add(sessionId);
|
||||
}
|
||||
if (sessionIds != null) {
|
||||
result.addAll(sessionIds.stream().filter(value -> value != null).toList());
|
||||
}
|
||||
return List.copyOf(new ArrayList<>(result));
|
||||
}
|
||||
|
||||
public UnifiedRuntimeProcessingRequest withDriverKey(String value) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
sessionIds,
|
||||
compositeSessionId,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
eventBackend,
|
||||
value,
|
||||
Set.of(),
|
||||
false,
|
||||
vehicleKeys,
|
||||
includeAllVehicles,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
occurredFrom,
|
||||
occurredTo,
|
||||
expandVehicleEvents,
|
||||
vehicleExpansionPaddingMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public boolean scopeDriverSelectionRequested() {
|
||||
return includeAllDrivers || driverKeys.size() > 1 || (driverKey == null && !driverKeys.isEmpty());
|
||||
}
|
||||
|
||||
private static Set<String> normalizeStrings(Set<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||
for (String value : values) {
|
||||
String normalizedValue = normalize(value);
|
||||
if (normalizedValue != null) {
|
||||
normalized.add(normalizedValue);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(normalized);
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,302 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
|
||||
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||
|
||||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final RuntimeEventScopeClassifier scopeClassifier;
|
||||
|
||||
public RuntimeDriverVehicleEvidenceAttachmentService(
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
RuntimeEventScopeClassifier scopeClassifier
|
||||
) {
|
||||
this.timelineReconstructor = timelineReconstructor;
|
||||
this.scopeClassifier = scopeClassifier;
|
||||
}
|
||||
|
||||
public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
|
||||
String driverKey,
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> runtimeScopeEvents,
|
||||
boolean attachVehicleOnlyEvents,
|
||||
int vehicleEvidencePaddingMinutes
|
||||
) {
|
||||
List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
|
||||
List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
|
||||
int paddingMinutes = Math.max(0, vehicleEvidencePaddingMinutes);
|
||||
|
||||
List<String> notes = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (!attachVehicleOnlyEvents) {
|
||||
notes.add("Vehicle-only evidence attachment is disabled for driver partition " + driverKey + ".");
|
||||
return new RuntimeDriverVehicleEvidenceAttachmentResult(
|
||||
driverKey,
|
||||
safeDriverEvents,
|
||||
List.of(),
|
||||
deduplicateAndSort(safeDriverEvents, List.of()),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents);
|
||||
List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals());
|
||||
List<EventHubEventDto> candidateVehicleEvidence = safeScopeEvents.stream()
|
||||
.filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
|
||||
.toList();
|
||||
List<EventHubEventDto> attached = new ArrayList<>();
|
||||
int ignored = 0;
|
||||
for (EventHubEventDto vehicleEvent : candidateVehicleEvidence) {
|
||||
List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes);
|
||||
if (matchingIntervals.isEmpty()) {
|
||||
ignored++;
|
||||
continue;
|
||||
}
|
||||
attached.add(vehicleEvent);
|
||||
if (matchingIntervals.size() > 1) {
|
||||
warnings.add("Vehicle-only event " + vehicleEvent.externalSourceEventId()
|
||||
+ " matched multiple vehicle-usage intervals for driver " + driverKey
|
||||
+ "; it was attached once after deduplication.");
|
||||
}
|
||||
}
|
||||
|
||||
notes.add("Vehicle-only evidence attachment used " + usageIntervals.size()
|
||||
+ " reconstructed vehicle-usage interval(s) for driver " + driverKey + ".");
|
||||
notes.add("Vehicle-only evidence padding minutes: " + paddingMinutes + ".");
|
||||
notes.add("Candidate vehicle-only evidence events: " + candidateVehicleEvidence.size() + ".");
|
||||
notes.add("Attached vehicle-only evidence events: " + attached.size() + ".");
|
||||
notes.add("Ignored vehicle-only evidence events: " + ignored + ".");
|
||||
if (usageIntervals.isEmpty() && !candidateVehicleEvidence.isEmpty()) {
|
||||
warnings.add("Vehicle-only evidence was available for driver " + driverKey
|
||||
+ ", but no driver vehicle-usage intervals were reconstructed; no vehicle-only evidence was attached.");
|
||||
}
|
||||
|
||||
return new RuntimeDriverVehicleEvidenceAttachmentResult(
|
||||
driverKey,
|
||||
safeDriverEvents,
|
||||
attached,
|
||||
deduplicateAndSort(safeDriverEvents, attached),
|
||||
usageIntervals.size(),
|
||||
candidateVehicleEvidence.size(),
|
||||
ignored,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> matchingUsageIntervals(
|
||||
EventHubEventDto vehicleEvent,
|
||||
List<ResolvedVehicleUsageInterval> usageIntervals,
|
||||
int paddingMinutes
|
||||
) {
|
||||
if (vehicleEvent == null || vehicleEvent.occurredAt() == null || usageIntervals == null || usageIntervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
|
||||
for (ResolvedVehicleUsageInterval interval : usageIntervals) {
|
||||
if (matchesVehicle(vehicleEvent, interval) && timeInside(vehicleEvent.occurredAt(), interval, paddingMinutes)) {
|
||||
result.add(interval);
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private boolean timeInside(OffsetDateTime occurredAt, ResolvedVehicleUsageInterval interval, int paddingMinutes) {
|
||||
if (occurredAt == null || interval == null || interval.from() == null) {
|
||||
return false;
|
||||
}
|
||||
OffsetDateTime from = interval.from().minusMinutes(paddingMinutes);
|
||||
OffsetDateTime to = interval.to() == null ? OffsetDateTime.MAX : interval.to().plusMinutes(paddingMinutes);
|
||||
return !occurredAt.isBefore(from) && !occurredAt.isAfter(to);
|
||||
}
|
||||
|
||||
private boolean matchesVehicle(EventHubEventDto event, ResolvedVehicleUsageInterval interval) {
|
||||
if (event == null || interval == null) {
|
||||
return false;
|
||||
}
|
||||
Set<String> eventKeys = vehicleKeys(event);
|
||||
Set<String> intervalKeys = vehicleKeys(interval);
|
||||
if (eventKeys.isEmpty() || intervalKeys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return eventKeys.stream().anyMatch(intervalKeys::contains);
|
||||
}
|
||||
|
||||
private Set<String> vehicleKeys(EventHubEventDto event) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
JsonNode raw = rawPayload(event);
|
||||
add(result, text(raw, "vehicleKey"));
|
||||
add(result, text(raw, "registrationKey"));
|
||||
VehicleRefDto vehicleRef = event.vehicleRef();
|
||||
if (vehicleRef != null) {
|
||||
add(result, vehicleRef.sourceVehicleEntityId());
|
||||
add(result, vehicleRef.sourceRegistrationEntityId());
|
||||
add(result, vehicleRef.vin());
|
||||
if (vehicleRef.vin() != null) {
|
||||
add(result, "VIN:" + vehicleRef.vin());
|
||||
}
|
||||
if (vehicleRef.vehicleRegistration() != null) {
|
||||
String registrationKey = vehicleRef.vehicleRegistration().stableKey();
|
||||
add(result, registrationKey);
|
||||
add(result, "VR:" + registrationKey);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private Set<String> vehicleKeys(ResolvedVehicleUsageInterval interval) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
add(result, interval.vehicleKey());
|
||||
add(result, interval.registrationKey());
|
||||
if (interval.vehicleKey() != null) {
|
||||
add(result, "VIN:" + interval.vehicleKey());
|
||||
}
|
||||
if (interval.registrationKey() != null) {
|
||||
add(result, "VR:" + interval.registrationKey());
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private void add(Set<String> keys, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
keys.add(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(List<ResolvedVehicleUsageInterval> intervals) {
|
||||
if (intervals == null || intervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ResolvedVehicleUsageInterval> sorted = intervals.stream()
|
||||
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(ResolvedVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
List<ResolvedVehicleUsageInterval> merged = new ArrayList<>();
|
||||
for (ResolvedVehicleUsageInterval next : sorted) {
|
||||
if (merged.isEmpty()) {
|
||||
merged.add(next);
|
||||
continue;
|
||||
}
|
||||
ResolvedVehicleUsageInterval current = merged.get(merged.size() - 1);
|
||||
if (canMerge(current, next)) {
|
||||
merged.set(merged.size() - 1, merge(current, next));
|
||||
} else {
|
||||
merged.add(next);
|
||||
}
|
||||
}
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
private boolean canMerge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
|
||||
if (left == null || right == null || left.to() == null || right.from() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(left.driverKey(), right.driverKey())
|
||||
&& Objects.equals(left.registrationKey(), right.registrationKey())
|
||||
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
|
||||
&& !right.from().isAfter(left.to().plusSeconds(1));
|
||||
}
|
||||
|
||||
private ResolvedVehicleUsageInterval merge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
|
||||
LinkedHashSet<String> sourceIntervalIds = new LinkedHashSet<>();
|
||||
if (left.sourceIntervalIds() != null) {
|
||||
sourceIntervalIds.addAll(left.sourceIntervalIds());
|
||||
}
|
||||
if (right.sourceIntervalIds() != null) {
|
||||
sourceIntervalIds.addAll(right.sourceIntervalIds());
|
||||
}
|
||||
OffsetDateTime end = left.to();
|
||||
if (right.to() != null && (end == null || right.to().isAfter(end))) {
|
||||
end = right.to();
|
||||
}
|
||||
return ResolvedVehicleUsageInterval.resolved(
|
||||
left.sessionId(),
|
||||
left.driverKey(),
|
||||
left.intervalId(),
|
||||
left.from(),
|
||||
end,
|
||||
left.odometerBeginKm(),
|
||||
right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
|
||||
left.registrationKey(),
|
||||
left.vehicleKey(),
|
||||
left.sourceKind(),
|
||||
List.copyOf(sourceIntervalIds)
|
||||
);
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> deduplicateAndSort(
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> vehicleEvidenceEvents
|
||||
) {
|
||||
LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
|
||||
appendDeduplicated(byKey, directDriverEvents);
|
||||
appendDeduplicated(byKey, vehicleEvidenceEvents);
|
||||
return sort(new ArrayList<>(byKey.values()));
|
||||
}
|
||||
|
||||
private void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
|
||||
for (EventHubEventDto event : events == null ? List.<EventHubEventDto>of() : events) {
|
||||
byKey.putIfAbsent(dedupKey(event), event);
|
||||
}
|
||||
}
|
||||
|
||||
private String dedupKey(EventHubEventDto event) {
|
||||
String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null
|
||||
? event.packageInfo().eventSource().stableKey()
|
||||
: "NO_SOURCE";
|
||||
return sourceKey + "|" + event.externalSourceEventId();
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> sort(List<EventHubEventDto> events) {
|
||||
return (events == null ? List.<EventHubEventDto>of() : events).stream()
|
||||
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
|
||||
.thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
|
||||
.thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
|
||||
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,14 @@ import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
|||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest;
|
||||
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
||||
import at.procon.eventhub.service.EventHubEventSorter;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
|
|
@ -15,13 +22,22 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
|
||||
private final UnifiedDriverEventSourceService driverEventSourceService;
|
||||
private final UnifiedVehicleEventSourceService vehicleEventSourceService;
|
||||
private final TachographCompositeSessionRepository compositeSessionRepository;
|
||||
private final EventAcquisitionRecordKeyService eventKeyService;
|
||||
private final EventHubEventSorter eventSorter;
|
||||
|
||||
public TachographFileSessionRuntimeEventLoader(
|
||||
UnifiedDriverEventSourceService driverEventSourceService,
|
||||
UnifiedVehicleEventSourceService vehicleEventSourceService
|
||||
UnifiedVehicleEventSourceService vehicleEventSourceService,
|
||||
TachographCompositeSessionRepository compositeSessionRepository,
|
||||
EventAcquisitionRecordKeyService eventKeyService,
|
||||
EventHubEventSorter eventSorter
|
||||
) {
|
||||
this.driverEventSourceService = driverEventSourceService;
|
||||
this.vehicleEventSourceService = vehicleEventSourceService;
|
||||
this.compositeSessionRepository = compositeSessionRepository;
|
||||
this.eventKeyService = eventKeyService;
|
||||
this.eventSorter = eventSorter;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -32,14 +48,18 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
|
||||
@Override
|
||||
public List<EventHubEventDto> loadDriverEvents(UnifiedRuntimeProcessingRequest request) {
|
||||
return driverEventSourceService.loadDriverEvents(
|
||||
List<EventHubEventDto> result = new ArrayList<>();
|
||||
for (UUID sessionId : resolveSessionIds(request)) {
|
||||
result.addAll(driverEventSourceService.loadDriverEvents(
|
||||
UnifiedDriverEventsRequest.forTachographFileSession(
|
||||
request.sessionId(),
|
||||
sessionId,
|
||||
request.driverKey(),
|
||||
request.occurredFrom(),
|
||||
request.occurredTo()
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
return deduplicateBySignatureAndSort(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -47,9 +67,11 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
UnifiedRuntimeProcessingRequest request,
|
||||
UnifiedDiscoveredVehicleRef vehicleRef
|
||||
) {
|
||||
return vehicleEventSourceService.loadVehicleEvents(
|
||||
List<EventHubEventDto> result = new ArrayList<>();
|
||||
for (UUID sessionId : resolveSessionIds(request)) {
|
||||
result.addAll(vehicleEventSourceService.loadVehicleEvents(
|
||||
UnifiedVehicleEventsRequest.forTachographFileSession(
|
||||
request.sessionId(),
|
||||
sessionId,
|
||||
vehicleRef.sourceVehicleEntityId(),
|
||||
vehicleRef.vin(),
|
||||
vehicleRef.registrationNation(),
|
||||
|
|
@ -57,6 +79,25 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
request.vehicleOccurredFrom(),
|
||||
request.vehicleOccurredTo()
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
return deduplicateBySignatureAndSort(result);
|
||||
}
|
||||
|
||||
private List<UUID> resolveSessionIds(UnifiedRuntimeProcessingRequest request) {
|
||||
if (request.compositeSessionId() != null) {
|
||||
return compositeSessionRepository.find(request.compositeSessionId())
|
||||
.orElseThrow(() -> new TachographCompositeSessionNotFoundException(request.compositeSessionId()))
|
||||
.memberSessionIds();
|
||||
}
|
||||
return request.sessionIds();
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> deduplicateBySignatureAndSort(List<EventHubEventDto> events) {
|
||||
LinkedHashMap<String, EventHubEventDto> bySignature = new LinkedHashMap<>();
|
||||
for (EventHubEventDto event : events) {
|
||||
bySignature.putIfAbsent(eventKeyService.buildEventSignatureHash(event), event);
|
||||
}
|
||||
return eventSorter.sort(new ArrayList<>(bySignature.values()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ public class TachographFileSessionUnifiedDriverEventSource implements UnifiedDri
|
|||
public List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request) {
|
||||
TachographFileSession session = repository.find(request.sessionId())
|
||||
.orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId()));
|
||||
if (request.driverKey() == null) {
|
||||
return session.driversByKey().values().stream()
|
||||
.flatMap(driver -> eventBuilder.buildEvents(session, driver).stream())
|
||||
.filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo()))
|
||||
.toList();
|
||||
}
|
||||
DriverExtractionSession driver = session.driversByKey().get(request.driverKey());
|
||||
if (driver == null) {
|
||||
throw new DriverNotFoundInSessionException(request.sessionId(), request.driverKey());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,472 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UnifiedRuntimeDerivedProjectionService {
|
||||
|
||||
private final UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService;
|
||||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||
private final TachographEsperProcessingCore esperProcessingCore;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionService(
|
||||
UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this(
|
||||
runtimeEventAssemblyService,
|
||||
timelineReconstructor,
|
||||
driverTimelineBuilder,
|
||||
reusableProjectionBuilder,
|
||||
properties,
|
||||
new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)
|
||||
);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public UnifiedRuntimeDerivedProjectionService(
|
||||
UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventHubProperties properties,
|
||||
TachographEsperProcessingCore esperProcessingCore
|
||||
) {
|
||||
this.runtimeEventAssemblyService = runtimeEventAssemblyService;
|
||||
this.timelineReconstructor = timelineReconstructor;
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||
this.properties = properties;
|
||||
this.esperProcessingCore = esperProcessingCore;
|
||||
}
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto loadDriverDerivedProjections(
|
||||
UnifiedRuntimeProcessingApiRequest apiRequest
|
||||
) {
|
||||
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
|
||||
UnifiedRuntimeEventBundle eventBundle = runtimeEventAssemblyService.assembleDriverScopedEvents(request);
|
||||
return buildDriverDerivedProjection(apiRequest, request, eventBundle, null);
|
||||
}
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto buildDriverDerivedProjection(
|
||||
UnifiedRuntimeProcessingApiRequest apiRequest,
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
UnifiedRuntimeEventBundle eventBundle,
|
||||
String explicitDriverKey
|
||||
) {
|
||||
String driverKey = explicitDriverKey == null
|
||||
? resolveDriverKey(request, eventBundle.mergedEvents())
|
||||
: explicitDriverKey;
|
||||
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(
|
||||
runtimeSessionId(request),
|
||||
driverKey,
|
||||
eventBundle.mergedEvents()
|
||||
);
|
||||
|
||||
OffsetDateTime requestedFrom = apiRequest.occurredFrom() == null
|
||||
? timeline.loadedFrom()
|
||||
: utc(apiRequest.occurredFrom());
|
||||
OffsetDateTime requestedTo = apiRequest.occurredTo() == null
|
||||
? timeline.loadedTo()
|
||||
: utc(apiRequest.occurredTo());
|
||||
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
|
||||
}
|
||||
|
||||
int significantDrivingMinutes = apiRequest.significantDrivingMinutes() == null
|
||||
? processingProperties().getSignificantDrivingMinutes()
|
||||
: Math.max(1, apiRequest.significantDrivingMinutes());
|
||||
int minimumRestPeriodMinutes = apiRequest.minimumRestPeriodMinutes() == null
|
||||
? processingProperties().getMinimumRestPeriodMinutes()
|
||||
: Math.max(1, apiRequest.minimumRestPeriodMinutes());
|
||||
|
||||
List<String> notes = new ArrayList<>(eventBundle.notes());
|
||||
notes.add("Runtime derived projections were evaluated from the unified merged event stream using the shared tachograph Esper processing core.");
|
||||
notes.add("Significant driving threshold minutes: " + significantDrivingMinutes + ".");
|
||||
notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + ".");
|
||||
if (request.occurredFrom() != null || request.occurredTo() != null) {
|
||||
notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request.");
|
||||
}
|
||||
|
||||
TachographEsperDriverProcessingResultDto projection = esperProcessingCore.process(TachographEsperProcessingInput.fromEvents(
|
||||
runtimeSessionId(request),
|
||||
driverKey,
|
||||
timeline,
|
||||
eventBundle.mergedEvents(),
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes,
|
||||
notes
|
||||
));
|
||||
notes = projection.notes();
|
||||
|
||||
return new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
request,
|
||||
eventBundle.driverSeedEvents().size(),
|
||||
eventBundle.discoveredVehicles().size(),
|
||||
eventBundle.expandedVehicleEvents().size(),
|
||||
eventBundle.mergedEvents().size(),
|
||||
eventBundle.discoveredVehicles(),
|
||||
projection,
|
||||
notes
|
||||
);
|
||||
}
|
||||
|
||||
private EventHubProperties.Processing processingProperties() {
|
||||
return properties.getTachographFileSession().getProcessing();
|
||||
}
|
||||
|
||||
private UUID runtimeSessionId(UnifiedRuntimeProcessingRequest request) {
|
||||
if (request.compositeSessionId() != null || request.sessionIds().size() > 1) {
|
||||
return null;
|
||||
}
|
||||
return request.sessionIds().size() == 1 ? request.sessionIds().get(0) : request.sessionId();
|
||||
}
|
||||
|
||||
private String resolveDriverKey(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
if (request.driverKey() != null) {
|
||||
return request.driverKey();
|
||||
}
|
||||
if (request.driverSourceEntityId() != null) {
|
||||
return request.driverSourceEntityId();
|
||||
}
|
||||
for (EventHubEventDto event : events) {
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
if (driverRef != null && driverRef.hasAnyReference()) {
|
||||
return driverRef.stableKey();
|
||||
}
|
||||
}
|
||||
if (request.driverCardNation() != null && request.driverCardNumber() != null) {
|
||||
return request.driverCardNation() + ":" + request.driverCardNumber();
|
||||
}
|
||||
return request.driverCardNumber();
|
||||
}
|
||||
|
||||
|
||||
private List<TachographEsperVehicleUsageIntervalEvent> mergeVehicleUsageIntervals(
|
||||
List<TachographEsperVehicleUsageIntervalEvent> intervals
|
||||
) {
|
||||
if (intervals == null || intervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<TachographEsperVehicleUsageIntervalEvent> sorted = intervals.stream()
|
||||
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
List<TachographEsperVehicleUsageIntervalEvent> merged = new ArrayList<>();
|
||||
for (TachographEsperVehicleUsageIntervalEvent next : sorted) {
|
||||
if (merged.isEmpty()) {
|
||||
merged.add(next);
|
||||
continue;
|
||||
}
|
||||
TachographEsperVehicleUsageIntervalEvent current = merged.get(merged.size() - 1);
|
||||
if (canMergeVehicleUsage(current, next)) {
|
||||
merged.set(merged.size() - 1, mergeVehicleUsage(current, next));
|
||||
} else {
|
||||
merged.add(next);
|
||||
}
|
||||
}
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
private boolean canMergeVehicleUsage(
|
||||
TachographEsperVehicleUsageIntervalEvent left,
|
||||
TachographEsperVehicleUsageIntervalEvent right
|
||||
) {
|
||||
if (left == null || right == null || left.endedAt() == null || right.startedAt() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(left.driverKey(), right.driverKey())
|
||||
&& Objects.equals(left.registrationKey(), right.registrationKey())
|
||||
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
|
||||
&& !right.startedAt().isAfter(left.endedAt().plusSeconds(1));
|
||||
}
|
||||
|
||||
private TachographEsperVehicleUsageIntervalEvent mergeVehicleUsage(
|
||||
TachographEsperVehicleUsageIntervalEvent left,
|
||||
TachographEsperVehicleUsageIntervalEvent right
|
||||
) {
|
||||
List<String> sourceIntervalIds = new ArrayList<>();
|
||||
if (left.sourceIntervalIds() != null) {
|
||||
sourceIntervalIds.addAll(left.sourceIntervalIds());
|
||||
}
|
||||
if (right.sourceIntervalIds() != null) {
|
||||
for (String sourceIntervalId : right.sourceIntervalIds()) {
|
||||
if (!sourceIntervalIds.contains(sourceIntervalId)) {
|
||||
sourceIntervalIds.add(sourceIntervalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
OffsetDateTime end = right.endedAt() == null || right.endedAt().isBefore(left.endedAt())
|
||||
? left.endedAt()
|
||||
: right.endedAt();
|
||||
return new TachographEsperVehicleUsageIntervalEvent(
|
||||
left.sessionId(),
|
||||
left.driverKey(),
|
||||
left.intervalId(),
|
||||
left.startedAt(),
|
||||
end,
|
||||
Duration.between(left.startedAt(), end).getSeconds(),
|
||||
left.odometerBeginKm(),
|
||||
right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
|
||||
left.registrationKey(),
|
||||
left.vehicleKey(),
|
||||
left.sourceKind(),
|
||||
sourceIntervalIds
|
||||
);
|
||||
}
|
||||
|
||||
private List<TachographEsperActivityIntervalEvent> clipActivityIntervals(
|
||||
List<TachographEsperActivityIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return intervals == null ? List.of() : List.copyOf(intervals);
|
||||
}
|
||||
return (intervals == null ? List.<TachographEsperActivityIntervalEvent>of() : intervals).stream()
|
||||
.map(interval -> {
|
||||
if (!intersects(interval.startedAt(), interval.endedAt(), requestedFrom, requestedTo)) {
|
||||
return null;
|
||||
}
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (start == null || end == null || !end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
boolean clipped = interval.clippedToRequestedPeriod()
|
||||
|| !Objects.equals(start, interval.startedAt())
|
||||
|| !Objects.equals(end, interval.endedAt());
|
||||
return new TachographEsperActivityIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
interval.intervalId(),
|
||||
interval.activityType(),
|
||||
interval.cardSlot(),
|
||||
interval.cardStatus(),
|
||||
interval.drivingStatus(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
interval.sourceIntervalIds(),
|
||||
interval.synthetic(),
|
||||
clipped,
|
||||
interval.level()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperActivityIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperVehicleUsageIntervalEvent> clipVehicleUsageIntervals(
|
||||
List<TachographEsperVehicleUsageIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return intervals == null ? List.of() : List.copyOf(intervals);
|
||||
}
|
||||
return (intervals == null ? List.<TachographEsperVehicleUsageIntervalEvent>of() : intervals).stream()
|
||||
.map(interval -> {
|
||||
if (!intersects(interval.startedAt(), interval.endedAt(), requestedFrom, requestedTo)) {
|
||||
return null;
|
||||
}
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (start == null || end == null || !end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
boolean startClipped = !Objects.equals(start, interval.startedAt());
|
||||
boolean endClipped = !Objects.equals(end, interval.endedAt());
|
||||
return new TachographEsperVehicleUsageIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
interval.intervalId(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
startClipped ? null : interval.odometerBeginKm(),
|
||||
endClipped ? null : interval.odometerEndKm(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind(),
|
||||
interval.sourceIntervalIds()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperDrivingInterruptionIntervalEvent> clipDrivingIntervals(
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
return filterIntersecting(
|
||||
intervals,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
TachographEsperDrivingInterruptionIntervalEvent::startedAt,
|
||||
TachographEsperDrivingInterruptionIntervalEvent::endedAt
|
||||
);
|
||||
}
|
||||
|
||||
private List<TachographEsperVuCardAbsentIntervalEvent> clipVuCardAbsentIntervals(
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
return filterIntersecting(
|
||||
intervals,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
TachographEsperVuCardAbsentIntervalEvent::startedAt,
|
||||
TachographEsperVuCardAbsentIntervalEvent::endedAt
|
||||
);
|
||||
}
|
||||
|
||||
private List<TachographEsperSupportGeoEvent> clipSupportGeoEvents(
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
String driverKey,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
return (supportEvents == null ? List.<ExtractedSupportEvent>of() : supportEvents).stream()
|
||||
.filter(event -> event.occurredAt() != null)
|
||||
.filter(event -> driverKey == null || event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
|
||||
.filter(event -> requestedFrom == null || !event.occurredAt().isBefore(requestedFrom))
|
||||
.filter(event -> requestedTo == null || !event.occurredAt().isAfter(requestedTo))
|
||||
.map(event -> new TachographEsperSupportGeoEvent(
|
||||
event.eventId(),
|
||||
event.driverKey(),
|
||||
event.occurredAt(),
|
||||
event.eventDomain(),
|
||||
event.eventType(),
|
||||
event.eventLifecycle(),
|
||||
event.registrationKey(),
|
||||
event.vehicleKey(),
|
||||
event.country(),
|
||||
event.region(),
|
||||
event.countryFrom(),
|
||||
event.countryTo(),
|
||||
event.operation(),
|
||||
event.latitude(),
|
||||
event.longitude(),
|
||||
event.odometerKm(),
|
||||
event.rawRecordPath()
|
||||
))
|
||||
.sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
|
||||
.thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private <T> List<T> filterIntersecting(
|
||||
List<T> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo,
|
||||
TimeAccessor<T> startAccessor,
|
||||
TimeAccessor<T> endAccessor
|
||||
) {
|
||||
if (intervals == null || intervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.copyOf(intervals);
|
||||
}
|
||||
return intervals.stream()
|
||||
.filter(interval -> intersects(startAccessor.get(interval), endAccessor.get(interval), requestedFrom, requestedTo))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean intersects(
|
||||
OffsetDateTime intervalStart,
|
||||
OffsetDateTime intervalEnd,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (intervalStart == null || intervalEnd == null || requestedFrom == null || requestedTo == null) {
|
||||
return false;
|
||||
}
|
||||
return intervalEnd.isAfter(requestedFrom) && intervalStart.isBefore(requestedTo);
|
||||
}
|
||||
|
||||
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isBefore(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isAfter(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime utc(OffsetDateTime value) {
|
||||
return value == null ? null : value.withOffsetSameInstant(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TimeAccessor<T> {
|
||||
OffsetDateTime get(T value);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,15 @@ public class UnifiedRuntimeEventAssemblyService {
|
|||
notes.add(request.eventBackend() == UnifiedRuntimeEventBackend.EVENTHUB_DB
|
||||
? "Driver seed events were loaded from the local EventHub event store."
|
||||
: "Driver seed events were loaded directly from the selected runtime sources.");
|
||||
if (request.sourceFamilies().contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION)) {
|
||||
if (request.compositeSessionId() != null) {
|
||||
notes.add("Tachograph file-session events were loaded from composite session " + request.compositeSessionId() + ".");
|
||||
} else if (request.sessionIds().size() > 1) {
|
||||
notes.add("Tachograph file-session events were loaded from " + request.sessionIds().size() + " selected sessions.");
|
||||
} else if (request.sessionId() != null) {
|
||||
notes.add("Tachograph file-session events were loaded from session " + request.sessionId() + ".");
|
||||
}
|
||||
}
|
||||
if (request.expandVehicleEvents()) {
|
||||
notes.add("Vehicle expansion loaded additional events for vehicles discovered in the driver seed set.");
|
||||
notes.add("Vehicle expansion padding minutes: " + request.vehicleExpansionPaddingMinutes() + ".");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UnifiedRuntimeTachographEsperScopeProcessingService {
|
||||
|
||||
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
|
||||
private final UnifiedRuntimeDerivedProjectionService derivedProjectionService;
|
||||
private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeProcessingService(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService,
|
||||
RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
|
||||
) {
|
||||
this.eventAssemblyService = eventAssemblyService;
|
||||
this.derivedProjectionService = derivedProjectionService;
|
||||
this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
|
||||
}
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
|
||||
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
|
||||
UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request);
|
||||
LinkedHashSet<String> selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents());
|
||||
if (selectedDriverKeys.isEmpty()) {
|
||||
throw new IllegalArgumentException("No driver partitions could be resolved from the runtime event scope.");
|
||||
}
|
||||
|
||||
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>();
|
||||
Map<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
for (String driverKey : selectedDriverKeys) {
|
||||
UnifiedRuntimeEventBundle driverBundle = partitionForDriver(request, broadBundle, driverKey);
|
||||
for (EventHubEventDto attachedEvent : driverBundle.expandedVehicleEvents()) {
|
||||
attachedVehicleEvidenceByEvent
|
||||
.computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>())
|
||||
.add(driverKey);
|
||||
}
|
||||
driverBundle.notes().stream()
|
||||
.filter(note -> note.startsWith("WARNING:"))
|
||||
.map(note -> note.substring("WARNING:".length()).trim())
|
||||
.forEach(warnings::add);
|
||||
if (driverBundle.mergedEvents().isEmpty()) {
|
||||
warnings.add("No events remained after partitioning runtime scope for driver " + driverKey + ".");
|
||||
continue;
|
||||
}
|
||||
UnifiedRuntimeProcessingRequest driverRequest = request.withDriverKey(driverKey);
|
||||
UnifiedRuntimeDerivedProjectionResultDto driverResult = derivedProjectionService.buildDriverDerivedProjection(
|
||||
apiRequest,
|
||||
driverRequest,
|
||||
driverBundle,
|
||||
driverKey
|
||||
);
|
||||
driverResults.put(driverKey, driverResult);
|
||||
}
|
||||
attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> {
|
||||
if (drivers.size() > 1) {
|
||||
warnings.add("Vehicle-only event " + eventKey + " was attached to multiple driver partitions "
|
||||
+ drivers + "; check overlapping vehicle-usage intervals.");
|
||||
}
|
||||
});
|
||||
|
||||
List<String> notes = new ArrayList<>(broadBundle.notes());
|
||||
notes.add("Runtime tachograph Esper scope processing used Java-side driver partitioning before calling the common event-input Esper pipeline.");
|
||||
notes.add("Selected driver partitions: " + selectedDriverKeys.size() + ".");
|
||||
if (!request.includeAllDrivers() && !request.driverKeys().isEmpty()) {
|
||||
notes.add("The broad runtime event set was filtered to the requested driverKeys.");
|
||||
}
|
||||
if (!request.vehicleKeys().isEmpty() || request.includeAllVehicles()) {
|
||||
notes.add("vehicleKeys/includeAllVehicles are accepted in the request model for source-neutral scopes; driver partitions are enriched only with vehicle evidence that overlaps reconstructed driver vehicle-usage intervals.");
|
||||
}
|
||||
notes.add("Vehicle-only evidence attachment is controlled by expandVehicleEvents/attachVehicleOnlyEvents and vehicleExpansionPaddingMinutes/vehicleEvidencePaddingMinutes.");
|
||||
|
||||
return new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
request,
|
||||
broadBundle.mergedEvents().size(),
|
||||
driverResults.size(),
|
||||
broadBundle.discoveredVehicles().size(),
|
||||
broadBundle.discoveredVehicles(),
|
||||
driverResults,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> selectedDriverKeys(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
LinkedHashSet<String> allDrivers = discoverDriverKeys(events);
|
||||
if (request.includeAllDrivers()) {
|
||||
return allDrivers;
|
||||
}
|
||||
LinkedHashSet<String> requested = new LinkedHashSet<>(request.driverKeys());
|
||||
if (request.driverKey() != null) {
|
||||
requested.add(request.driverKey());
|
||||
}
|
||||
if (requested.isEmpty()) {
|
||||
return allDrivers;
|
||||
}
|
||||
LinkedHashSet<String> selected = new LinkedHashSet<>();
|
||||
for (String driverKey : allDrivers) {
|
||||
if (requested.contains(driverKey)) {
|
||||
selected.add(driverKey);
|
||||
}
|
||||
}
|
||||
for (String driverKey : requested) {
|
||||
selected.add(driverKey);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private UnifiedRuntimeEventBundle partitionForDriver(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
UnifiedRuntimeEventBundle broadBundle,
|
||||
String driverKey
|
||||
) {
|
||||
List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
|
||||
.filter(event -> Objects.equals(driverKey(event), driverKey))
|
||||
.toList();
|
||||
RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
|
||||
driverKey,
|
||||
directDriverEvents,
|
||||
broadBundle.mergedEvents(),
|
||||
request.expandVehicleEvents(),
|
||||
request.vehicleExpansionPaddingMinutes()
|
||||
);
|
||||
List<UnifiedDiscoveredVehicleRef> driverVehicles = discoverVehicles(attachmentResult.mergedEvents());
|
||||
List<String> notes = new ArrayList<>(broadBundle.notes());
|
||||
notes.add("Partitioned mixed runtime event scope for driver " + driverKey + ".");
|
||||
notes.add("Driver direct events: " + attachmentResult.directDriverEvents().size() + ".");
|
||||
notes.add("Vehicle-only evidence events attached to driver partition: " + attachmentResult.attachedVehicleEvidenceEvents().size() + ".");
|
||||
notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + ".");
|
||||
notes.addAll(attachmentResult.notes());
|
||||
attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning));
|
||||
return new UnifiedRuntimeEventBundle(
|
||||
request.withDriverKey(driverKey),
|
||||
attachmentResult.directDriverEvents(),
|
||||
driverVehicles,
|
||||
attachmentResult.attachedVehicleEvidenceEvents(),
|
||||
attachmentResult.mergedEvents(),
|
||||
notes
|
||||
);
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> discoverDriverKeys(List<EventHubEventDto> events) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
for (EventHubEventDto event : sort(events)) {
|
||||
String driverKey = driverKey(event);
|
||||
if (driverKey != null) {
|
||||
result.add(driverKey);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
|
||||
List<UnifiedDiscoveredVehicleRef> result = new ArrayList<>();
|
||||
for (EventHubEventDto event : events) {
|
||||
UnifiedDiscoveredVehicleRef candidate = vehicleRef(event.vehicleRef());
|
||||
if (candidate == null || !candidate.hasAnyReference()) {
|
||||
continue;
|
||||
}
|
||||
boolean merged = false;
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
UnifiedDiscoveredVehicleRef existing = result.get(i);
|
||||
if (existing.matches(candidate)) {
|
||||
result.set(i, existing.merge(candidate));
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!merged) {
|
||||
result.add(candidate);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(UnifiedDiscoveredVehicleRef::stableKey));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private boolean matchesAnyVehicle(VehicleRefDto vehicleRef, List<UnifiedDiscoveredVehicleRef> vehicles) {
|
||||
UnifiedDiscoveredVehicleRef candidate = vehicleRef(vehicleRef);
|
||||
if (candidate == null || !candidate.hasAnyReference()) {
|
||||
return false;
|
||||
}
|
||||
return vehicles.stream().anyMatch(vehicle -> vehicle.matches(candidate));
|
||||
}
|
||||
|
||||
private UnifiedDiscoveredVehicleRef vehicleRef(VehicleRefDto vehicleRef) {
|
||||
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
|
||||
return null;
|
||||
}
|
||||
return new UnifiedDiscoveredVehicleRef(
|
||||
vehicleRef.sourceVehicleEntityId(),
|
||||
vehicleRef.vin(),
|
||||
vehicleRef.vehicleRegistration() == null
|
||||
? null
|
||||
: vehicleRef.vehicleRegistration().nationNumericCode() == null
|
||||
? vehicleRef.vehicleRegistration().nation()
|
||||
: vehicleRef.vehicleRegistration().nationNumericCode().toString(),
|
||||
vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number()
|
||||
);
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> deduplicateAndSort(
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> vehicleEvidenceEvents
|
||||
) {
|
||||
LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
|
||||
appendDeduplicated(byKey, directDriverEvents);
|
||||
appendDeduplicated(byKey, vehicleEvidenceEvents);
|
||||
return sort(new ArrayList<>(byKey.values()));
|
||||
}
|
||||
|
||||
private void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
|
||||
for (EventHubEventDto event : events) {
|
||||
byKey.putIfAbsent(dedupKey(event), event);
|
||||
}
|
||||
}
|
||||
|
||||
private String dedupKey(EventHubEventDto event) {
|
||||
String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null
|
||||
? event.packageInfo().eventSource().stableKey()
|
||||
: "NO_SOURCE";
|
||||
return sourceKey + "|" + event.externalSourceEventId();
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> sort(List<EventHubEventDto> events) {
|
||||
return (events == null ? List.<EventHubEventDto>of() : events).stream()
|
||||
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
|
||||
.thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
|
||||
.thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
|
||||
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String driverKey(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return null;
|
||||
}
|
||||
String rawDriverKey = text(rawPayload(event), "driverKey");
|
||||
if (rawDriverKey != null) {
|
||||
return rawDriverKey;
|
||||
}
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
if (driverRef != null && driverRef.hasAnyReference()) {
|
||||
return driverRef.stableKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -61,26 +61,23 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder;
|
||||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public DriverTimelineReusableProjectionBuilder(
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this(driverTimelineBuilder, null, new UnifiedEventTimelineReconstructor(), properties);
|
||||
this(driverTimelineBuilder, null, properties);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public DriverTimelineReusableProjectionBuilder(
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
RawSourceDriverTimelineEventBuilder rawSourceEventBuilder,
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.rawSourceEventBuilder = rawSourceEventBuilder;
|
||||
this.timelineReconstructor = timelineReconstructor;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
|
|
@ -325,20 +322,17 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
String fallbackDriverKey,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor = new UnifiedEventTimelineReconstructor();
|
||||
ResolvedDriverTimeline reconstructed = timelineReconstructor.reconstruct(
|
||||
fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId,
|
||||
fallbackDriverKey,
|
||||
safeList(events)
|
||||
);
|
||||
List<ResolvedVehicleUsageInterval> mergedVehicleUsageIntervals = mergeVehicleUsageIntervals(
|
||||
reconstructed.vehicleUsageIntervals(),
|
||||
reconstructed.sourceKind()
|
||||
);
|
||||
return new ResolvedDriverTimeline(
|
||||
reconstructed.sourceKind(),
|
||||
reconstructed.loadedFrom(),
|
||||
reconstructed.loadedTo(),
|
||||
mergedVehicleUsageIntervals,
|
||||
mergeVehicleUsageIntervals(reconstructed.vehicleUsageIntervals(), reconstructed.sourceKind()),
|
||||
reconstructed.activityIntervals(),
|
||||
reconstructed.supportEvents(),
|
||||
reconstructed.warnings()
|
||||
|
|
@ -411,7 +405,105 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
throw new UnsupportedOperationException("Direct EPL point-input preprocessing is currently disabled.");
|
||||
if ((activityPointInputEvents == null || activityPointInputEvents.isEmpty())
|
||||
&& (vehicleUsagePointInputEvents == null || vehicleUsagePointInputEvents.isEmpty())) {
|
||||
return emptyBundle();
|
||||
}
|
||||
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = new ArrayList<>();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals = new ArrayList<>();
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>();
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals = new ArrayList<>();
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = new ArrayList<>();
|
||||
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals = new ArrayList<>();
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals = new ArrayList<>();
|
||||
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals = new ArrayList<>();
|
||||
|
||||
executeWithRuntime(
|
||||
configuration -> {
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographActivityPointInputEvent",
|
||||
activityPointInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographVehicleUsagePointInputEvent",
|
||||
vehicleUsagePointInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographProjectionFinalizeEvent",
|
||||
projectionFinalizeInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographActivityIntervalInputEvent",
|
||||
activityIntervalInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographVehicleUsageIntervalInputEvent",
|
||||
vehicleUsageIntervalInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographSupportGeoEvidenceInputEvent",
|
||||
supportGeoEvidenceInputDefinition()
|
||||
);
|
||||
},
|
||||
renderDrivingDerivedProjectionEventsEpl(significantDrivingMinutes, minimumRestPeriodMinutes),
|
||||
Map.of(
|
||||
"drivingInterruptionIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionIntervals),
|
||||
"dailyWeeklyRestCandidateIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, dailyWeeklyRestCandidateIntervals),
|
||||
"dailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, dailyWeeklyRestCandidateCoverageIntervals),
|
||||
"unclassifiedDailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, unclassifiedDailyWeeklyRestCandidateCoverageIntervals),
|
||||
"drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals),
|
||||
"vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals),
|
||||
"potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals),
|
||||
"potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals),
|
||||
"potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals)
|
||||
),
|
||||
runtime -> {
|
||||
if (supportGeoInputEvents != null) {
|
||||
for (Map<String, Object> supportGeoEvidence : supportGeoInputEvents) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
supportGeoEvidence,
|
||||
"TachographSupportGeoEvidenceInputEvent"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (vehicleUsagePointInputEvents != null) {
|
||||
for (Map<String, Object> point : vehicleUsagePointInputEvents) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
point,
|
||||
"TachographVehicleUsagePointInputEvent"
|
||||
);
|
||||
}
|
||||
for (Map<String, Object> finalizeEvent : buildProjectionFinalizeEvents(vehicleUsagePointInputEvents)) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
finalizeEvent,
|
||||
"TachographProjectionFinalizeEvent"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (activityPointInputEvents != null) {
|
||||
for (Map<String, Object> point : activityPointInputEvents) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
point,
|
||||
"TachographActivityPointInputEvent"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return new TachographEsperDrivingDerivedProjectionBundle(
|
||||
sortDrivingInterruptionIntervals(drivingInterruptionIntervals),
|
||||
sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals),
|
||||
sortDailyWeeklyRestCandidateCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals),
|
||||
sortDailyWeeklyRestCandidateCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals),
|
||||
sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals),
|
||||
sortVuCardAbsentIntervals(vuCardAbsentIntervals),
|
||||
sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals),
|
||||
sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals),
|
||||
sortPotentialInVehicleTripIntervals(potentialInVehicleTripIntervals)
|
||||
);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildProjectionFinalizeEvents(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,760 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.*;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class TachographEsperProcessingCore {
|
||||
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public TachographEsperProcessingCore(
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
|
||||
Objects.requireNonNull(input, "input must not be null");
|
||||
ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null");
|
||||
String driverKey = input.driverKey();
|
||||
OffsetDateTime requestedFrom = input.requestedFrom() == null ? timeline.loadedFrom() : utc(input.requestedFrom());
|
||||
OffsetDateTime requestedTo = input.requestedTo() == null ? timeline.loadedTo() : utc(input.requestedTo());
|
||||
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
|
||||
}
|
||||
int significantDrivingMinutes = Math.max(1, input.significantDrivingMinutes());
|
||||
int minimumRestPeriodMinutes = Math.max(1, input.minimumRestPeriodMinutes());
|
||||
|
||||
List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
|
||||
driverTimelineBuilder.buildEsperActivityIntervalEvents(input.sessionId(), driverKey, timeline),
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
|
||||
driverTimelineBuilder.buildEsperDrivingIntervalEvents(input.sessionId(), driverKey, timeline),
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
|
||||
TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle = buildDerivedProjection(
|
||||
input,
|
||||
timeline,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
|
||||
derivedProjectionBundle.drivingInterruptionIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionIntervals, requestedFrom, requestedTo);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
|
||||
derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(rawDailyWeeklyRestCandidateIntervals, requestedFrom, requestedTo);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
|
||||
derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(rawDrivingInterruptionVehicleChangeIntervals, requestedFrom, requestedTo);
|
||||
|
||||
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals =
|
||||
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
|
||||
derivedProjectionBundle.vuCardAbsentIntervals();
|
||||
|
||||
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
|
||||
clipEsperPotentialHomeOvernightStayIntervalEvents(
|
||||
derivedProjectionBundle.potentialHomeOvernightStayIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals =
|
||||
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
|
||||
derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals =
|
||||
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
|
||||
derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals =
|
||||
clipEsperPotentialInVehicleOvernightStayIntervalEvents(
|
||||
derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals =
|
||||
clipEsperPotentialInVehicleTripIntervalEvents(
|
||||
derivedProjectionBundle.potentialInVehicleTripIntervals(),
|
||||
potentialInVehicleOvernightStayIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
|
||||
rawVuCardAbsentIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperSupportGeoEvent> supportGeoEvents = clipEsperSupportGeoEvents(
|
||||
timeline.supportEvents(),
|
||||
driverKey,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
|
||||
return new TachographEsperDriverProcessingResultDto(
|
||||
input.sessionId(),
|
||||
driverKey,
|
||||
timeline.sourceKind(),
|
||||
timeline.loadedFrom(),
|
||||
timeline.loadedTo(),
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
activityIntervals.size(),
|
||||
drivingIntervals.size(),
|
||||
drivingInterruptionIntervals.size(),
|
||||
drivingInterruptionVehicleChangeIntervals.size(),
|
||||
dailyWeeklyRestCandidateIntervals.size(),
|
||||
dailyWeeklyRestCandidateCoverageIntervals.size(),
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(),
|
||||
potentialHomeOvernightStayIntervals.size(),
|
||||
potentialInVehicleOvernightStayIntervals.size(),
|
||||
potentialInVehicleTripIntervals.size(),
|
||||
vehicleUsageIntervals.size(),
|
||||
vuCardAbsentIntervals.size(),
|
||||
supportGeoEvents.size(),
|
||||
activityIntervals,
|
||||
drivingIntervals,
|
||||
drivingInterruptionIntervals,
|
||||
drivingInterruptionVehicleChangeIntervals,
|
||||
dailyWeeklyRestCandidateIntervals,
|
||||
dailyWeeklyRestCandidateCoverageIntervals,
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
|
||||
potentialHomeOvernightStayIntervals,
|
||||
potentialInVehicleOvernightStayIntervals,
|
||||
potentialInVehicleTripIntervals,
|
||||
vehicleUsageIntervals,
|
||||
vuCardAbsentIntervals,
|
||||
supportGeoEvents,
|
||||
combinedNotes(input.notes())
|
||||
);
|
||||
}
|
||||
|
||||
private TachographEsperDrivingDerivedProjectionBundle buildDerivedProjection(
|
||||
TachographEsperProcessingInput input,
|
||||
ResolvedDriverTimeline timeline,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
if (input.forceEventInput() || input.hasEventInputEvents()) {
|
||||
return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(
|
||||
input.sessionId(),
|
||||
input.driverKey(),
|
||||
input.eventInputEvents(),
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
if (properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode()
|
||||
== EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS
|
||||
&& input.session() != null
|
||||
&& input.driverSession() != null) {
|
||||
return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
|
||||
input.session(),
|
||||
input.driverSession(),
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
return reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
|
||||
input.sessionId(),
|
||||
input.driverKey(),
|
||||
timeline,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> combinedNotes(List<String> extraNotes) {
|
||||
List<String> notes = new ArrayList<>();
|
||||
notes.addAll(esperProjectionNotes());
|
||||
if (extraNotes != null) {
|
||||
notes.addAll(extraNotes);
|
||||
}
|
||||
return List.copyOf(notes);
|
||||
}
|
||||
|
||||
|
||||
private List<TachographEsperActivityIntervalEvent> clipEsperActivityIntervalEvents(
|
||||
List<TachographEsperActivityIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
boolean clipped = interval.clippedToRequestedPeriod()
|
||||
|| !start.equals(interval.startedAt())
|
||||
|| !end.equals(interval.endedAt());
|
||||
return new TachographEsperActivityIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
interval.intervalId(),
|
||||
interval.activityType(),
|
||||
interval.cardSlot(),
|
||||
interval.cardStatus(),
|
||||
interval.drivingStatus(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
interval.sourceIntervalIds(),
|
||||
interval.synthetic(),
|
||||
clipped,
|
||||
interval.level()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperActivityIntervalEvent::endedAt)
|
||||
.thenComparing(TachographEsperActivityIntervalEvent::activityType, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperVehicleUsageIntervalEvent> clipEsperVehicleUsageIntervalEvents(
|
||||
List<TachographEsperVehicleUsageIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
boolean startClipped = !start.equals(interval.startedAt());
|
||||
boolean endClipped = !end.equals(interval.endedAt());
|
||||
return new TachographEsperVehicleUsageIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
interval.intervalId(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
startClipped ? null : interval.odometerBeginKm(),
|
||||
endClipped ? null : interval.odometerEndKm(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind(),
|
||||
interval.sourceIntervalIds()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt)
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperSupportGeoEvent> clipEsperSupportGeoEvents(
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
String driverKey,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (supportEvents == null || supportEvents.isEmpty() || requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return supportEvents.stream()
|
||||
.filter(event -> event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
|
||||
.filter(event -> event.occurredAt() != null)
|
||||
.filter(event -> event.latitude() != null && event.longitude() != null)
|
||||
.filter(event -> !event.occurredAt().isBefore(requestedFrom) && !event.occurredAt().isAfter(requestedTo))
|
||||
.map(event -> new TachographEsperSupportGeoEvent(
|
||||
event.eventId(),
|
||||
event.driverKey(),
|
||||
event.occurredAt(),
|
||||
event.eventDomain(),
|
||||
event.eventType(),
|
||||
event.eventLifecycle(),
|
||||
event.registrationKey(),
|
||||
event.vehicleKey(),
|
||||
event.country(),
|
||||
event.region(),
|
||||
event.countryFrom(),
|
||||
event.countryTo(),
|
||||
event.operation(),
|
||||
event.latitude(),
|
||||
event.longitude(),
|
||||
event.odometerKm(),
|
||||
event.rawRecordPath()
|
||||
))
|
||||
.sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
|
||||
.thenComparing(TachographEsperSupportGeoEvent::eventDomain, Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperDrivingInterruptionIntervalEvent> clipEsperDrivingInterruptionIntervalEvents(
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
return new TachographEsperDrivingInterruptionIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
interval.previousDrivingSourceIntervalId(),
|
||||
interval.nextDrivingSourceIntervalId(),
|
||||
interval.previousRegistrationKey(),
|
||||
interval.nextRegistrationKey(),
|
||||
interval.previousVehicleKey(),
|
||||
interval.nextVehicleKey()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperDrivingInterruptionIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperDrivingInterruptionIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperVuCardAbsentIntervalEvent> clipEsperVuCardAbsentIntervalEvents(
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
return new TachographEsperVuCardAbsentIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
interval.previousUsageIntervalId(),
|
||||
interval.nextUsageIntervalId(),
|
||||
interval.previousRegistrationKey(),
|
||||
interval.nextRegistrationKey(),
|
||||
interval.previousVehicleKey(),
|
||||
interval.nextVehicleKey()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperVuCardAbsentIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperVuCardAbsentIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> intervals,
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
|
||||
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
long durationSeconds = Duration.between(start, end).getSeconds();
|
||||
boolean beginBoundaryChanged = !start.equals(interval.startedAt());
|
||||
boolean endBoundaryChanged = !end.equals(interval.endedAt());
|
||||
return new TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
start,
|
||||
end,
|
||||
durationSeconds,
|
||||
interval.cardAbsentDurationSeconds(),
|
||||
interval.cardAbsentCoveragePercent(),
|
||||
interval.previousDrivingSourceIntervalId(),
|
||||
interval.nextDrivingSourceIntervalId(),
|
||||
interval.previousRegistrationKey(),
|
||||
interval.nextRegistrationKey(),
|
||||
interval.previousVehicleKey(),
|
||||
interval.nextVehicleKey(),
|
||||
beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(),
|
||||
endBoundaryChanged ? null : interval.endBoundaryOdometerKm(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoEventId(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
|
||||
beginBoundaryChanged ? null : interval.beginLatitude(),
|
||||
beginBoundaryChanged ? null : interval.beginLongitude(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
|
||||
endBoundaryChanged ? null : interval.endGeoEventId(),
|
||||
endBoundaryChanged ? null : interval.endGeoEventDomain(),
|
||||
endBoundaryChanged ? null : interval.endGeoOccurredAt(),
|
||||
endBoundaryChanged ? null : interval.endLatitude(),
|
||||
endBoundaryChanged ? null : interval.endLongitude(),
|
||||
endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
|
||||
endBoundaryChanged ? null : interval.endGeoOdometerKm(),
|
||||
beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
|
||||
beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
|
||||
beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
|
||||
endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperPotentialHomeOvernightStayIntervalEvent> clipEsperPotentialHomeOvernightStayIntervalEvents(
|
||||
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> intervals,
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
|
||||
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
long durationSeconds = Duration.between(start, end).getSeconds();
|
||||
boolean beginBoundaryChanged = !start.equals(interval.startedAt());
|
||||
boolean endBoundaryChanged = !end.equals(interval.endedAt());
|
||||
return new TachographEsperPotentialHomeOvernightStayIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
start,
|
||||
end,
|
||||
durationSeconds,
|
||||
interval.cardAbsentDurationSeconds(),
|
||||
interval.cardAbsentCoveragePercent(),
|
||||
interval.previousDrivingSourceIntervalId(),
|
||||
interval.nextDrivingSourceIntervalId(),
|
||||
interval.previousRegistrationKey(),
|
||||
interval.nextRegistrationKey(),
|
||||
interval.previousVehicleKey(),
|
||||
interval.nextVehicleKey(),
|
||||
beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(),
|
||||
endBoundaryChanged ? null : interval.endBoundaryOdometerKm(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoEventId(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
|
||||
beginBoundaryChanged ? null : interval.beginLatitude(),
|
||||
beginBoundaryChanged ? null : interval.beginLongitude(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
|
||||
endBoundaryChanged ? null : interval.endGeoEventId(),
|
||||
endBoundaryChanged ? null : interval.endGeoEventDomain(),
|
||||
endBoundaryChanged ? null : interval.endGeoOccurredAt(),
|
||||
endBoundaryChanged ? null : interval.endLatitude(),
|
||||
endBoundaryChanged ? null : interval.endLongitude(),
|
||||
endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
|
||||
endBoundaryChanged ? null : interval.endGeoOdometerKm(),
|
||||
beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
|
||||
beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
|
||||
beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
|
||||
endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperPotentialHomeOvernightStayIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> clipEsperPotentialInVehicleOvernightStayIntervalEvents(
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> intervals,
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals,
|
||||
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
long durationSeconds = Duration.between(start, end).getSeconds();
|
||||
boolean beginBoundaryChanged = !start.equals(interval.startedAt());
|
||||
boolean endBoundaryChanged = !end.equals(interval.endedAt());
|
||||
return new TachographEsperPotentialInVehicleOvernightStayIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
start,
|
||||
end,
|
||||
durationSeconds,
|
||||
interval.cardAbsentDurationSeconds(),
|
||||
interval.cardAbsentCoveragePercent(),
|
||||
interval.previousDrivingSourceIntervalId(),
|
||||
interval.nextDrivingSourceIntervalId(),
|
||||
interval.previousRegistrationKey(),
|
||||
interval.nextRegistrationKey(),
|
||||
interval.previousVehicleKey(),
|
||||
interval.nextVehicleKey(),
|
||||
beginBoundaryChanged ? null : interval.beginBoundaryOdometerKm(),
|
||||
endBoundaryChanged ? null : interval.endBoundaryOdometerKm(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoEventId(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoEventDomain(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoOccurredAt(),
|
||||
beginBoundaryChanged ? null : interval.beginLatitude(),
|
||||
beginBoundaryChanged ? null : interval.beginLongitude(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoDistanceSeconds(),
|
||||
beginBoundaryChanged ? null : interval.beginGeoOdometerKm(),
|
||||
endBoundaryChanged ? null : interval.endGeoEventId(),
|
||||
endBoundaryChanged ? null : interval.endGeoEventDomain(),
|
||||
endBoundaryChanged ? null : interval.endGeoOccurredAt(),
|
||||
endBoundaryChanged ? null : interval.endLatitude(),
|
||||
endBoundaryChanged ? null : interval.endLongitude(),
|
||||
endBoundaryChanged ? null : interval.endGeoDistanceSeconds(),
|
||||
endBoundaryChanged ? null : interval.endGeoOdometerKm(),
|
||||
beginBoundaryChanged || endBoundaryChanged ? null : interval.geoEvidenceMovementMeters(),
|
||||
beginBoundaryChanged || endBoundaryChanged ? "UNKNOWN" : interval.geoEvidenceMovementCategory(),
|
||||
beginBoundaryChanged ? null : geoEvidenceEvent(interval.beginGeoEventId(), interval.beginGeoEventDomain(), interval.beginGeoOccurredAt(), interval.beginLatitude(), interval.beginLongitude(), interval.beginGeoDistanceSeconds(), interval.beginGeoOdometerKm()),
|
||||
endBoundaryChanged ? null : geoEvidenceEvent(interval.endGeoEventId(), interval.endGeoEventDomain(), interval.endGeoOccurredAt(), interval.endLatitude(), interval.endLongitude(), interval.endGeoDistanceSeconds(), interval.endGeoOdometerKm())
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperPotentialInVehicleTripIntervalEvent> clipEsperPotentialInVehicleTripIntervalEvents(
|
||||
List<TachographEsperPotentialInVehicleTripIntervalEvent> intervals,
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (intervals == null || intervals.isEmpty()
|
||||
|| potentialInVehicleOvernightStayIntervals == null || potentialInVehicleOvernightStayIntervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return intervals.stream()
|
||||
.map(interval -> {
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (!end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> containedIntervals =
|
||||
potentialInVehicleOvernightStayIntervals.stream()
|
||||
.filter(candidate -> tripContainsPotentialInterval(
|
||||
interval.driverKey(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
start,
|
||||
end,
|
||||
candidate
|
||||
))
|
||||
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::endedAt))
|
||||
.toList();
|
||||
if (containedIntervals.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
TachographEsperPotentialInVehicleOvernightStayIntervalEvent first = containedIntervals.get(0);
|
||||
TachographEsperPotentialInVehicleOvernightStayIntervalEvent last =
|
||||
containedIntervals.get(containedIntervals.size() - 1);
|
||||
return new TachographEsperPotentialInVehicleTripIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
containedIntervals.size(),
|
||||
containedIntervals.stream()
|
||||
.mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::durationSeconds)
|
||||
.sum(),
|
||||
containedIntervals.stream()
|
||||
.mapToLong(TachographEsperPotentialInVehicleOvernightStayIntervalEvent::cardAbsentDurationSeconds)
|
||||
.sum(),
|
||||
first.startedAt(),
|
||||
last.endedAt(),
|
||||
first.previousDrivingSourceIntervalId(),
|
||||
last.nextDrivingSourceIntervalId(),
|
||||
containedIntervals
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperPotentialInVehicleTripIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperPotentialInVehicleTripIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean tripContainsPotentialInterval(
|
||||
String driverKey,
|
||||
String registrationKey,
|
||||
String vehicleKey,
|
||||
OffsetDateTime tripStartedAt,
|
||||
OffsetDateTime tripEndedAt,
|
||||
TachographEsperPotentialInVehicleOvernightStayIntervalEvent candidate
|
||||
) {
|
||||
if (!Objects.equals(driverKey, candidate.driverKey())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(registrationKey, candidate.previousRegistrationKey())) {
|
||||
return false;
|
||||
}
|
||||
if (vehicleKey != null && candidate.previousVehicleKey() != null
|
||||
&& !Objects.equals(vehicleKey, candidate.previousVehicleKey())) {
|
||||
return false;
|
||||
}
|
||||
return !candidate.startedAt().isBefore(tripStartedAt)
|
||||
&& !candidate.endedAt().isAfter(tripEndedAt);
|
||||
}
|
||||
|
||||
|
||||
private List<String> esperProjectionNotes() {
|
||||
return List.of(
|
||||
"This endpoint returns Esper-backed per-driver interval projections from the in-memory tachograph file-session model.",
|
||||
"Driving intervals are a filtered projection of activity intervals where activityType = DRIVE.",
|
||||
"Driving interruption intervals are gaps between consecutive driving intervals longer than the configured significant-driving threshold.",
|
||||
"Driving interruption vehicle-change intervals are daily/weekly rest candidates where previousRegistrationKey differs from nextRegistrationKey.",
|
||||
"Daily/weekly rest candidate intervals are driving interruption intervals longer than the configured minimum rest-period threshold.",
|
||||
"Daily/weekly rest candidate coverage intervals enrich each rest candidate with card-present and card-absent coverage metrics computed from vehicle-usage and VU card-absent overlap.",
|
||||
"Daily/weekly rest candidate coverage intervals also attach begin/end geo evidence from nearby support events for the same driver and boundary-side vehicle identity.",
|
||||
"Boundary geo evidence prefers the nearest matching POSITION event, then PLACE, BORDER_CROSSING, and LOAD_UNLOAD within the configured lookback/lookahead windows.",
|
||||
"If both begin and end geo evidence carry odometer values, geoEvidenceMovementCategory classifies the interval as STATIONARY, MINOR, MOVED, or UNKNOWN.",
|
||||
"Unclassified daily/weekly rest candidate coverage intervals are the rest candidates that are neither potential home overnight stays nor potential in-vehicle overnight stays.",
|
||||
"Potential home overnight stay intervals are vehicle-change daily/weekly rest candidate coverage intervals where VU card-absent overlap covers at least 95% of the candidate interval.",
|
||||
"Potential in-vehicle overnight stay intervals are no-change daily/weekly rest candidate coverage intervals where card-present overlap covers the candidate rest period.",
|
||||
"Potential in-vehicle trip intervals span from the end of the coverage interval before a same-vehicle in-vehicle-overnight sequence to the start of the first coverage interval after that sequence.",
|
||||
"VU card-absent intervals are gaps between consecutive normalized vehicle-usage intervals for the same driver.",
|
||||
"occurredFrom and occurredTo clip the returned interval projections to the requested UTC time window.",
|
||||
"Vehicle-usage intervals clear clipped odometer endpoints because boundary odometer values cannot be recomputed safely from the source interval."
|
||||
);
|
||||
}
|
||||
|
||||
private OffsetDateTime utc(OffsetDateTime value) {
|
||||
return value == null ? null : value.withOffsetSameInstant(java.time.ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
private TachographEsperGeoEvidenceEvent geoEvidenceEvent(
|
||||
String eventId,
|
||||
String eventDomain,
|
||||
OffsetDateTime occurredAt,
|
||||
Double latitude,
|
||||
Double longitude,
|
||||
Long distanceSeconds,
|
||||
Long odometerKm
|
||||
) {
|
||||
if (eventId == null
|
||||
&& eventDomain == null
|
||||
&& occurredAt == null
|
||||
&& latitude == null
|
||||
&& longitude == null
|
||||
&& distanceSeconds == null
|
||||
&& odometerKm == null) {
|
||||
return null;
|
||||
}
|
||||
return new TachographEsperGeoEvidenceEvent(
|
||||
eventId,
|
||||
eventDomain,
|
||||
occurredAt,
|
||||
latitude,
|
||||
longitude,
|
||||
distanceSeconds,
|
||||
odometerKm
|
||||
);
|
||||
}
|
||||
|
||||
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isAfter(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isBefore(right) ? left : right;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record TachographEsperProcessingInput(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession,
|
||||
ResolvedDriverTimeline timeline,
|
||||
List<EventHubEventDto> eventInputEvents,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes,
|
||||
boolean forceEventInput,
|
||||
List<String> notes
|
||||
) {
|
||||
public TachographEsperProcessingInput {
|
||||
eventInputEvents = eventInputEvents == null ? List.of() : List.copyOf(eventInputEvents);
|
||||
significantDrivingMinutes = Math.max(1, significantDrivingMinutes);
|
||||
minimumRestPeriodMinutes = Math.max(1, minimumRestPeriodMinutes);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
}
|
||||
|
||||
public static TachographEsperProcessingInput fromFileSession(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession,
|
||||
ResolvedDriverTimeline timeline,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes,
|
||||
List<String> notes
|
||||
) {
|
||||
return new TachographEsperProcessingInput(
|
||||
session == null ? null : session.sessionId(),
|
||||
driverSession == null ? null : driverSession.driverKey(),
|
||||
session,
|
||||
driverSession,
|
||||
timeline,
|
||||
List.of(),
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes,
|
||||
false,
|
||||
notes
|
||||
);
|
||||
}
|
||||
|
||||
public static TachographEsperProcessingInput fromEvents(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
ResolvedDriverTimeline timeline,
|
||||
List<EventHubEventDto> eventInputEvents,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes,
|
||||
List<String> notes
|
||||
) {
|
||||
return new TachographEsperProcessingInput(
|
||||
sessionId,
|
||||
driverKey,
|
||||
null,
|
||||
null,
|
||||
timeline,
|
||||
eventInputEvents,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes,
|
||||
true,
|
||||
notes
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasEventInputEvents() {
|
||||
return !eventInputEvents.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import java.util.LinkedHashMap;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
|
|
@ -42,6 +43,7 @@ public class TachographFileSessionProcessingService {
|
|||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||
private final EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder;
|
||||
private final TachographEsperProcessingCore esperProcessingCore;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public TachographFileSessionProcessingService(
|
||||
|
|
@ -50,12 +52,32 @@ public class TachographFileSessionProcessingService {
|
|||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this(
|
||||
repository,
|
||||
driverTimelineBuilder,
|
||||
reusableProjectionBuilder,
|
||||
eventBackedDriverTimelineBuilder,
|
||||
properties,
|
||||
new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)
|
||||
);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public TachographFileSessionProcessingService(
|
||||
TachographFileSessionRepository repository,
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventBackedDriverTimelineBuilder eventBackedDriverTimelineBuilder,
|
||||
EventHubProperties properties,
|
||||
TachographEsperProcessingCore esperProcessingCore
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||
this.eventBackedDriverTimelineBuilder = eventBackedDriverTimelineBuilder;
|
||||
this.properties = properties;
|
||||
this.esperProcessingCore = esperProcessingCore;
|
||||
}
|
||||
|
||||
public TachographOperatingPeriodsProcessingResultDto evaluateOperatingPeriods(
|
||||
|
|
@ -156,160 +178,26 @@ public class TachographFileSessionProcessingService {
|
|||
}
|
||||
|
||||
ResolvedDriverTimeline timeline = resolveTimeline(session, driver);
|
||||
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null ? timeline.loadedFrom() : utc(effectiveRequest.occurredFrom());
|
||||
OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null ? timeline.loadedTo() : utc(effectiveRequest.occurredTo());
|
||||
OffsetDateTime requestedFrom = effectiveRequest.occurredFrom() == null
|
||||
? timeline.loadedFrom()
|
||||
: utc(effectiveRequest.occurredFrom());
|
||||
OffsetDateTime requestedTo = effectiveRequest.occurredTo() == null
|
||||
? timeline.loadedTo()
|
||||
: utc(effectiveRequest.occurredTo());
|
||||
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
|
||||
}
|
||||
int significantDrivingMinutes = resolveEsperSignificantDrivingMinutes(effectiveRequest);
|
||||
int minimumRestPeriodMinutes = resolveMinimumRestPeriodMinutes(effectiveRequest);
|
||||
|
||||
List<TachographEsperActivityIntervalEvent> activityIntervals = clipEsperActivityIntervalEvents(
|
||||
driverTimelineBuilder.buildEsperActivityIntervalEvents(sessionId, driverKey, timeline),
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperActivityIntervalEvent> drivingIntervals = clipEsperActivityIntervalEvents(
|
||||
driverTimelineBuilder.buildEsperDrivingIntervalEvents(sessionId, driverKey, timeline),
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
TachographEsperDrivingDerivedProjectionBundle derivedProjectionBundle =
|
||||
properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode()
|
||||
== EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS
|
||||
? reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
|
||||
return esperProcessingCore.process(TachographEsperProcessingInput.fromFileSession(
|
||||
session,
|
||||
driver,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
)
|
||||
: reusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundle(
|
||||
sessionId,
|
||||
driverKey,
|
||||
timeline,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionIntervals =
|
||||
derivedProjectionBundle.drivingInterruptionIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(
|
||||
rawDrivingInterruptionIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDailyWeeklyRestCandidateIntervals =
|
||||
derivedProjectionBundle.dailyWeeklyRestCandidateIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(
|
||||
rawDailyWeeklyRestCandidateIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> rawDrivingInterruptionVehicleChangeIntervals =
|
||||
derivedProjectionBundle.drivingInterruptionVehicleChangeIntervals();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals =
|
||||
clipEsperDrivingInterruptionIntervalEvents(
|
||||
rawDrivingInterruptionVehicleChangeIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperVehicleUsageIntervalEvent> rawVehicleUsageIntervals =
|
||||
driverTimelineBuilder.buildEsperVehicleUsageIntervalEvents(timeline);
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> rawVuCardAbsentIntervals =
|
||||
derivedProjectionBundle.vuCardAbsentIntervals();
|
||||
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals =
|
||||
clipEsperPotentialHomeOvernightStayIntervalEvents(
|
||||
derivedProjectionBundle.potentialHomeOvernightStayIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals =
|
||||
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
|
||||
derivedProjectionBundle.dailyWeeklyRestCandidateCoverageIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals =
|
||||
clipEsperDailyWeeklyRestCandidateCoverageIntervalEvents(
|
||||
derivedProjectionBundle.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals =
|
||||
clipEsperPotentialInVehicleOvernightStayIntervalEvents(
|
||||
derivedProjectionBundle.potentialInVehicleOvernightStayIntervals(),
|
||||
rawVuCardAbsentIntervals,
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals =
|
||||
clipEsperPotentialInVehicleTripIntervalEvents(
|
||||
derivedProjectionBundle.potentialInVehicleTripIntervals(),
|
||||
potentialInVehicleOvernightStayIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals = clipEsperVehicleUsageIntervalEvents(
|
||||
rawVehicleUsageIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = clipEsperVuCardAbsentIntervalEvents(
|
||||
rawVuCardAbsentIntervals,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
List<TachographEsperSupportGeoEvent> supportGeoEvents = clipEsperSupportGeoEvents(
|
||||
timeline.supportEvents(),
|
||||
driverKey,
|
||||
requestedFrom,
|
||||
requestedTo
|
||||
);
|
||||
|
||||
return new TachographEsperDriverProcessingResultDto(
|
||||
sessionId,
|
||||
driverKey,
|
||||
timeline.sourceKind(),
|
||||
timeline.loadedFrom(),
|
||||
timeline.loadedTo(),
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
activityIntervals.size(),
|
||||
drivingIntervals.size(),
|
||||
drivingInterruptionIntervals.size(),
|
||||
drivingInterruptionVehicleChangeIntervals.size(),
|
||||
dailyWeeklyRestCandidateIntervals.size(),
|
||||
dailyWeeklyRestCandidateCoverageIntervals.size(),
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervals.size(),
|
||||
potentialHomeOvernightStayIntervals.size(),
|
||||
potentialInVehicleOvernightStayIntervals.size(),
|
||||
potentialInVehicleTripIntervals.size(),
|
||||
vehicleUsageIntervals.size(),
|
||||
vuCardAbsentIntervals.size(),
|
||||
supportGeoEvents.size(),
|
||||
activityIntervals,
|
||||
drivingIntervals,
|
||||
drivingInterruptionIntervals,
|
||||
drivingInterruptionVehicleChangeIntervals,
|
||||
dailyWeeklyRestCandidateIntervals,
|
||||
dailyWeeklyRestCandidateCoverageIntervals,
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
|
||||
potentialHomeOvernightStayIntervals,
|
||||
potentialInVehicleOvernightStayIntervals,
|
||||
potentialInVehicleTripIntervals,
|
||||
vehicleUsageIntervals,
|
||||
vuCardAbsentIntervals,
|
||||
supportGeoEvents,
|
||||
esperProjectionNotes()
|
||||
);
|
||||
resolveEsperSignificantDrivingMinutes(effectiveRequest),
|
||||
resolveMinimumRestPeriodMinutes(effectiveRequest),
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
private ResolvedDriverTimeline resolveTimeline(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package at.procon.eventhub.processing.api;
|
|||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
|
@ -14,13 +15,21 @@ import at.procon.eventhub.dto.EventLifecycle;
|
|||
import at.procon.eventhub.dto.EventType;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||
|
|
@ -48,7 +57,8 @@ class UnifiedRuntimeProcessingControllerTest {
|
|||
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))
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService, derivedProjectionService))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
|
@ -98,7 +108,8 @@ class UnifiedRuntimeProcessingControllerTest {
|
|||
void loadsDriverTimelineViaRuntimeApi() 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))
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService, derivedProjectionService))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
|
@ -162,11 +173,286 @@ class UnifiedRuntimeProcessingControllerTest {
|
|||
.andExpect(jsonPath("$.vehicleUsageIntervals[0].intervalId").value("CVU-1"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void loadsDriverDerivedProjectionsViaRuntimeApi() throws Exception {
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
|
||||
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService, derivedProjectionService))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSession(
|
||||
sessionId,
|
||||
"12:123",
|
||||
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
|
||||
true,
|
||||
0
|
||||
);
|
||||
TachographEsperDriverProcessingResultDto projection = new TachographEsperDriverProcessingResultDto(
|
||||
sessionId,
|
||||
"12:123",
|
||||
"UNIFIED_EVENT_STREAM",
|
||||
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of("runtime derived")
|
||||
);
|
||||
when(derivedProjectionService.loadDriverDerivedProjections(any()))
|
||||
.thenReturn(new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
request,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
|
||||
projection,
|
||||
List.of("runtime derived")
|
||||
));
|
||||
|
||||
mockMvc.perform(post("/api/eventhub/runtime-processing/driver-derived-projections")
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{
|
||||
"sessionId": "%s",
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"driverKey": "12:123",
|
||||
"occurredFrom": "2026-05-01T08:00:00Z",
|
||||
"occurredTo": "2026-05-01T10:00:00Z",
|
||||
"expandVehicleEvents": true,
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720
|
||||
}
|
||||
""".formatted(sessionId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.mergedEventCount").value(3))
|
||||
.andExpect(jsonPath("$.projection.driverKey").value("12:123"))
|
||||
.andExpect(jsonPath("$.projection.drivingInterruptionIntervalCount").value(1))
|
||||
.andExpect(jsonPath("$.projection.dailyWeeklyRestCandidateIntervalCount").value(1));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void listsRuntimeEventProcessingProfilesViaRuntimeApi() throws Exception {
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
|
||||
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
RuntimeEventProcessingService runtimeEventProcessingService = org.mockito.Mockito.mock(RuntimeEventProcessingService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(
|
||||
eventAssemblyService,
|
||||
timelineService,
|
||||
derivedProjectionService,
|
||||
null,
|
||||
runtimeEventProcessingService
|
||||
))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
||||
when(runtimeEventProcessingService.listProfiles())
|
||||
.thenReturn(List.of(new RuntimeEventProcessingProfileDescriptorDto(
|
||||
"tachograph-driver-esper-v1",
|
||||
"Tachograph Driver Esper Processing",
|
||||
"Runs tachograph driver Esper processing over runtime event scopes.",
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
List.of(RuntimeEventPartitioningStrategy.DRIVER),
|
||||
Set.of(),
|
||||
Set.of("significantDrivingMinutes", "minimumRestPeriodMinutes")
|
||||
)));
|
||||
|
||||
mockMvc.perform(get("/api/eventhub/runtime-processing/event-processing/profiles"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].profileKey").value("tachograph-driver-esper-v1"))
|
||||
.andExpect(jsonPath("$[0].displayName").value("Tachograph Driver Esper Processing"))
|
||||
.andExpect(jsonPath("$[0].defaultPartitioningStrategy").value("DRIVER"))
|
||||
.andExpect(jsonPath("$[0].supportedPartitioningStrategies[0]").value("DRIVER"))
|
||||
.andExpect(jsonPath("$[0].optionalParameters[0]").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsGenericEventProcessingProfileViaRuntimeApi() throws Exception {
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
|
||||
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
RuntimeEventProcessingService runtimeEventProcessingService = org.mockito.Mockito.mock(RuntimeEventProcessingService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(
|
||||
eventAssemblyService,
|
||||
timelineService,
|
||||
derivedProjectionService,
|
||||
null,
|
||||
runtimeEventProcessingService
|
||||
))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSession(
|
||||
sessionId,
|
||||
"12:123",
|
||||
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
|
||||
true,
|
||||
0
|
||||
);
|
||||
when(runtimeEventProcessingService.process(any()))
|
||||
.thenReturn(new RuntimeEventProcessingResultDto(
|
||||
"tachograph-driver-esper-v1",
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
request,
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
|
||||
Map.of(),
|
||||
List.of("generic profile"),
|
||||
List.of()
|
||||
));
|
||||
|
||||
mockMvc.perform(post("/api/eventhub/runtime-processing/event-processing")
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"scope": {
|
||||
"sessionId": "%s",
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"driverKey": "12:123",
|
||||
"occurredFrom": "2026-05-01T08:00:00Z",
|
||||
"occurredTo": "2026-05-01T10:00:00Z"
|
||||
},
|
||||
"partitioning": {
|
||||
"strategy": "DRIVER",
|
||||
"includeAllPartitions": false
|
||||
},
|
||||
"parameters": {
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720
|
||||
}
|
||||
}
|
||||
""".formatted(sessionId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.profileKey").value("tachograph-driver-esper-v1"))
|
||||
.andExpect(jsonPath("$.partitioningStrategy").value("DRIVER"))
|
||||
.andExpect(jsonPath("$.inputEventCount").value(3))
|
||||
.andExpect(jsonPath("$.discoveredVehicles[0].vin").value("VIN-1"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void compatibilityTachographEndpointDelegatesThroughGenericProfileRuntimeApi() throws Exception {
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService = org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
|
||||
UnifiedRuntimeDriverTimelineService timelineService = org.mockito.Mockito.mock(UnifiedRuntimeDriverTimelineService.class);
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
RuntimeEventProcessingService runtimeEventProcessingService = org.mockito.Mockito.mock(RuntimeEventProcessingService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(
|
||||
eventAssemblyService,
|
||||
timelineService,
|
||||
derivedProjectionService,
|
||||
null,
|
||||
runtimeEventProcessingService
|
||||
))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSession(
|
||||
sessionId,
|
||||
"12:123",
|
||||
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T10:00:00Z"),
|
||||
true,
|
||||
0
|
||||
);
|
||||
UnifiedRuntimeDerivedProjectionResultDto driverResult = new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
request,
|
||||
2,
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
|
||||
null,
|
||||
List.of("processed through generic profile")
|
||||
);
|
||||
when(runtimeEventProcessingService.process(any()))
|
||||
.thenReturn(new RuntimeEventProcessingResultDto(
|
||||
"tachograph-driver-esper-v1",
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
request,
|
||||
5,
|
||||
1,
|
||||
1,
|
||||
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
|
||||
Map.of("12:123", new RuntimeEventProcessingPartitionResultDto(
|
||||
"DRIVER",
|
||||
"12:123",
|
||||
"UnifiedRuntimeDerivedProjectionResultDto",
|
||||
driverResult,
|
||||
Map.of("mergedEventCount", 5)
|
||||
)),
|
||||
List.of("generic adapter"),
|
||||
List.of()
|
||||
));
|
||||
|
||||
mockMvc.perform(post("/api/eventhub/runtime-processing/tachograph/esper-processing")
|
||||
.contentType("application/json")
|
||||
.content("""
|
||||
{
|
||||
"sessionId": "%s",
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"driverKey": "12:123",
|
||||
"occurredFrom": "2026-05-01T08:00:00Z",
|
||||
"occurredTo": "2026-05-01T10:00:00Z",
|
||||
"expandVehicleEvents": true,
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720
|
||||
}
|
||||
""".formatted(sessionId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.inputEventCount").value(5))
|
||||
.andExpect(jsonPath("$.selectedDriverCount").value(1))
|
||||
.andExpect(jsonPath("$.driverResults['12:123'].mergedEventCount").value(5))
|
||||
.andExpect(jsonPath("$.notes[0]").value("generic adapter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsBadRequestForInvalidRuntimeRequest() 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))
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService = org.mockito.Mockito.mock(UnifiedRuntimeDerivedProjectionService.class);
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UnifiedRuntimeProcessingController(eventAssemblyService, timelineService, derivedProjectionService))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
.build();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package at.procon.eventhub.processing.eventprocessing;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.RuntimeEventProcessingProfile;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.RuntimeEventProcessingProfileRegistry;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
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;
|
||||
|
||||
class RuntimeEventProcessingServiceTest {
|
||||
|
||||
@Test
|
||||
void processesThroughSelectedProfileAndListsProfiles() {
|
||||
RuntimeEventProcessingService service = new RuntimeEventProcessingService(
|
||||
new RuntimeEventProcessingProfileRegistry(List.of(new EchoProfile()))
|
||||
);
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
RuntimeEventProcessingApiRequest request = new RuntimeEventProcessingApiRequest(
|
||||
"echo-profile-v1",
|
||||
new UnifiedRuntimeProcessingApiRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
null,
|
||||
"12:DRIVER-1",
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T01:00:00Z"),
|
||||
true,
|
||||
0,
|
||||
null,
|
||||
null
|
||||
),
|
||||
new RuntimeEventPartitioningApiRequest(RuntimeEventPartitioningStrategy.DRIVER, null, false, null, false, null, false, null, null),
|
||||
Map.of()
|
||||
);
|
||||
|
||||
RuntimeEventProcessingResultDto result = service.process(request);
|
||||
|
||||
assertThat(result.profileKey()).isEqualTo("echo-profile-v1");
|
||||
assertThat(result.partitioningStrategy()).isEqualTo(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
var profiles = service.listProfiles();
|
||||
assertThat(profiles).hasSize(1);
|
||||
assertThat(profiles.get(0).profileKey()).isEqualTo("echo-profile-v1");
|
||||
}
|
||||
|
||||
private static final class EchoProfile implements RuntimeEventProcessingProfile {
|
||||
|
||||
@Override
|
||||
public String profileKey() {
|
||||
return "echo-profile-v1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
|
||||
return RuntimeEventPartitioningStrategy.DRIVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
UnifiedRuntimeProcessingRequest runtimeRequest = request.scope().toRuntimeRequest();
|
||||
return new RuntimeEventProcessingResultDto(
|
||||
profileKey(),
|
||||
defaultPartitioningStrategy(),
|
||||
runtimeRequest,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
List.of(),
|
||||
Map.of(),
|
||||
List.of("echo"),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RuntimeEventProcessingProfileRegistryTest {
|
||||
|
||||
@Test
|
||||
void exposesProfileDescriptorsForDiscovery() {
|
||||
RuntimeEventProcessingProfileRegistry registry = new RuntimeEventProcessingProfileRegistry(List.of(new TestProfile()));
|
||||
|
||||
var descriptors = registry.profileDescriptors();
|
||||
|
||||
assertThat(descriptors).hasSize(1);
|
||||
assertThat(descriptors.get(0).profileKey()).isEqualTo("test-profile-v1");
|
||||
assertThat(descriptors.get(0).displayName()).isEqualTo("Test Profile");
|
||||
assertThat(descriptors.get(0).defaultPartitioningStrategy()).isEqualTo(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
assertThat(descriptors.get(0).supportedPartitioningStrategies()).containsExactly(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
assertThat(descriptors.get(0).optionalParameters()).containsExactly("thresholdMinutes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateProfileKeys() {
|
||||
assertThatThrownBy(() -> new RuntimeEventProcessingProfileRegistry(List.of(new TestProfile(), new TestProfile())))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Duplicate runtime event processing profileKey");
|
||||
}
|
||||
|
||||
private static final class TestProfile implements RuntimeEventProcessingProfile {
|
||||
|
||||
@Override
|
||||
public String profileKey() {
|
||||
return "test-profile-v1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String displayName() {
|
||||
return "Test Profile";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Test profile descriptor.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
|
||||
return RuntimeEventPartitioningStrategy.DRIVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> optionalParameters() {
|
||||
return Set.of("thresholdMinutes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
throw new UnsupportedOperationException("Not needed by this test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService;
|
||||
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.mockito.ArgumentCaptor;
|
||||
|
||||
class TachographDriverEsperRuntimeEventProcessingProfileTest {
|
||||
|
||||
@Test
|
||||
void exposesDiscoveryMetadata() {
|
||||
TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(
|
||||
org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class)
|
||||
);
|
||||
|
||||
assertThat(profile.profileKey()).isEqualTo("tachograph-driver-esper-v1");
|
||||
assertThat(profile.displayName()).isEqualTo("Tachograph Driver Esper Processing");
|
||||
assertThat(profile.defaultPartitioningStrategy()).isEqualTo(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
assertThat(profile.supportedPartitioningStrategies()).containsExactly(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
assertThat(profile.optionalParameters()).containsExactlyInAnyOrder("significantDrivingMinutes", "minimumRestPeriodMinutes", "attachVehicleOnlyEvents", "vehicleEvidencePaddingMinutes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void delegatesToTachographScopeServiceAndMapsPartitionResults() {
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService scopeService = org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class);
|
||||
TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(scopeService);
|
||||
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingApiRequest scope = new UnifiedRuntimeProcessingApiRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
null,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-31T23:59:59Z"),
|
||||
true,
|
||||
15,
|
||||
null,
|
||||
null
|
||||
);
|
||||
RuntimeEventProcessingApiRequest request = new RuntimeEventProcessingApiRequest(
|
||||
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
|
||||
scope,
|
||||
new RuntimeEventPartitioningApiRequest(
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
Set.of("12:DRIVER-1"),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
),
|
||||
Map.of(
|
||||
"significantDrivingMinutes", 5,
|
||||
"minimumRestPeriodMinutes", "600",
|
||||
"vehicleEvidencePaddingMinutes", 20,
|
||||
"attachVehicleOnlyEvents", true
|
||||
)
|
||||
);
|
||||
|
||||
UnifiedRuntimeProcessingRequest processedRequest = new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
List.of(sessionId),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
null,
|
||||
"12:DRIVER-1",
|
||||
Set.of("12:DRIVER-1"),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-31T23:59:59Z"),
|
||||
true,
|
||||
15
|
||||
);
|
||||
UnifiedRuntimeDerivedProjectionResultDto driverResult = new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
processedRequest,
|
||||
2,
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
List.of(),
|
||||
null,
|
||||
List.of("driver processed")
|
||||
);
|
||||
when(scopeService.processScope(any()))
|
||||
.thenReturn(new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
processedRequest,
|
||||
5,
|
||||
1,
|
||||
1,
|
||||
List.of(),
|
||||
Map.of("12:DRIVER-1", driverResult),
|
||||
List.of("scope processed"),
|
||||
List.of()
|
||||
));
|
||||
|
||||
var result = profile.process(request);
|
||||
|
||||
assertThat(result.profileKey()).isEqualTo(TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY);
|
||||
assertThat(result.partitioningStrategy()).isEqualTo(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
assertThat(result.partitionResults()).containsOnlyKeys("12:DRIVER-1");
|
||||
assertThat(result.partitionResults().get("12:DRIVER-1").partitionType()).isEqualTo("DRIVER");
|
||||
assertThat(result.partitionResults().get("12:DRIVER-1").result()).isSameAs(driverResult);
|
||||
|
||||
ArgumentCaptor<UnifiedRuntimeProcessingApiRequest> captor = ArgumentCaptor.forClass(UnifiedRuntimeProcessingApiRequest.class);
|
||||
verify(scopeService).processScope(captor.capture());
|
||||
UnifiedRuntimeProcessingApiRequest delegated = captor.getValue();
|
||||
assertThat(delegated.driverKeys()).containsExactly("12:DRIVER-1");
|
||||
assertThat(delegated.significantDrivingMinutes()).isEqualTo(5);
|
||||
assertThat(delegated.minimumRestPeriodMinutes()).isEqualTo(600);
|
||||
assertThat(delegated.vehicleExpansionPaddingMinutes()).isEqualTo(20);
|
||||
assertThat(delegated.expandVehicleEvents()).isTrue();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -89,14 +90,141 @@ class UnifiedRuntimeProcessingRequestTest {
|
|||
assertThat(request.eventBackend()).isEqualTo(UnifiedRuntimeEventBackend.SOURCE_DB);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void canBuildMultiFileSessionRuntimeRequest() {
|
||||
UUID firstSessionId = UUID.randomUUID();
|
||||
UUID secondSessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographFileSessions(
|
||||
List.of(firstSessionId, secondSessionId),
|
||||
"12:123",
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-03T00:00:00Z"),
|
||||
true,
|
||||
10
|
||||
);
|
||||
|
||||
assertThat(request.sessionId()).isEqualTo(firstSessionId);
|
||||
assertThat(request.sessionIds()).containsExactly(firstSessionId, secondSessionId);
|
||||
assertThat(request.compositeSessionId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void canBuildCompositeFileSessionRuntimeRequest() {
|
||||
UUID compositeSessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographCompositeSession(
|
||||
compositeSessionId,
|
||||
"12:123",
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-03T00:00:00Z"),
|
||||
true,
|
||||
10
|
||||
);
|
||||
|
||||
assertThat(request.sessionId()).isNull();
|
||||
assertThat(request.sessionIds()).isEmpty();
|
||||
assertThat(request.compositeSessionId()).isEqualTo(compositeSessionId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void canBuildMultiDriverFileSessionRuntimeScopeRequest() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
null,
|
||||
Set.of("12:123", "12:456"),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
|
||||
true,
|
||||
10
|
||||
);
|
||||
|
||||
assertThat(request.driverKey()).isNull();
|
||||
assertThat(request.driverKeys()).containsExactlyInAnyOrder("12:123", "12:456");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canBuildIncludeAllDriversRuntimeScopeRequest() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UnifiedRuntimeProcessingRequest request = new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
null,
|
||||
Set.of(),
|
||||
true,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
|
||||
true,
|
||||
10
|
||||
);
|
||||
|
||||
assertThat(request.includeAllDrivers()).isTrue();
|
||||
assertThat(request.driverKey()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAmbiguousExplicitAndCompositeSessionSelectors() {
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
UUID compositeSessionId = UUID.randomUUID();
|
||||
assertThatThrownBy(() -> new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
compositeSessionId,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
"12:123",
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
0
|
||||
)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Use either compositeSessionId");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void rejectsRequestWithoutDriverSelector() {
|
||||
assertThatThrownBy(() -> new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
"default",
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_DB),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventDomain;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.EventLifecycle;
|
||||
import at.procon.eventhub.dto.EventType;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
|
||||
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RuntimeDriverVehicleEvidenceAttachmentServiceTest {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private final RuntimeDriverVehicleEvidenceAttachmentService service = new RuntimeDriverVehicleEvidenceAttachmentService(
|
||||
new UnifiedEventTimelineReconstructor(),
|
||||
new RuntimeEventScopeClassifier()
|
||||
);
|
||||
|
||||
@Test
|
||||
void attachesVehicleOnlyEvidenceInsideDriverVehicleUsageInterval() {
|
||||
List<EventHubEventDto> driverEvents = List.of(
|
||||
cardEvent("card-1-in", EventType.CARD_INSERTED, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T08:00:00Z"),
|
||||
cardEvent("card-1-out", EventType.CARD_WITHDRAWN, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T18:00:00Z")
|
||||
);
|
||||
EventHubEventDto vehicleEvidence = vehicleOnlyEvent("pos-inside", "VIN-1", "AT:W-1", "2026-05-01T12:00:00Z");
|
||||
EventHubEventDto outside = vehicleOnlyEvent("pos-outside", "VIN-1", "AT:W-1", "2026-05-01T20:00:00Z");
|
||||
EventHubEventDto wrongVehicle = vehicleOnlyEvent("pos-wrong", "VIN-2", "AT:W-2", "2026-05-01T12:00:00Z");
|
||||
|
||||
RuntimeDriverVehicleEvidenceAttachmentResult result = service.attachVehicleEvidence(
|
||||
"DRIVER-1",
|
||||
driverEvents,
|
||||
List.of(driverEvents.get(0), driverEvents.get(1), vehicleEvidence, outside, wrongVehicle),
|
||||
true,
|
||||
0
|
||||
);
|
||||
|
||||
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(1);
|
||||
assertThat(result.candidateVehicleEvidenceEventCount()).isEqualTo(3);
|
||||
assertThat(result.attachedVehicleEvidenceEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||
.containsExactly("pos-inside");
|
||||
assertThat(result.ignoredVehicleEvidenceEventCount()).isEqualTo(2);
|
||||
assertThat(result.mergedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||
.containsExactly("card-1-in", "pos-inside", "card-1-out");
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesConfiguredPaddingWhenAttachingVehicleEvidence() {
|
||||
List<EventHubEventDto> driverEvents = List.of(
|
||||
cardEvent("card-1-in", EventType.CARD_INSERTED, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T08:00:00Z"),
|
||||
cardEvent("card-1-out", EventType.CARD_WITHDRAWN, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T18:00:00Z")
|
||||
);
|
||||
EventHubEventDto justAfter = vehicleOnlyEvent("pos-after", "VIN-1", "AT:W-1", "2026-05-01T18:10:00Z");
|
||||
|
||||
RuntimeDriverVehicleEvidenceAttachmentResult result = service.attachVehicleEvidence(
|
||||
"DRIVER-1",
|
||||
driverEvents,
|
||||
List.of(driverEvents.get(0), driverEvents.get(1), justAfter),
|
||||
true,
|
||||
15
|
||||
);
|
||||
|
||||
assertThat(result.attachedVehicleEvidenceEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||
.containsExactly("pos-after");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotAttachVehicleEvidenceWhenDisabled() {
|
||||
List<EventHubEventDto> driverEvents = List.of(
|
||||
cardEvent("card-1-in", EventType.CARD_INSERTED, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T08:00:00Z"),
|
||||
cardEvent("card-1-out", EventType.CARD_WITHDRAWN, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T18:00:00Z")
|
||||
);
|
||||
EventHubEventDto vehicleEvidence = vehicleOnlyEvent("pos-inside", "VIN-1", "AT:W-1", "2026-05-01T12:00:00Z");
|
||||
|
||||
RuntimeDriverVehicleEvidenceAttachmentResult result = service.attachVehicleEvidence(
|
||||
"DRIVER-1",
|
||||
driverEvents,
|
||||
List.of(driverEvents.get(0), driverEvents.get(1), vehicleEvidence),
|
||||
false,
|
||||
0
|
||||
);
|
||||
|
||||
assertThat(result.attachedVehicleEvidenceEvents()).isEmpty();
|
||||
assertThat(result.mergedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||
.containsExactly("card-1-in", "card-1-out");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergesMidnightVehicleUsageContinuationBeforeEvidenceAttachment() {
|
||||
List<EventHubEventDto> driverEvents = List.of(
|
||||
cardEvent("card-1-in", EventType.CARD_INSERTED, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T20:00:00Z"),
|
||||
cardEvent("card-1-out", EventType.CARD_WITHDRAWN, "DRIVER-1", "interval-1", "VIN-1", "AT:W-1", "2026-05-01T23:59:59Z"),
|
||||
cardEvent("card-2-in", EventType.CARD_INSERTED, "DRIVER-1", "interval-2", "VIN-1", "AT:W-1", "2026-05-02T00:00:00Z"),
|
||||
cardEvent("card-2-out", EventType.CARD_WITHDRAWN, "DRIVER-1", "interval-2", "VIN-1", "AT:W-1", "2026-05-02T08:00:00Z")
|
||||
);
|
||||
EventHubEventDto midnightEvidence = vehicleOnlyEvent("pos-midnight", "VIN-1", "AT:W-1", "2026-05-02T00:00:00Z");
|
||||
|
||||
RuntimeDriverVehicleEvidenceAttachmentResult result = service.attachVehicleEvidence(
|
||||
"DRIVER-1",
|
||||
driverEvents,
|
||||
List.of(driverEvents.get(0), driverEvents.get(1), driverEvents.get(2), driverEvents.get(3), midnightEvidence),
|
||||
true,
|
||||
0
|
||||
);
|
||||
|
||||
assertThat(result.vehicleUsageIntervalCount()).isEqualTo(1);
|
||||
assertThat(result.attachedVehicleEvidenceEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||
.containsExactly("pos-midnight");
|
||||
}
|
||||
|
||||
private EventHubEventDto cardEvent(
|
||||
String externalId,
|
||||
EventType eventType,
|
||||
String driverKey,
|
||||
String intervalId,
|
||||
String vehicleKey,
|
||||
String registrationKey,
|
||||
String occurredAt
|
||||
) {
|
||||
EventLifecycle lifecycle = eventType == EventType.CARD_INSERTED ? EventLifecycle.INSERT : EventLifecycle.WITHDRAW;
|
||||
return new EventHubEventDto(
|
||||
UUID.randomUUID(),
|
||||
externalId,
|
||||
new DriverRefDto(driverKey, null),
|
||||
vehicleRef(vehicleKey, registrationKey),
|
||||
OffsetDateTime.parse(occurredAt),
|
||||
null,
|
||||
OffsetDateTime.parse(occurredAt),
|
||||
EventDomain.DRIVER_CARD,
|
||||
eventType,
|
||||
lifecycle,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
raw(driverKey, intervalId, vehicleKey, registrationKey),
|
||||
false,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private EventHubEventDto vehicleOnlyEvent(
|
||||
String externalId,
|
||||
String vehicleKey,
|
||||
String registrationKey,
|
||||
String occurredAt
|
||||
) {
|
||||
return new EventHubEventDto(
|
||||
UUID.randomUUID(),
|
||||
externalId,
|
||||
null,
|
||||
vehicleRef(vehicleKey, registrationKey),
|
||||
OffsetDateTime.parse(occurredAt),
|
||||
null,
|
||||
OffsetDateTime.parse(occurredAt),
|
||||
EventDomain.POSITION,
|
||||
EventType.POSITION_RECORDED,
|
||||
EventLifecycle.SNAPSHOT,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
raw(null, null, vehicleKey, registrationKey),
|
||||
false,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private VehicleRefDto vehicleRef(String vehicleKey, String registrationKey) {
|
||||
String[] registrationParts = registrationKey.split(":", 2);
|
||||
return new VehicleRefDto(
|
||||
"VIN:" + vehicleKey,
|
||||
vehicleKey,
|
||||
"VR:" + registrationKey,
|
||||
new VehicleRegistrationRefDto(registrationParts[0], registrationParts[1])
|
||||
);
|
||||
}
|
||||
|
||||
private JsonNode raw(String driverKey, String intervalId, String vehicleKey, String registrationKey) {
|
||||
ObjectNode root = OBJECT_MAPPER.createObjectNode();
|
||||
ObjectNode raw = root.putObject("raw");
|
||||
if (driverKey != null) {
|
||||
raw.put("driverKey", driverKey);
|
||||
}
|
||||
if (intervalId != null) {
|
||||
raw.put("intervalId", intervalId);
|
||||
raw.put("sourceRowId", intervalId);
|
||||
}
|
||||
raw.put("vehicleKey", vehicleKey);
|
||||
raw.put("registrationKey", registrationKey);
|
||||
raw.put("sourceKind", "TEST");
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
||||
import at.procon.eventhub.service.EventDetailsFactory;
|
||||
import at.procon.eventhub.service.EventHubEventSorter;
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||
|
|
@ -19,6 +21,8 @@ import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
|||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverKeyFactory;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession;
|
||||
import at.procon.eventhub.tachographfilesession.service.InMemoryTachographCompositeSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.InMemoryTachographFileSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.IntervalBackedDriverTimelineEventBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
|
||||
|
|
@ -39,6 +43,7 @@ class TachographFileSessionRuntimeEventLoaderTest {
|
|||
void loadsDriverAndVehicleEventsFromFileSessionRuntimePath() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
InMemoryTachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
|
||||
IntervalBackedDriverTimelineEventBuilder eventBuilder = new IntervalBackedDriverTimelineEventBuilder(
|
||||
new DriverTimelineBuilder(),
|
||||
new DriverKeyFactory(),
|
||||
|
|
@ -47,7 +52,10 @@ class TachographFileSessionRuntimeEventLoaderTest {
|
|||
);
|
||||
TachographFileSessionRuntimeEventLoader loader = new TachographFileSessionRuntimeEventLoader(
|
||||
new UnifiedDriverEventSourceService(List.of(new TachographFileSessionUnifiedDriverEventSource(repository, eventBuilder))),
|
||||
new UnifiedVehicleEventSourceService(List.of(new TachographFileSessionUnifiedVehicleEventSource(repository, eventBuilder)))
|
||||
new UnifiedVehicleEventSourceService(List.of(new TachographFileSessionUnifiedVehicleEventSource(repository, eventBuilder))),
|
||||
compositeRepository,
|
||||
new EventAcquisitionRecordKeyService(),
|
||||
new EventHubEventSorter()
|
||||
);
|
||||
|
||||
DriverExtractionSession driver = driver();
|
||||
|
|
@ -68,6 +76,56 @@ class TachographFileSessionRuntimeEventLoaderTest {
|
|||
assertThat(loader.loadVehicleEvents(request, new UnifiedDiscoveredVehicleRef("VIN-1", "VIN-1", "12", "REG-1"))).hasSize(5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
void loadsDriverAndVehicleEventsFromCompositeSessionRuntimePath() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
InMemoryTachographCompositeSessionRepository compositeRepository = new InMemoryTachographCompositeSessionRepository();
|
||||
IntervalBackedDriverTimelineEventBuilder eventBuilder = new IntervalBackedDriverTimelineEventBuilder(
|
||||
new DriverTimelineBuilder(),
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
);
|
||||
TachographFileSessionRuntimeEventLoader loader = new TachographFileSessionRuntimeEventLoader(
|
||||
new UnifiedDriverEventSourceService(List.of(new TachographFileSessionUnifiedDriverEventSource(repository, eventBuilder))),
|
||||
new UnifiedVehicleEventSourceService(List.of(new TachographFileSessionUnifiedVehicleEventSource(repository, eventBuilder))),
|
||||
compositeRepository,
|
||||
new EventAcquisitionRecordKeyService(),
|
||||
new EventHubEventSorter()
|
||||
);
|
||||
|
||||
DriverExtractionSession firstDriverSession = driver();
|
||||
DriverExtractionSession secondDriverSession = driverOnSecondDay();
|
||||
TachographFileSession firstSession = session(firstDriverSession);
|
||||
TachographFileSession secondSession = session(secondDriverSession);
|
||||
repository.save(firstSession);
|
||||
repository.save(secondSession);
|
||||
UUID compositeSessionId = UUID.randomUUID();
|
||||
compositeRepository.save(new TachographCompositeSession(
|
||||
compositeSessionId,
|
||||
"default",
|
||||
"runtime-composite",
|
||||
List.of(firstSession.sessionId(), secondSession.sessionId()),
|
||||
Instant.now()
|
||||
));
|
||||
|
||||
UnifiedRuntimeProcessingRequest request = UnifiedRuntimeProcessingRequest.forTachographCompositeSession(
|
||||
compositeSessionId,
|
||||
firstDriverSession.driverKey(),
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-03T00:00:00Z"),
|
||||
true,
|
||||
0
|
||||
);
|
||||
|
||||
assertThat(loader.loadDriverEvents(request)).hasSize(10);
|
||||
assertThat(loader.loadVehicleEvents(request, new UnifiedDiscoveredVehicleRef("VIN-1", "VIN-1", "12", "REG-1"))).hasSize(10);
|
||||
}
|
||||
|
||||
|
||||
private DriverExtractionSession driver() {
|
||||
return new DriverExtractionSession(
|
||||
"12:123",
|
||||
|
|
@ -125,6 +183,66 @@ class TachographFileSessionRuntimeEventLoaderTest {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private DriverExtractionSession driverOnSecondDay() {
|
||||
return new DriverExtractionSession(
|
||||
"12:123",
|
||||
new ExtractedDriver("12:123", "DRV:12:123", "Doe", "Jane", null, null, null, null, null),
|
||||
new ExtractedDriverCard("CARD:12:123", "12", "123", "123", null, null, null, null),
|
||||
List.of(new ExtractedVehicleRegistration("12:REG-1", "VR:12:REG-1", "12", "REG-1")),
|
||||
List.of(new ExtractedVehicle("VIN-1", "VIN:VIN-1", "VIN-1")),
|
||||
List.of(new ExtractedCardVehicleUsageInterval(
|
||||
"CVU-2",
|
||||
OffsetDateTime.parse("2026-05-02T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T10:00:00Z"),
|
||||
200L,
|
||||
300L,
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"vu-2"
|
||||
)),
|
||||
List.of(new ExtractedCardActivityInterval(
|
||||
"ACT-2",
|
||||
OffsetDateTime.parse("2026-05-02T08:30:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T09:00:00Z"),
|
||||
"DRIVE",
|
||||
"DRIVER",
|
||||
"INSERTED",
|
||||
"SINGLE",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"a2"
|
||||
)),
|
||||
List.of(new ExtractedSupportEvent(
|
||||
"SUP-2",
|
||||
"12:123",
|
||||
OffsetDateTime.parse("2026-05-02T08:45:00Z"),
|
||||
"POSITION",
|
||||
"POSITION_RECORDED",
|
||||
"SNAPSHOT",
|
||||
"DRIVER",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
BigDecimal.valueOf(48.2082),
|
||||
BigDecimal.valueOf(16.3738),
|
||||
"AUTHENTIC",
|
||||
250L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"raw-path-2"
|
||||
)),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private TachographFileSession session(DriverExtractionSession driver) {
|
||||
return new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
|
|
|
|||
|
|
@ -41,11 +41,17 @@ class UnifiedRuntimeDriverTimelineServiceTest {
|
|||
|
||||
ResolvedDriverTimeline timeline = service.loadDriverTimeline(
|
||||
new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
"default",
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_DB),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
"DRIVER:42",
|
||||
null,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -34,11 +34,17 @@ class UnifiedRuntimeEventAssemblyServiceTest {
|
|||
|
||||
UnifiedRuntimeEventBundle bundle = service.assembleDriverScopedEvents(
|
||||
new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
"default",
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_DB),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
"DRIVER:42",
|
||||
null,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.service.EventDetailsFactory;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class TachographEsperProcessingCoreParityTest {
|
||||
|
||||
@Test
|
||||
void fileSessionAndEventInputUseSameCoreAndProduceEquivalentProjectionSections() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
DriverTimelineBuilder driverTimelineBuilder = new DriverTimelineBuilder();
|
||||
IntervalBackedDriverTimelineEventBuilder intervalEventBuilder = new IntervalBackedDriverTimelineEventBuilder(
|
||||
driverTimelineBuilder,
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory(),
|
||||
new EventDetailsFactory(new ObjectMapper())
|
||||
);
|
||||
RawSourceDriverTimelineEventBuilder rawSourceEventBuilder = new RawSourceDriverTimelineEventBuilder(intervalEventBuilder);
|
||||
DriverTimelineReusableProjectionBuilder projectionBuilder = new DriverTimelineReusableProjectionBuilder(
|
||||
driverTimelineBuilder,
|
||||
rawSourceEventBuilder,
|
||||
properties
|
||||
);
|
||||
TachographEsperProcessingCore core = new TachographEsperProcessingCore(
|
||||
driverTimelineBuilder,
|
||||
projectionBuilder,
|
||||
properties
|
||||
);
|
||||
|
||||
DriverExtractionSession driver = driver();
|
||||
TachographFileSession session = session(driver);
|
||||
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driver);
|
||||
List<EventHubEventDto> rawEvents = rawSourceEventBuilder.buildRawEventBundle(session, driver).allEvents();
|
||||
|
||||
TachographEsperDriverProcessingResultDto fileSessionResult = core.process(TachographEsperProcessingInput.fromFileSession(
|
||||
session,
|
||||
driver,
|
||||
timeline,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-03T00:00:00Z"),
|
||||
3,
|
||||
420,
|
||||
List.of("file-session-adapter")
|
||||
));
|
||||
TachographEsperDriverProcessingResultDto eventInputResult = core.process(TachographEsperProcessingInput.fromEvents(
|
||||
session.sessionId(),
|
||||
driver.driverKey(),
|
||||
timeline,
|
||||
rawEvents,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-03T00:00:00Z"),
|
||||
3,
|
||||
420,
|
||||
List.of("runtime-adapter")
|
||||
));
|
||||
|
||||
assertThat(eventInputResult.activityIntervalCount()).isEqualTo(fileSessionResult.activityIntervalCount());
|
||||
assertThat(eventInputResult.drivingIntervalCount()).isEqualTo(fileSessionResult.drivingIntervalCount());
|
||||
assertThat(eventInputResult.vehicleUsageIntervalCount()).isEqualTo(fileSessionResult.vehicleUsageIntervalCount());
|
||||
assertThat(eventInputResult.vuCardAbsentIntervalCount()).isEqualTo(fileSessionResult.vuCardAbsentIntervalCount());
|
||||
assertThat(eventInputResult.dailyWeeklyRestCandidateIntervalCount())
|
||||
.isEqualTo(fileSessionResult.dailyWeeklyRestCandidateIntervalCount());
|
||||
assertThat(eventInputResult.dailyWeeklyRestCandidateCoverageIntervalCount())
|
||||
.isEqualTo(fileSessionResult.dailyWeeklyRestCandidateCoverageIntervalCount());
|
||||
assertThat(eventInputResult.potentialInVehicleOvernightStayIntervalCount())
|
||||
.isEqualTo(fileSessionResult.potentialInVehicleOvernightStayIntervalCount());
|
||||
assertThat(eventInputResult.potentialHomeOvernightStayIntervalCount())
|
||||
.isEqualTo(fileSessionResult.potentialHomeOvernightStayIntervalCount());
|
||||
assertThat(eventInputResult.notes()).contains("runtime-adapter");
|
||||
}
|
||||
|
||||
private DriverExtractionSession driver() {
|
||||
return new DriverExtractionSession(
|
||||
"12:123",
|
||||
null,
|
||||
null,
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(
|
||||
new ExtractedCardVehicleUsageInterval(
|
||||
"CVU-1",
|
||||
OffsetDateTime.parse("2026-05-01T08:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T23:59:59Z"),
|
||||
100L,
|
||||
200L,
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"vu-1"
|
||||
),
|
||||
new ExtractedCardVehicleUsageInterval(
|
||||
"CVU-2",
|
||||
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T10:00:00Z"),
|
||||
200L,
|
||||
260L,
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"vu-2"
|
||||
)
|
||||
),
|
||||
List.of(
|
||||
new ExtractedCardActivityInterval(
|
||||
"ACT-1",
|
||||
OffsetDateTime.parse("2026-05-01T08:30:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T09:00:00Z"),
|
||||
"DRIVE",
|
||||
"DRIVER",
|
||||
"INSERTED",
|
||||
"SINGLE",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"a"
|
||||
),
|
||||
new ExtractedCardActivityInterval(
|
||||
"ACT-2",
|
||||
OffsetDateTime.parse("2026-05-01T18:30:00Z"),
|
||||
OffsetDateTime.parse("2026-05-01T18:45:00Z"),
|
||||
"DRIVE",
|
||||
"DRIVER",
|
||||
"INSERTED",
|
||||
"SINGLE",
|
||||
"12:REG-1",
|
||||
"VIN-1",
|
||||
"b"
|
||||
)
|
||||
),
|
||||
List.of(),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private TachographFileSession session(DriverExtractionSession driver) {
|
||||
return new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
new TachographFileSessionMetadata(
|
||||
"default",
|
||||
"legalrequirements-drivercard",
|
||||
"sample",
|
||||
"sample.ddd",
|
||||
"a",
|
||||
2,
|
||||
"42",
|
||||
"b",
|
||||
true,
|
||||
null
|
||||
),
|
||||
Map.of(driver.driverKey(), driver),
|
||||
new ExtractionStats(1, 2, 2, 1, 1, 0),
|
||||
List.of(),
|
||||
Instant.now(),
|
||||
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue