Compare commits

...

8 Commits

Author SHA1 Message Date
trifonovt 82e2bd0860 Add runtime processing pipeline and validation flows 2026-05-26 15:27:29 +02:00
trifonovt 5c887e8cb2 Fix runtime processing build regressions 2026-05-26 13:17:46 +02:00
trifonovt 27b411e647 Add runtime support evidence normalization 2026-05-25 23:12:49 +02:00
trifonovt 471726c4cc Add runtime tachograph parity validation 2026-05-25 22:58:47 +02:00
trifonovt e68047feab Add runtime vehicle evidence debug output 2026-05-25 22:44:42 +02:00
trifonovt b04b333db7 Add runtime event scope processing 2026-05-25 22:42:11 +02:00
trifonovt 3bccda20e8 Add raw event tachograph projection input path 2026-05-25 16:33:59 +02:00
trifonovt 8a75db58fd Add tachograph raw payload metadata support 2026-05-25 15:58:04 +02:00
133 changed files with 14348 additions and 292 deletions

17
README_PATCH.md Normal file
View File

@ -0,0 +1,17 @@
# EventHub fix-list patch
This patch implements the requested remaining items from the fix list, excluding build verification and SQL Server 2008 SQL rewriting.
## Apply notes
1. Copy the files from this archive into the project root.
2. Delete the old migration files listed in `DELETE_FILES.txt`.
3. Run the test suite locally with Maven/Java 21.
## Main changes
- Renumbered Flyway migrations to remove duplicate `V9` and `V10` versions.
- Removed the duplicated Timescale/Event source record migration.
- Switched local Docker Compose DB from plain PostgreSQL to a TimescaleDB/PostGIS-capable image.
- Added normalized raw tachograph payload metadata for DB-extracted EventHub events.
- Added tests for Flyway version uniqueness and tachograph DB mapper → timeline reconstruction metadata.

View File

@ -1,6 +1,9 @@
services:
postgres:
image: postgres:16
# The Flyway migrations enable both TimescaleDB and PostGIS. The TimescaleDB HA
# image is used for local development because it includes these PostgreSQL
# extensions, unlike the plain postgres image.
image: timescale/timescaledb-ha:pg16
container_name: eventhub-postgres
environment:
POSTGRES_DB: eventhub

View File

@ -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.

View File

@ -0,0 +1,70 @@
# Runtime driver working-time processing
Runtime driver working-time processing is a source-neutral processing plan over canonical EventHub events.
The preferred runtime execution endpoint is:
```http
POST /api/eventhub/runtime-processing/executions
```
Use:
```json
{
"processingPlanKey": "driver-working-time-v1"
}
```
## Canonical input idea
The plan should work with events from any source once they are normalized into canonical EventHub event semantics:
```text
DRIVER_ACTIVITY START/END
DRIVER_CARD or DRIVER_VEHICLE_USAGE INSERT/WITHDRAW
POSITION / PLACE / BORDER_CROSSING / LOAD_UNLOAD / IGNITION / ODOMETER support evidence
```
Tachograph files and tachograph databases are only two possible sources. YellowFox and future telematics providers can contribute vehicle-only evidence that is attached to driver partitions by vehicle/time overlap.
## New source-neutral artifacts
```text
DriverWorkingTimeProcessingResultDto
DriverWorkingTimeProcessingCore
RuntimeDriverWorkingTimeScopeProcessingService
DriverWorkingTimeRuntimeProcessingPlan
runtime-driver-event-interval-preprocessor.epl
driver-working-time-derived-projections.epl
```
## Compatibility artifacts
The following names are kept only for backward compatibility with existing file-session APIs and older Postman calls:
```text
TachographEsperDriverProcessingResultDto
TachographEsperProcessingCore
UnifiedRuntimeTachographEsperScopeProcessingService
tachograph-driving-derived-projection-events-preprocessor.epl
tachograph-driving-derived-projection-bundle.epl
```
New runtime-processing code should use the driver-working-time names.
## EPL-backed phase modules
The driver working-time plan now contains first-class EPL-backed phase modules for event-to-interval conversion:
```text
event-to-activity-intervals
EventHub DRIVER_ACTIVITY START/END events
-> DriverActivityIntervalEvent
event-to-vehicle-usage-intervals
EventHub DRIVER_CARD INSERT/WITHDRAW events
-> DriverVehicleUsageIntervalEvent
```
The final derived projection module still delegates to the shared working-time projection service for parity with existing file-session processing. This lets us migrate the pipeline gradually: common event-to-interval conversion is now EPL-module based, while the larger rest/trip/overnight projection bundle remains compatibility-safe.

View File

@ -0,0 +1,189 @@
# Runtime event processing
Runtime processing is now centered on **processing executions**. An execution loads canonical EventHub runtime events from selected sources, optionally partitions the event set, then runs a configured processing plan made of common EPL and/or Java modules.
The preferred endpoint is:
```http
POST /api/eventhub/runtime-processing/executions
```
List available plans:
```http
GET /api/eventhub/runtime-processing/executions/plans
```
The older endpoint is still available for compatibility:
```http
POST /api/eventhub/runtime-processing/event-processing
```
but it should be treated as a legacy profile adapter. New clients should use `processingPlanKey`, not `profileKey`.
## First predefined plan
The first predefined plan is:
```text
driver-working-time-v1
```
Legacy alias:
```text
tachograph-driver-esper-v1
```
This plan runs the source-neutral driver working-time chain over canonical runtime events. Tachograph file/database data is only one possible source of those canonical events. Its modules include:
```text
event-to-activity-intervals
event-to-vehicle-usage-intervals
vehicle-evidence-attachment
support-evidence-normalization
driving-derived-projections
```
The important design point is that runtime processing is not tachograph-specific. Tachograph is now treated as a source/compatibility layer; the working-time plan consumes canonical driver activity, vehicle usage, and support-evidence events from any supported source.
## Example execution
```json
{
"processingPlanKey": "driver-working-time-v1",
"sourceSelection": {
"sessionIds": [
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222"
],
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION", "YELLOWFOX_DB"],
"occurredFrom": "2026-05-01T00:00:00Z",
"occurredTo": "2026-05-31T23:59:59Z",
"expandVehicleEvents": true
},
"partitioning": {
"strategy": "DRIVER",
"includeAllPartitions": true,
"attachVehicleEvidence": true,
"vehicleEvidencePaddingMinutes": 15,
"includeDebug": true
},
"parameters": {
"significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720,
"includePartitionDebug": true
}
}
```
## Conceptual flow
```text
source selection
-> runtime event loaders
-> canonical EventHub events
-> partitioning
-> vehicle evidence attachment
-> support evidence normalization
-> event-to-interval EPL modules
-> driver working-time derived projection EPL/Java modules
-> partition result map
```
## Why processing plans instead of profiles
A plan describes **which modules should run over a runtime event set**. This matches the EventHub goal better than domain-oriented profiles.
Future plans can reuse the same event-loading infrastructure:
```text
vehicle-trip-detection-v1
vehicle-stop-detection-v1
driver-settlement-v1
mixed-driver-vehicle-correlation-v1
telematics-poi-clustering-v1
```
Each plan may use different partitioning and modules, but the source loading and canonical event model remain common.
## Compatibility
Legacy profile endpoint:
```http
POST /api/eventhub/runtime-processing/event-processing
```
with:
```json
{
"profileKey": "tachograph-driver-esper-v1",
"scope": {},
"partitioning": {},
"parameters": {}
}
```
internally delegates to:
```text
processingPlanKey = driver-working-time-v1
```
## Source-neutral driver working-time modules
The former tachograph-named processing artifacts are now represented by source-neutral driver working-time names:
```text
runtime-driver-event-interval-preprocessor.epl
driver-working-time-derived-projections.epl
DriverWorkingTimeProcessingResultDto
DriverWorkingTimeProcessingCore
RuntimeDriverWorkingTimeScopeProcessingService
```
Legacy tachograph names are kept as compatibility adapters for existing file-session endpoints and Postman requests. New runtime code should use the `driver-working-time-*` classes/resources and the `driver-working-time-v1` processing plan.
## Module execution results
`/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly.
Top-level execution response includes:
```text
moduleResults
```
This map contains one entry per executed module. The top-level entries are intentionally sanitized: they expose status, warnings, and metadata without duplicating the full partition output payload.
Each partition result also includes:
```text
partitionResults[*].moduleResults
```
For `driver-working-time-v1`, the partition-level module results currently expose:
```text
vehicle-evidence-attachment
support-evidence-normalization
driving-derived-projections
```
The first implementation uses wrapper modules around the existing validated driver-working-time scope service. Logical phase modules such as `event-to-activity-intervals`, `event-to-vehicle-usage-intervals`, and `vehicle-usage-merge` are registered and executed as delegated modules so they can later be split into true standalone EPL/Java modules without changing the external processing-plan contract.
## First-class EPL modules
Runtime processing now supports modules implemented directly with Esper EPL through the common `RuntimeEplModuleExecutor`.
The first source-neutral EPL modules are:
| Module key | Engine | EPL resource | Output statement |
|---|---|---|---|
| `event-to-activity-intervals` | `EPL` | `esper/runtime-driver-activity-intervals.epl` | `driverActivityIntervals` |
| `event-to-vehicle-usage-intervals` | `EPL` | `esper/runtime-driver-vehicle-usage-intervals.epl` | `driverVehicleUsageIntervals` |
These modules operate on canonical EventHub runtime events, not on tachograph-specific source rows. They are currently used as first-class phase modules in `driver-working-time-v1`; the final `driving-derived-projections` module remains a compatibility adapter over the validated working-time projection service until the remaining projection stages are split into direct EPL modules.

View File

@ -0,0 +1,53 @@
# Runtime tachograph Esper scope processing
This document is kept for compatibility. The preferred architecture is now the common Runtime Processing execution model.
Preferred endpoint:
```http
POST /api/eventhub/runtime-processing/executions
```
Use:
```json
{
"processingPlanKey": "driver-working-time-v1"
}
```
Legacy compatibility endpoint:
```http
POST /api/eventhub/runtime-processing/tachograph/esper-processing
```
still exists and delegates through the common runtime processing infrastructure.
Legacy profile endpoint:
```http
POST /api/eventhub/runtime-processing/event-processing
```
with:
```json
{
"profileKey": "tachograph-driver-esper-v1"
}
```
also remains available, but new clients should use `processingPlanKey` and `/executions`.
The current `driver-working-time-v1` plan uses these modules:
```text
event-to-activity-intervals
event-to-vehicle-usage-intervals
vehicle-evidence-attachment
support-evidence-normalization
driving-derived-projections
```
It can load runtime events from multiple sources and sessions, partition them by driver, attach vehicle-only evidence by vehicle/time overlap, normalize support evidence, and run the existing tachograph Esper/Java processing chain per driver partition.

View File

@ -0,0 +1,552 @@
{
"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. Includes plan execution, compatibility profile execution, validation endpoints, and legacy runtime diagnostic endpoints (driver-events, driver-timeline, driver-derived-projections)."
},
"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 processing plans",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions/plans",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"executions",
"plans"
]
}
}
},
{
"name": "Execute processing plan - driver working time single session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"executions"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": false,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
}
}
},
{
"name": "Execute processing plan - driver working time multiple sessions all drivers",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"executions"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
}
}
},
{
"name": "Execute processing plan - driver working time composite session all drivers",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"executions"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
}
}
},
{
"name": "Execute processing plan - driver working time source DB mixed sources",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"executions"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{driverSourceEntityId}}\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": false,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
}
}
},
{
"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 \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\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 \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\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 \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\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"
]
}
}
},
{
"name": "Validate tachograph profile parity - single session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/validation/tachograph-parity",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"event-processing",
"validation",
"tachograph-parity"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"driverKey\": \"{{driverKey}}\",\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 \"includeDebug\": true\n}"
}
}
},
{
"name": "Validate mixed-source vehicle evidence attachment and normalization",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"processingRequest\": {\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId1}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\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 \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n },\n \"minimumAttachedVehicleEvidenceEvents\": 1,\n \"minimumNormalizedSupportEvidenceEvents\": 1,\n \"failWhenPartitionDebugMissing\": true\n}"
},
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/validation/mixed-source-evidence",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"event-processing",
"validation",
"mixed-source-evidence"
]
}
}
},
{
"name": "Runtime diagnostics - driver events from single file session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-events"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
}
}
},
{
"name": "Runtime diagnostics - driver timeline from single file session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-timeline"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
}
}
},
{
"name": "Runtime diagnostics - driver derived projections from single file session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-derived-projections",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-derived-projections"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}"
}
}
},
{
"name": "Runtime diagnostics - driver events from multiple file sessions",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-events"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
}
}
},
{
"name": "Runtime diagnostics - driver timeline from composite session",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-timeline"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
}
}
},
{
"name": "Runtime diagnostics - driver events from source DB",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-events"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{driverSourceEntityId}}\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
}
}
},
{
"name": "Runtime diagnostics - driver timeline from EventHub DB",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"eventhub",
"runtime-processing",
"driver-timeline"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
}
}
}
]
}

View File

@ -357,6 +357,7 @@ public class EventHubProperties {
public static class Processing {
private TimelineInputMode timelineInputMode = TimelineInputMode.INTERVALS;
private DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode = DrivingDerivedProjectionInputMode.INTERVALS;
private int operatingSplitIdleHours = 7;
private int significantDrivingMinutes = 3;
private int minimumRestPeriodMinutes = 720;
@ -377,6 +378,16 @@ public class EventHubProperties {
}
}
public DrivingDerivedProjectionInputMode getDrivingDerivedProjectionInputMode() {
return drivingDerivedProjectionInputMode;
}
public void setDrivingDerivedProjectionInputMode(DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode) {
if (drivingDerivedProjectionInputMode != null) {
this.drivingDerivedProjectionInputMode = drivingDerivedProjectionInputMode;
}
}
public int getOperatingSplitIdleHours() {
return operatingSplitIdleHours;
}
@ -456,6 +467,14 @@ public class EventHubProperties {
EVENTS
}
public enum DrivingDerivedProjectionInputMode {
/** Existing stable path: Java resolves intervals and EPL receives interval input streams. */
INTERVALS,
/** New path: EPL receives EventHub point events and reconstructs interval streams internally. */
EVENTS
}
public static class LegalRequirements {
private static final String DEFAULT_BASE_URL = "https://legalrequirements.services.bytebar.eu/ODataV4/LR";

View File

@ -0,0 +1,24 @@
package at.procon.eventhub.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcJsonConfig implements WebMvcConfigurer {
private final ObjectMapper objectMapper;
public WebMvcJsonConfig(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance);
converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
}
}

View File

@ -1,15 +1,36 @@
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.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionService;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingPlanDescriptorDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationApiRequest;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationResultDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationService;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationApiRequest;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService;
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.RuntimeDriverWorkingTimeScopeProcessingService;
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 +38,64 @@ public class UnifiedRuntimeProcessingController {
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService;
private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService;
private final RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService;
private final RuntimeEventProcessingService runtimeEventProcessingService;
private final RuntimeProcessingExecutionService runtimeProcessingExecutionService;
private final RuntimeTachographParityValidationService tachographParityValidationService;
private final RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService;
public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService
) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null, null, null);
}
public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService
) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
tachographEsperScopeProcessingService, runtimeEventProcessingService, null, null, null);
}
@Autowired
public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService,
RuntimeTachographParityValidationService tachographParityValidationService,
RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService,
RuntimeProcessingExecutionService runtimeProcessingExecutionService
) {
this.eventAssemblyService = eventAssemblyService;
this.runtimeDriverTimelineService = runtimeDriverTimelineService;
this.runtimeDerivedProjectionService = runtimeDerivedProjectionService;
this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService;
this.runtimeEventProcessingService = runtimeEventProcessingService;
this.runtimeProcessingExecutionService = runtimeProcessingExecutionService;
this.tachographParityValidationService = tachographParityValidationService;
this.mixedSourceEvidenceValidationService = mixedSourceEvidenceValidationService;
}
public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService,
RuntimeTachographParityValidationService tachographParityValidationService
) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
tachographEsperScopeProcessingService, runtimeEventProcessingService,
tachographParityValidationService, null, null);
}
@PostMapping("/driver-events")
@ -39,4 +111,88 @@ 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("/executions/plans")
public ResponseEntity<List<RuntimeProcessingPlanDescriptorDto>> listRuntimeProcessingPlans() {
if (runtimeProcessingExecutionService == null) {
throw new IllegalStateException("Runtime processing execution service is not configured.");
}
return ResponseEntity.ok(runtimeProcessingExecutionService.listPlans());
}
@PostMapping("/executions")
public ResponseEntity<RuntimeProcessingExecutionResultDto> runRuntimeProcessingExecution(
@RequestBody RuntimeProcessingExecutionApiRequest request
) {
if (runtimeProcessingExecutionService == null) {
throw new IllegalStateException("Runtime processing execution service is not configured.");
}
return ResponseEntity.ok(runtimeProcessingExecutionService.execute(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("/event-processing/validation/tachograph-parity")
public ResponseEntity<RuntimeTachographParityValidationResultDto> validateTachographParity(
@RequestBody RuntimeTachographParityValidationApiRequest request
) {
if (tachographParityValidationService == null) {
throw new IllegalStateException("Runtime tachograph parity validation service is not configured.");
}
return ResponseEntity.ok(tachographParityValidationService.validate(request));
}
@PostMapping("/event-processing/validation/mixed-source-evidence")
public ResponseEntity<RuntimeMixedSourceEvidenceValidationResultDto> validateMixedSourceEvidence(
@RequestBody RuntimeMixedSourceEvidenceValidationApiRequest request
) {
if (mixedSourceEvidenceValidationService == null) {
throw new IllegalStateException("Runtime mixed-source evidence validation service is not configured.");
}
return ResponseEntity.ok(mixedSourceEvidenceValidationService.validate(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(UnifiedRuntimeTachographEsperScopeResultDto.fromDriverWorkingTime(
tachographEsperScopeProcessingService.processScope(request)
));
}
}

View File

@ -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);

View File

@ -0,0 +1,52 @@
package at.procon.eventhub.processing.driverworkingtime.dto;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
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 java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public record DriverWorkingTimeProcessingResultDto(
UUID sessionId,
String driverKey,
String sourceKind,
OffsetDateTime loadedFrom,
OffsetDateTime loadedTo,
OffsetDateTime requestedFrom,
OffsetDateTime requestedTo,
int activityIntervalCount,
int drivingIntervalCount,
int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int dailyWeeklyRestCandidateCoverageIntervalCount,
int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int potentialInVehicleOvernightStayIntervalCount,
int potentialInVehicleTripIntervalCount,
int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount,
int supportGeoEventCount,
List<TachographEsperActivityIntervalEvent> activityIntervals,
List<TachographEsperActivityIntervalEvent> drivingIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals,
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals,
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals,
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals,
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals,
List<TachographEsperVehicleUsageIntervalEvent> vehicleUsageIntervals,
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals,
List<TachographEsperSupportGeoEvent> supportGeoEvents,
List<String> notes
) {
}

View File

@ -0,0 +1,28 @@
package at.procon.eventhub.processing.driverworkingtime.service;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
import org.springframework.stereotype.Service;
/**
* Source-neutral driver working-time processing core.
*
* <p>Tachograph file/database data is only one source of the canonical driver activity,
* vehicle-usage, and support-evidence event streams consumed here. The legacy
* TachographEsperProcessingCore delegates to the same processing logic for backward
* compatibility.</p>
*/
@Service
public class DriverWorkingTimeProcessingCore {
private final TachographEsperProcessingCore delegate;
public DriverWorkingTimeProcessingCore(TachographEsperProcessingCore delegate) {
this.delegate = delegate;
}
public DriverWorkingTimeProcessingResultDto process(TachographEsperProcessingInput input) {
return delegate.processDriverWorkingTime(input);
}
}

View File

@ -0,0 +1,24 @@
package at.procon.eventhub.processing.dto;
import java.util.List;
public record RuntimeDriverPartitionDebugDto(
String driverKey,
int directDriverEventCount,
int vehicleUsageIntervalCount,
int candidateVehicleEvidenceEventCount,
int attachedVehicleEvidenceEventCount,
int ignoredVehicleEvidenceEventCount,
int mergedEventCount,
List<RuntimeVehicleUsageIntervalDebugDto> vehicleUsageIntervals,
List<RuntimeVehicleEvidenceAttachmentDecisionDto> vehicleEvidenceDecisions,
List<String> notes,
List<String> warnings
) {
public RuntimeDriverPartitionDebugDto {
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
vehicleEvidenceDecisions = vehicleEvidenceDecisions == null ? List.of() : List.copyOf(vehicleEvidenceDecisions);
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,17 @@
package at.procon.eventhub.processing.dto;
import java.util.List;
public record RuntimeSupportEvidenceNormalizationDebugDto(
int inputEventCount,
int normalizedSupportEvidenceEventCount,
int unchangedEventCount,
List<String> notes
) {
public RuntimeSupportEvidenceNormalizationDebugDto {
inputEventCount = Math.max(0, inputEventCount);
normalizedSupportEvidenceEventCount = Math.max(0, normalizedSupportEvidenceEventCount);
unchangedEventCount = Math.max(0, unchangedEventCount);
notes = notes == null ? List.of() : List.copyOf(notes);
}
}

View File

@ -0,0 +1,25 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Set;
public record RuntimeVehicleEvidenceAttachmentDecisionDto(
String decision,
String reason,
String eventKey,
String externalSourceEventId,
OffsetDateTime occurredAt,
String eventDomain,
String eventType,
String lifecycle,
RuntimeEventScopeType scopeType,
Set<String> vehicleKeys,
List<String> matchingVehicleUsageIntervalIds
) {
public RuntimeVehicleEvidenceAttachmentDecisionDto {
vehicleKeys = vehicleKeys == null ? Set.of() : Set.copyOf(vehicleKeys);
matchingVehicleUsageIntervalIds = matchingVehicleUsageIntervalIds == null ? List.of() : List.copyOf(matchingVehicleUsageIntervalIds);
}
}

View File

@ -0,0 +1,27 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import java.time.OffsetDateTime;
public record RuntimeVehicleUsageIntervalDebugDto(
String intervalId,
OffsetDateTime from,
OffsetDateTime to,
String registrationKey,
String vehicleKey,
String sourceKind
) {
public static RuntimeVehicleUsageIntervalDebugDto from(ResolvedVehicleUsageInterval interval) {
if (interval == null) {
return null;
}
return new RuntimeVehicleUsageIntervalDebugDto(
interval.intervalId(),
interval.from(),
interval.to(),
interval.registrationKey(),
interval.vehicleKey(),
interval.sourceKind()
);
}
}

View File

@ -0,0 +1,90 @@
package at.procon.eventhub.processing.dto;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import java.util.List;
public record UnifiedRuntimeDerivedProjectionResultDto(
UnifiedRuntimeProcessingRequest request,
int driverSeedEventCount,
int discoveredVehicleCount,
int expandedVehicleEventCount,
int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
DriverWorkingTimeProcessingResultDto projection,
List<String> notes,
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization,
RuntimeDriverPartitionDebugDto partitionDebug
) {
public UnifiedRuntimeDerivedProjectionResultDto {
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
notes = notes == null ? List.of() : List.copyOf(notes);
}
public UnifiedRuntimeDerivedProjectionResultDto(
UnifiedRuntimeProcessingRequest request,
int driverSeedEventCount,
int discoveredVehicleCount,
int expandedVehicleEventCount,
int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
DriverWorkingTimeProcessingResultDto projection,
List<String> notes
) {
this(
request,
driverSeedEventCount,
discoveredVehicleCount,
expandedVehicleEventCount,
mergedEventCount,
discoveredVehicles,
projection,
notes,
null,
null
);
}
public UnifiedRuntimeDerivedProjectionResultDto(
UnifiedRuntimeProcessingRequest request,
int driverSeedEventCount,
int discoveredVehicleCount,
int expandedVehicleEventCount,
int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
DriverWorkingTimeProcessingResultDto projection,
List<String> notes,
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization
) {
this(
request,
driverSeedEventCount,
discoveredVehicleCount,
expandedVehicleEventCount,
mergedEventCount,
discoveredVehicles,
projection,
notes,
supportEvidenceNormalization,
null
);
}
public UnifiedRuntimeDerivedProjectionResultDto withPartitionDebug(RuntimeDriverPartitionDebugDto debug) {
return new UnifiedRuntimeDerivedProjectionResultDto(
request,
driverSeedEventCount,
discoveredVehicleCount,
expandedVehicleEventCount,
mergedEventCount,
discoveredVehicles,
projection,
notes,
supportEvidenceNormalization,
debug
);
}
}

View File

@ -0,0 +1,74 @@
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 UnifiedRuntimeDriverWorkingTimeScopeResultDto(
UnifiedRuntimeProcessingRequest request,
int inputEventCount,
int selectedDriverCount,
int discoveredVehicleCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults,
Map<String, RuntimeDriverPartitionDebugDto> partitionDebugByDriver,
List<String> notes,
List<String> warnings
) {
public UnifiedRuntimeDriverWorkingTimeScopeResultDto {
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
driverResults = driverResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(driverResults));
partitionDebugByDriver = partitionDebugByDriver == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionDebugByDriver));
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
public static UnifiedRuntimeDriverWorkingTimeScopeResultDto 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 UnifiedRuntimeDriverWorkingTimeScopeResultDto(
genericResult.request(),
genericResult.inputEventCount(),
genericResult.selectedPartitionCount(),
genericResult.discoveredVehicleCount(),
genericResult.discoveredVehicles(),
driverResults,
extractPartitionDebug(genericResult),
genericResult.notes(),
genericResult.warnings()
);
}
private static Map<String, RuntimeDriverPartitionDebugDto> extractPartitionDebug(
RuntimeEventProcessingResultDto genericResult
) {
LinkedHashMap<String, RuntimeDriverPartitionDebugDto> debugByDriver = new LinkedHashMap<>();
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : genericResult.partitionResults().entrySet()) {
Object debug = entry.getValue().metadata().get("partitionDebug");
if (debug instanceof RuntimeDriverPartitionDebugDto partitionDebug) {
debugByDriver.put(entry.getKey(), partitionDebug);
}
}
return debugByDriver;
}
}

View File

@ -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,

View File

@ -0,0 +1,93 @@
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,
Map<String, RuntimeDriverPartitionDebugDto> partitionDebugByDriver,
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));
partitionDebugByDriver = partitionDebugByDriver == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionDebugByDriver));
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
public static UnifiedRuntimeTachographEsperScopeResultDto fromDriverWorkingTime(
UnifiedRuntimeDriverWorkingTimeScopeResultDto result
) {
if (result == null) {
return null;
}
return new UnifiedRuntimeTachographEsperScopeResultDto(
result.request(),
result.inputEventCount(),
result.selectedDriverCount(),
result.discoveredVehicleCount(),
result.discoveredVehicles(),
result.driverResults(),
result.partitionDebugByDriver(),
result.notes(),
result.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,
extractPartitionDebug(genericResult),
genericResult.notes(),
genericResult.warnings()
);
}
private static Map<String, RuntimeDriverPartitionDebugDto> extractPartitionDebug(
RuntimeEventProcessingResultDto genericResult
) {
LinkedHashMap<String, RuntimeDriverPartitionDebugDto> debugByDriver = new LinkedHashMap<>();
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : genericResult.partitionResults().entrySet()) {
Object debug = entry.getValue().metadata().get("partitionDebug");
if (debug instanceof RuntimeDriverPartitionDebugDto partitionDebug) {
debugByDriver.put(entry.getKey(), partitionDebug);
}
}
return debugByDriver;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,51 @@
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,
Boolean includeDebug
) {
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);
}
public boolean includeDebugOrDefault() {
return includeDebug != null && includeDebug;
}
}

View File

@ -0,0 +1,49 @@
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, 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,
null
),
Map.of()
);
}
}

View File

@ -0,0 +1,30 @@
package at.procon.eventhub.processing.eventprocessing.dto;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
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,
Map<String, RuntimeProcessingModuleResult> moduleResults
) {
public RuntimeEventProcessingPartitionResultDto {
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
moduleResults = moduleResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(moduleResults));
}
public RuntimeEventProcessingPartitionResultDto(
String partitionType,
String partitionKey,
String resultType,
Object result,
Map<String, Object> metadata
) {
this(partitionType, partitionKey, resultType, result, metadata, Map.of());
}
}

View File

@ -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()
);
}
}

View File

@ -0,0 +1,52 @@
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.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
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);
}
public static RuntimeEventProcessingResultDto fromExecution(
RuntimeProcessingExecutionResultDto result,
String legacyProfileKey
) {
if (result == null) {
throw new IllegalArgumentException("result must not be null");
}
return new RuntimeEventProcessingResultDto(
legacyProfileKey == null || legacyProfileKey.isBlank() ? result.processingPlanKey() : legacyProfileKey,
result.partitioningStrategy(),
result.request(),
result.inputEventCount(),
result.selectedPartitionCount(),
result.discoveredVehicleCount(),
result.discoveredVehicles(),
result.partitionResults(),
result.notes(),
result.warnings()
);
}
}

View File

@ -0,0 +1,60 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
abstract class AbstractDriverWorkingTimePhaseModule implements RuntimeProcessingModule {
private final String moduleKey;
private final String displayName;
private final String description;
private final String engine;
private final Set<String> outputTypes;
protected AbstractDriverWorkingTimePhaseModule(
String moduleKey,
String displayName,
String description,
String engine,
Set<String> outputTypes
) {
this.moduleKey = moduleKey;
this.displayName = displayName;
this.description = description;
this.engine = engine;
this.outputTypes = outputTypes == null ? Set.of() : Set.copyOf(outputTypes);
}
@Override
public String moduleKey() {
return moduleKey;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey,
displayName,
description,
engine,
outputTypes
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("executionModel", "delegated");
metadata.put("delegatedTo", DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
metadata.put("note", "This logical module is currently executed inside the driver-working-time derived projections adapter. It is registered separately so it can be split into a standalone EPL/Java module without changing the processing plan contract.");
return new RuntimeProcessingModuleResult(
moduleKey,
RuntimeProcessingModuleStatus.SUCCESS,
null,
metadata,
java.util.List.of()
);
}
}

View File

@ -0,0 +1,82 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.eventprocessing.module.epl.DriverWorkingTimeEplEventMapper;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleDefinition;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutionResult;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutor;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DriverActivityIntervalsModule implements RuntimeEplModule {
public static final String OUTPUT_STATEMENT = "driverActivityIntervals";
private static final String EPL_RESOURCE = "esper/runtime-driver-activity-intervals.epl";
private final RuntimeEplModuleExecutor eplModuleExecutor;
@Autowired
public DriverActivityIntervalsModule(RuntimeEplModuleExecutor eplModuleExecutor) {
this.eplModuleExecutor = eplModuleExecutor;
}
/** Compatibility constructor for legacy tests that instantiate a local module registry. */
public DriverActivityIntervalsModule() {
this(new RuntimeEplModuleExecutor());
}
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.EVENT_TO_ACTIVITY_INTERVALS;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Event to activity intervals",
"EPL module that pairs canonical DRIVER_ACTIVITY START/END point events into source-neutral driver activity intervals.",
engine(),
Set.of("DriverActivityIntervalEvent")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
List<EventHubEventDto> sourceEvents = DriverWorkingTimeEplEventMapper.sourceEvents(context);
List<Map<String, Object>> pointEvents = DriverWorkingTimeEplEventMapper.activityPointEvents(sourceEvents);
RuntimeEplModuleExecutionResult eplResult = eplModuleExecutor.execute(new RuntimeEplModuleDefinition(
moduleKey(),
Map.of(
DriverWorkingTimeEplEventMapper.DRIVER_ACTIVITY_POINT_EVENT,
DriverWorkingTimeEplEventMapper.activityPointEventDefinition()
),
List.of(EPL_RESOURCE),
List.of(OUTPUT_STATEMENT),
List.of(new RuntimeEplInputEventStream(
DriverWorkingTimeEplEventMapper.DRIVER_ACTIVITY_POINT_EVENT,
pointEvents
))
));
List<Map<String, Object>> intervals = eplResult.output(OUTPUT_STATEMENT);
Map<String, Object> metadata = new LinkedHashMap<>(eplResult.metadata());
metadata.put("inputEventCount", sourceEvents.size());
metadata.put("activityPointEventCount", pointEvents.size());
metadata.put("activityIntervalCount", intervals.size());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
intervals,
metadata,
List.of()
);
}
}

View File

@ -0,0 +1,82 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.eventprocessing.module.epl.DriverWorkingTimeEplEventMapper;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleDefinition;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutionResult;
import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutor;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DriverVehicleUsageIntervalsModule implements RuntimeEplModule {
public static final String OUTPUT_STATEMENT = "driverVehicleUsageIntervals";
private static final String EPL_RESOURCE = "esper/runtime-driver-vehicle-usage-intervals.epl";
private final RuntimeEplModuleExecutor eplModuleExecutor;
@Autowired
public DriverVehicleUsageIntervalsModule(RuntimeEplModuleExecutor eplModuleExecutor) {
this.eplModuleExecutor = eplModuleExecutor;
}
/** Compatibility constructor for legacy tests that instantiate a local module registry. */
public DriverVehicleUsageIntervalsModule() {
this(new RuntimeEplModuleExecutor());
}
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Event to vehicle usage intervals",
"EPL module that pairs canonical driver/card INSERT/WITHDRAW point events into source-neutral driver vehicle-usage intervals.",
engine(),
Set.of("DriverVehicleUsageIntervalEvent")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
List<EventHubEventDto> sourceEvents = DriverWorkingTimeEplEventMapper.sourceEvents(context);
List<Map<String, Object>> pointEvents = DriverWorkingTimeEplEventMapper.vehicleUsagePointEvents(sourceEvents);
RuntimeEplModuleExecutionResult eplResult = eplModuleExecutor.execute(new RuntimeEplModuleDefinition(
moduleKey(),
Map.of(
DriverWorkingTimeEplEventMapper.DRIVER_VEHICLE_USAGE_POINT_EVENT,
DriverWorkingTimeEplEventMapper.vehicleUsagePointEventDefinition()
),
List.of(EPL_RESOURCE),
List.of(OUTPUT_STATEMENT),
List.of(new RuntimeEplInputEventStream(
DriverWorkingTimeEplEventMapper.DRIVER_VEHICLE_USAGE_POINT_EVENT,
pointEvents
))
));
List<Map<String, Object>> intervals = eplResult.output(OUTPUT_STATEMENT);
Map<String, Object> metadata = new LinkedHashMap<>(eplResult.metadata());
metadata.put("inputEventCount", sourceEvents.size());
metadata.put("vehicleUsagePointEventCount", pointEvents.size());
metadata.put("vehicleUsageIntervalCount", intervals.size());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
intervals,
metadata,
List.of()
);
}
}

View File

@ -0,0 +1,18 @@
package at.procon.eventhub.processing.eventprocessing.module;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class DriverVehicleUsageMergeModule extends AbstractDriverWorkingTimePhaseModule {
public DriverVehicleUsageMergeModule() {
super(
DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE,
"Vehicle usage merge",
"Merges adjacent or continuous same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations.",
"JAVA/ESPER",
Set.of("DriverVehicleUsageIntervalInputEvent")
);
}
}

View File

@ -0,0 +1,77 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcessingModule {
private final RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService;
public DriverWorkingTimeDerivedProjectionsModule(
RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService
) {
this.scopeProcessingService = scopeProcessingService;
}
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Driving-derived projections",
"Executes the shared driver working-time pipeline for driving interruptions, rest candidates, card-absence coverage, overnight candidates, and trip candidates.",
"ESPER+JAVA",
Set.of("DriverWorkingTimeProcessingResultDto")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false);
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = scopeProcessingService.processScope(
scopeRequest,
includePartitionDebug
);
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("inputEventCount", result.inputEventCount());
metadata.put("selectedDriverCount", result.selectedDriverCount());
metadata.put("discoveredVehicleCount", result.discoveredVehicleCount());
metadata.put("driverResultCount", result.driverResults().size());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
result,
metadata,
result.warnings()
);
}
private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
Object value = context.attributes().get("runtimeScopeApiRequest");
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
return request;
}
return context.request().sourceSelection();
}
private boolean booleanAttribute(RuntimeProcessingModuleContext context, String key, boolean fallback) {
Object value = context.attributes().get(key);
if (value instanceof Boolean booleanValue) {
return booleanValue;
}
return fallback;
}
}

View File

@ -0,0 +1,15 @@
package at.procon.eventhub.processing.eventprocessing.module;
public final class DriverWorkingTimeModuleKeys {
public static final String RUNTIME_EVENT_ASSEMBLY = "runtime-event-assembly";
public static final String EVENT_TO_ACTIVITY_INTERVALS = "event-to-activity-intervals";
public static final String EVENT_TO_VEHICLE_USAGE_INTERVALS = "event-to-vehicle-usage-intervals";
public static final String VEHICLE_USAGE_MERGE = "vehicle-usage-merge";
public static final String VEHICLE_EVIDENCE_ATTACHMENT = "vehicle-evidence-attachment";
public static final String SUPPORT_EVIDENCE_NORMALIZATION = "support-evidence-normalization";
public static final String DRIVING_DERIVED_PROJECTIONS = "driving-derived-projections";
private DriverWorkingTimeModuleKeys() {
}
}

View File

@ -0,0 +1,62 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class RuntimeEventAssemblyModule implements RuntimeProcessingModule {
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
public RuntimeEventAssemblyModule(UnifiedRuntimeEventAssemblyService eventAssemblyService) {
this.eventAssemblyService = eventAssemblyService;
}
@Override
public String moduleKey() {
return DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY;
}
@Override
public RuntimeProcessingModuleDescriptorDto descriptor() {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"Runtime event assembly",
"Loads and merges canonical runtime events from the selected source scope before plan-specific processing.",
"JAVA",
Set.of("UnifiedRuntimeEventBundle")
);
}
@Override
public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) {
UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context);
UnifiedRuntimeEventBundle bundle = eventAssemblyService.assembleDriverScopedEvents(scopeRequest.toRuntimeRequest());
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("driverSeedEventCount", bundle.driverSeedEvents().size());
metadata.put("expandedVehicleEventCount", bundle.expandedVehicleEvents().size());
metadata.put("mergedEventCount", bundle.mergedEvents().size());
metadata.put("discoveredVehicleCount", bundle.discoveredVehicles().size());
return new RuntimeProcessingModuleResult(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,
bundle,
metadata,
bundle.notes()
);
}
private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) {
Object value = context.attributes().get("runtimeScopeApiRequest");
if (value instanceof UnifiedRuntimeProcessingApiRequest request) {
return request;
}
return context.request().sourceSelection();
}
}

View File

@ -0,0 +1,20 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
/**
* Source-neutral runtime processing module.
*
* <p>Modules are the executable building blocks used by processing plans. A module can be
* implemented in EPL, Java, or a combination of both. The first predefined driver-working-time
* plan still uses an adapter over the existing processing services, but its descriptor now
* exposes the logical module sequence so the plan can be split into concrete modules incrementally.</p>
*/
public interface RuntimeProcessingModule {
String moduleKey();
RuntimeProcessingModuleDescriptorDto descriptor();
RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context);
}

View File

@ -0,0 +1,21 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record RuntimeProcessingModuleContext(
RuntimeProcessingExecutionApiRequest request,
List<EventHubEventDto> events,
Map<String, Object> attributes,
Map<String, RuntimeProcessingModuleResult> previousResults
) {
public RuntimeProcessingModuleContext {
events = events == null ? List.of() : List.copyOf(events);
attributes = attributes == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
previousResults = previousResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(previousResults));
}
}

View File

@ -0,0 +1,48 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
public class RuntimeProcessingModuleRegistry {
private final Map<String, RuntimeProcessingModule> modulesByKey;
public RuntimeProcessingModuleRegistry(List<RuntimeProcessingModule> modules) {
LinkedHashMap<String, RuntimeProcessingModule> byKey = new LinkedHashMap<>();
if (modules != null) {
for (RuntimeProcessingModule module : modules) {
if (module == null || module.moduleKey() == null || module.moduleKey().isBlank()) {
continue;
}
RuntimeProcessingModule previous = byKey.putIfAbsent(module.moduleKey(), module);
if (previous != null) {
throw new IllegalStateException("Duplicate runtime processing module key: " + module.moduleKey());
}
}
}
this.modulesByKey = Map.copyOf(byKey);
}
public Collection<RuntimeProcessingModule> modules() {
return modulesByKey.values();
}
public List<RuntimeProcessingModuleDescriptorDto> descriptors() {
return modulesByKey.values().stream()
.map(RuntimeProcessingModule::descriptor)
.toList();
}
public RuntimeProcessingModule require(String moduleKey) {
RuntimeProcessingModule module = modulesByKey.get(moduleKey);
if (module == null) {
throw new IllegalArgumentException("Unknown runtime processing module: " + moduleKey);
}
return module;
}
}

View File

@ -0,0 +1,19 @@
package at.procon.eventhub.processing.eventprocessing.module;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record RuntimeProcessingModuleResult(
String moduleKey,
RuntimeProcessingModuleStatus status,
Object output,
Map<String, Object> metadata,
List<String> warnings
) {
public RuntimeProcessingModuleResult {
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,7 @@
package at.procon.eventhub.processing.eventprocessing.module;
public enum RuntimeProcessingModuleStatus {
SUCCESS,
SKIPPED,
FAILED
}

View File

@ -0,0 +1,46 @@
package at.procon.eventhub.processing.eventprocessing.module;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class RuntimeProcessingPipelineExecutor {
private final RuntimeProcessingModuleRegistry moduleRegistry;
public RuntimeProcessingPipelineExecutor(RuntimeProcessingModuleRegistry moduleRegistry) {
this.moduleRegistry = moduleRegistry;
}
public Map<String, RuntimeProcessingModuleResult> execute(
RuntimeProcessingExecutionApiRequest request,
List<String> moduleKeys,
RuntimeProcessingModuleContext initialContext
) {
LinkedHashMap<String, RuntimeProcessingModuleResult> results = new LinkedHashMap<>();
RuntimeProcessingModuleContext context = initialContext == null
? new RuntimeProcessingModuleContext(request, List.of(), Map.of(), Map.of())
: initialContext;
for (String moduleKey : moduleKeys == null ? List.<String>of() : moduleKeys) {
if (moduleKey == null || moduleKey.isBlank()) {
continue;
}
RuntimeProcessingModule module = moduleRegistry.require(moduleKey.trim());
RuntimeProcessingModuleResult result = module.execute(context);
results.put(module.moduleKey(), result);
context = new RuntimeProcessingModuleContext(
request,
context.events(),
context.attributes(),
results
);
if (result.status() == RuntimeProcessingModuleStatus.FAILED) {
break;
}
}
return Map.copyOf(results);
}
}

View File

@ -0,0 +1,18 @@
package at.procon.eventhub.processing.eventprocessing.module;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class SupportEvidenceNormalizationModule extends AbstractDriverWorkingTimePhaseModule {
public SupportEvidenceNormalizationModule() {
super(
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
"Support evidence normalization",
"Normalizes mixed-source support evidence for driver working-time processing.",
"JAVA",
Set.of("RuntimeSupportEvidenceNormalizationDebugDto")
);
}
}

View File

@ -0,0 +1,18 @@
package at.procon.eventhub.processing.eventprocessing.module;
import java.util.Set;
import org.springframework.stereotype.Component;
@Component
public class VehicleEvidenceAttachmentModule extends AbstractDriverWorkingTimePhaseModule {
public VehicleEvidenceAttachmentModule() {
super(
DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
"Vehicle evidence attachment",
"Attaches vehicle-only evidence to driver partitions by overlapping driver vehicle-usage intervals.",
"JAVA",
Set.of("RuntimeDriverPartitionDebugDto")
);
}
}

View File

@ -0,0 +1,386 @@
package at.procon.eventhub.processing.eventprocessing.module.epl;
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.processing.eventprocessing.module.DriverWorkingTimeModuleKeys;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleContext;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
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.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public final class DriverWorkingTimeEplEventMapper {
public static final String DRIVER_ACTIVITY_POINT_EVENT = "DriverActivityPointEvent";
public static final String DRIVER_ACTIVITY_INTERVAL_EVENT = "DriverActivityIntervalEvent";
public static final String DRIVER_VEHICLE_USAGE_POINT_EVENT = "DriverVehicleUsagePointEvent";
public static final String DRIVER_VEHICLE_USAGE_INTERVAL_EVENT = "DriverVehicleUsageIntervalEvent";
private DriverWorkingTimeEplEventMapper() {
}
public static List<EventHubEventDto> sourceEvents(RuntimeProcessingModuleContext context) {
RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) {
return safeList(bundle.mergedEvents());
}
return safeList(context.events());
}
public static List<Map<String, Object>> activityPointEvents(List<EventHubEventDto> sourceEvents) {
return safeList(sourceEvents).stream()
.map(DriverWorkingTimeEplEventMapper::toActivityPointEvent)
.filter(Objects::nonNull)
.sorted(pointEventComparator())
.toList();
}
public static List<Map<String, Object>> vehicleUsagePointEvents(List<EventHubEventDto> sourceEvents) {
return safeList(sourceEvents).stream()
.map(DriverWorkingTimeEplEventMapper::toVehicleUsagePointEvent)
.filter(Objects::nonNull)
.sorted(pointEventComparator())
.toList();
}
public static Map<String, Object> activityPointEventDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("eventId", String.class);
definition.put("intervalId", String.class);
definition.put("sourceRowId", String.class);
definition.put("sourceRowIds", java.util.List.class);
definition.put("activityType", String.class);
definition.put("lifecycle", String.class);
definition.put("occurredAt", OffsetDateTime.class);
definition.put("occurredAtEpochSecond", long.class);
definition.put("cardSlot", String.class);
definition.put("cardStatus", String.class);
definition.put("drivingStatus", String.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("synthetic", boolean.class);
definition.put("clippedToRequestedPeriod", boolean.class);
definition.put("level", String.class);
return definition;
}
public static Map<String, Object> activityIntervalEventDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("intervalId", String.class);
definition.put("activityType", String.class);
definition.put("cardSlot", String.class);
definition.put("cardStatus", String.class);
definition.put("drivingStatus", String.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("firstSourceIntervalId", String.class);
definition.put("lastSourceIntervalId", String.class);
definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", long.class);
definition.put("durationSeconds", long.class);
definition.put("sourceIntervalIds", java.util.List.class);
definition.put("synthetic", boolean.class);
definition.put("clippedToRequestedPeriod", boolean.class);
definition.put("level", String.class);
return definition;
}
public static Map<String, Object> vehicleUsagePointEventDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("eventId", String.class);
definition.put("intervalId", String.class);
definition.put("sourceRowId", String.class);
definition.put("sourceRowIds", java.util.List.class);
definition.put("lifecycle", String.class);
definition.put("occurredAt", OffsetDateTime.class);
definition.put("occurredAtEpochSecond", long.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("odometerKm", Long.class);
return definition;
}
public static Map<String, Object> vehicleUsageIntervalEventDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("intervalId", String.class);
definition.put("firstSourceIntervalId", String.class);
definition.put("lastSourceIntervalId", String.class);
definition.put("startedAt", OffsetDateTime.class);
definition.put("endedAt", OffsetDateTime.class);
definition.put("startedAtEpochSecond", long.class);
definition.put("endedAtEpochSecond", Long.class);
definition.put("durationSeconds", long.class);
definition.put("odometerBeginKm", Long.class);
definition.put("odometerEndKm", Long.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("sourceIntervalIds", java.util.List.class);
return definition;
}
private static Map<String, Object> toActivityPointEvent(EventHubEventDto sourceEvent) {
if (sourceEvent == null
|| sourceEvent.eventDomain() != EventDomain.DRIVER_ACTIVITY
|| (sourceEvent.lifecycle() != EventLifecycle.START && sourceEvent.lifecycle() != EventLifecycle.END)
|| sourceEvent.occurredAt() == null) {
return null;
}
JsonNode raw = rawPayload(sourceEvent);
JsonNode attributes = attributes(sourceEvent);
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
String driverKey = firstNonBlank(text(raw, "driverKey"), driverKey(sourceEvent));
if (driverKey == null || intervalId == null) {
return null;
}
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId(raw, sourceEvent));
event.put("driverKey", driverKey);
event.put("eventId", sourceEvent.externalSourceEventId());
event.put("intervalId", intervalId);
event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
event.put("activityType", firstNonBlank(text(raw, "activityType"), eventTypeAsActivity(sourceEvent.eventType())));
event.put("lifecycle", sourceEvent.lifecycle().name());
event.put("occurredAt", sourceEvent.occurredAt());
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
event.put("cardSlot", firstNonBlank(text(raw, "cardSlot"), text(attributes, "cardSlot")));
event.put("cardStatus", firstNonBlank(text(raw, "cardStatus"), text(attributes, "cardStatus")));
event.put("drivingStatus", firstNonBlank(text(raw, "drivingStatus"), text(attributes, "drivingStatus")));
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
event.put("synthetic", booleanValue(raw, "synthetic", false));
event.put("clippedToRequestedPeriod", booleanValue(raw, "clippedToRequestedPeriod", false));
event.put("level", firstNonBlank(text(raw, "level"), "RAW_EVENT"));
return event;
}
private static Map<String, Object> toVehicleUsagePointEvent(EventHubEventDto sourceEvent) {
if (sourceEvent == null
|| sourceEvent.eventDomain() != EventDomain.DRIVER_CARD
|| (sourceEvent.lifecycle() != EventLifecycle.INSERT && sourceEvent.lifecycle() != EventLifecycle.WITHDRAW)
|| sourceEvent.occurredAt() == null) {
return null;
}
boolean supportedType = sourceEvent.eventType() == EventType.CARD_INSERTED
|| sourceEvent.eventType() == EventType.CARD_WITHDRAWN;
if (!supportedType) {
return null;
}
JsonNode raw = rawPayload(sourceEvent);
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
String driverKey = firstNonBlank(text(raw, "driverKey"), driverKey(sourceEvent));
if (driverKey == null || intervalId == null) {
return null;
}
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId(raw, sourceEvent));
event.put("driverKey", driverKey);
event.put("eventId", sourceEvent.externalSourceEventId());
event.put("intervalId", intervalId);
event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
event.put("lifecycle", sourceEvent.lifecycle().name());
event.put("occurredAt", sourceEvent.occurredAt());
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
event.put("odometerKm", odometerKm(sourceEvent, raw));
return event;
}
private static Comparator<Map<String, Object>> pointEventComparator() {
return Comparator
.comparing((Map<String, Object> event) -> (Long) event.get("occurredAtEpochSecond"))
.thenComparing(event -> lifecycleOrder(Objects.toString(event.get("lifecycle"), "")))
.thenComparing(event -> Objects.toString(event.get("driverKey"), ""))
.thenComparing(event -> Objects.toString(event.get("intervalId"), ""))
.thenComparing(event -> Objects.toString(event.get("eventId"), ""));
}
private static int lifecycleOrder(String lifecycle) {
return switch (lifecycle) {
case "INSERT", "START" -> 0;
case "WITHDRAW", "END" -> 1;
default -> 2;
};
}
private static 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 static JsonNode attributes(EventHubEventDto event) {
return event.eventDetails() == null ? null : event.eventDetails().attributes();
}
private static 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;
}
private static boolean booleanValue(JsonNode node, String field, boolean fallback) {
if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
return fallback;
}
return node.get(field).asBoolean(fallback);
}
private static Long longValue(JsonNode node, String field) {
if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
return null;
}
JsonNode value = node.get(field);
if (value.isNumber()) {
return value.asLong();
}
try {
return Long.parseLong(value.asText());
} catch (NumberFormatException ignored) {
return null;
}
}
private static List<String> stringList(JsonNode node, String field, String fallback) {
JsonNode value = node == null || field == null ? null : node.get(field);
if (value == null || value.isNull()) {
return fallback == null ? List.of() : List.of(fallback);
}
if (value.isArray()) {
List<String> result = new ArrayList<>();
value.forEach(item -> {
if (item != null && !item.isNull()) {
String text = item.asText(null);
if (text != null && !text.isBlank()) {
result.add(text);
}
}
});
return result.isEmpty() && fallback != null ? List.of(fallback) : List.copyOf(result);
}
String text = value.asText(null);
return text == null || text.isBlank() ? (fallback == null ? List.of() : List.of(fallback)) : List.of(text);
}
private static String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value.trim();
}
}
return null;
}
private static String eventTypeAsActivity(EventType eventType) {
if (eventType == null) {
return "UNKNOWN";
}
return switch (eventType) {
case DRIVE -> "DRIVE";
case WORK -> "WORK";
case AVAILABILITY -> "AVAILABILITY";
case BREAK_REST -> "BREAK_REST";
default -> eventType.name();
};
}
private static UUID sessionId(JsonNode raw, EventHubEventDto event) {
String rawSessionId = firstNonBlank(
text(raw, "sessionId"),
event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId()
);
if (rawSessionId != null) {
try {
return UUID.fromString(rawSessionId);
} catch (IllegalArgumentException ignored) {
// DB-acquired source packages need not be UUID-based file sessions.
}
}
return new UUID(0L, 0L);
}
private static String driverKey(EventHubEventDto event) {
if (event.driverRef() == null) {
return null;
}
if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) {
return event.driverRef().driverCard().stableKey();
}
return event.driverRef().sourceEntityId();
}
private static String registrationKey(EventHubEventDto event) {
if (event.vehicleRef() == null || event.vehicleRef().vehicleRegistration() == null) {
return null;
}
return event.vehicleRef().vehicleRegistration().stableKey();
}
private static String vehicleKey(EventHubEventDto event) {
if (event.vehicleRef() == null) {
return null;
}
return firstNonBlank(event.vehicleRef().vin(), event.vehicleRef().sourceVehicleEntityId());
}
private static String sourceKind(EventHubEventDto event) {
return event.packageInfo() == null || event.packageInfo().eventSource() == null
? null
: event.packageInfo().eventSource().sourceKind();
}
private static Long odometerKm(EventHubEventDto event, JsonNode raw) {
Long explicit = longValue(raw, event.lifecycle() == EventLifecycle.WITHDRAW ? "odometerEndKm" : "odometerBeginKm");
if (explicit != null) {
return explicit;
}
explicit = longValue(raw, "odometerKm");
if (explicit != null) {
return explicit;
}
return event.odometerM() == null ? null : event.odometerM() / 1_000L;
}
private static <T> List<T> safeList(List<T> values) {
return values == null ? List.of() : values;
}
}

View File

@ -0,0 +1,17 @@
package at.procon.eventhub.processing.eventprocessing.module.epl;
import java.util.List;
import java.util.Map;
public record RuntimeEplInputEventStream(
String eventTypeName,
List<Map<String, Object>> events
) {
public RuntimeEplInputEventStream {
if (eventTypeName == null || eventTypeName.isBlank()) {
throw new IllegalArgumentException("eventTypeName must not be blank");
}
eventTypeName = eventTypeName.trim();
events = events == null ? List.of() : List.copyOf(events);
}
}

View File

@ -0,0 +1,10 @@
package at.procon.eventhub.processing.eventprocessing.module.epl;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModule;
public interface RuntimeEplModule extends RuntimeProcessingModule {
default String engine() {
return "EPL";
}
}

View File

@ -0,0 +1,31 @@
package at.procon.eventhub.processing.eventprocessing.module.epl;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record RuntimeEplModuleDefinition(
String moduleKey,
Map<String, Map<String, Object>> eventTypes,
List<String> eplResources,
List<String> outputStatementNames,
List<RuntimeEplInputEventStream> inputStreams
) {
public RuntimeEplModuleDefinition {
if (moduleKey == null || moduleKey.isBlank()) {
throw new IllegalArgumentException("moduleKey must not be blank");
}
moduleKey = moduleKey.trim();
eventTypes = eventTypes == null ? Map.of() : immutableNestedMap(eventTypes);
eplResources = eplResources == null ? List.of() : List.copyOf(eplResources);
outputStatementNames = outputStatementNames == null ? List.of() : List.copyOf(outputStatementNames);
inputStreams = inputStreams == null ? List.of() : List.copyOf(inputStreams);
}
private static Map<String, Map<String, Object>> immutableNestedMap(Map<String, Map<String, Object>> source) {
LinkedHashMap<String, Map<String, Object>> copy = new LinkedHashMap<>();
source.forEach((key, value) -> copy.put(key, value == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(value))));
return Collections.unmodifiableMap(copy);
}
}

View File

@ -0,0 +1,26 @@
package at.procon.eventhub.processing.eventprocessing.module.epl;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record RuntimeEplModuleExecutionResult(
Map<String, List<Map<String, Object>>> outputsByStatement,
Map<String, Object> metadata
) {
public RuntimeEplModuleExecutionResult {
outputsByStatement = outputsByStatement == null ? Map.of() : immutableOutputMap(outputsByStatement);
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
}
public List<Map<String, Object>> output(String statementName) {
return outputsByStatement.getOrDefault(statementName, List.of());
}
private static Map<String, List<Map<String, Object>>> immutableOutputMap(Map<String, List<Map<String, Object>>> source) {
LinkedHashMap<String, List<Map<String, Object>>> copy = new LinkedHashMap<>();
source.forEach((key, value) -> copy.put(key, value == null ? List.of() : List.copyOf(value)));
return Collections.unmodifiableMap(copy);
}
}

View File

@ -0,0 +1,120 @@
package at.procon.eventhub.processing.eventprocessing.module.epl;
import com.espertech.esper.common.client.EPCompiled;
import com.espertech.esper.common.client.EventBean;
import com.espertech.esper.common.client.configuration.Configuration;
import com.espertech.esper.compiler.client.CompilerArguments;
import com.espertech.esper.compiler.client.EPCompileException;
import com.espertech.esper.compiler.client.EPCompilerProvider;
import com.espertech.esper.runtime.client.EPDeployException;
import com.espertech.esper.runtime.client.EPDeployment;
import com.espertech.esper.runtime.client.EPRuntime;
import com.espertech.esper.runtime.client.EPRuntimeProvider;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
@Service
public class RuntimeEplModuleExecutor {
private static final AtomicLong RUNTIME_COUNTER = new AtomicLong();
public RuntimeEplModuleExecutionResult execute(RuntimeEplModuleDefinition definition) {
EPRuntime runtime = null;
try {
Configuration configuration = new Configuration();
definition.eventTypes().forEach((eventTypeName, eventTypeDefinition) ->
configuration.getCommon().addEventType(eventTypeName, eventTypeDefinition));
String runtimeUri = "eventhub-runtime-epl-module-"
+ definition.moduleKey().replaceAll("[^A-Za-z0-9_-]", "-")
+ "-" + RUNTIME_COUNTER.incrementAndGet();
runtime = EPRuntimeProvider.getRuntime(runtimeUri, configuration);
EPCompiled compiled = EPCompilerProvider.getCompiler().compile(renderEpl(definition.eplResources()), new CompilerArguments(configuration));
EPDeployment deployment = runtime.getDeploymentService().deploy(compiled);
Map<String, List<Map<String, Object>>> outputs = new LinkedHashMap<>();
for (String statementName : definition.outputStatementNames()) {
outputs.put(statementName, new ArrayList<>());
var statement = runtime.getDeploymentService().getStatement(deployment.getDeploymentId(), statementName);
if (statement == null) {
throw new IllegalStateException("EPL module " + definition.moduleKey()
+ " did not deploy expected statement '" + statementName + "'.");
}
statement.addListener((newData, oldData, stmt, rt) -> collect(newData, outputs.get(statementName)));
}
for (RuntimeEplInputEventStream inputStream : definition.inputStreams()) {
for (Map<String, Object> event : inputStream.events()) {
runtime.getEventService().sendEventMap(event, inputStream.eventTypeName());
}
}
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("engine", "EPL");
metadata.put("eplResources", definition.eplResources());
Map<String, Integer> inputCounts = new LinkedHashMap<>();
for (RuntimeEplInputEventStream inputStream : definition.inputStreams()) {
inputCounts.merge(inputStream.eventTypeName(), inputStream.events().size(), Integer::sum);
}
metadata.put("inputCounts", inputCounts);
Map<String, Integer> outputCounts = new LinkedHashMap<>();
outputs.forEach((statement, events) -> outputCounts.put(statement, events.size()));
metadata.put("outputCounts", outputCounts);
return new RuntimeEplModuleExecutionResult(outputs, metadata);
} catch (EPCompileException | EPDeployException e) {
throw new IllegalStateException("Cannot compile/deploy runtime EPL module " + definition.moduleKey(), e);
} finally {
if (runtime != null) {
runtime.destroy();
}
}
}
private String renderEpl(List<String> resources) {
StringBuilder builder = new StringBuilder();
for (String resource : resources) {
if (resource == null || resource.isBlank()) {
continue;
}
if (!builder.isEmpty()) {
builder.append("\n\n");
}
builder.append(loadResource(resource.trim()));
}
return builder.toString();
}
private static String loadResource(String path) {
try {
ClassPathResource resource = new ClassPathResource(path);
return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalStateException("Cannot load EPL resource: " + path, e);
}
}
private void collect(EventBean[] newData, List<Map<String, Object>> target) {
if (newData == null || target == null) {
return;
}
for (EventBean eventBean : newData) {
LinkedHashMap<String, Object> values = new LinkedHashMap<>();
for (String propertyName : eventBean.getEventType().getPropertyNames()) {
if (propertyName == null) {
continue;
}
values.put(propertyName, eventBean.get(propertyName));
}
target.add(values);
}
}
}

View File

@ -0,0 +1,10 @@
package at.procon.eventhub.processing.eventprocessing.partition;
public enum RuntimeEventPartitioningStrategy {
NONE,
DRIVER,
VEHICLE,
DRIVER_VEHICLE,
SOURCE_FAMILY,
CUSTOM_PROFILE
}

View File

@ -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();
}
}

View File

@ -0,0 +1,9 @@
package at.procon.eventhub.processing.eventprocessing.partition;
public enum RuntimeEventScopeType {
DRIVER_SCOPED,
VEHICLE_SCOPED,
DRIVER_VEHICLE_SCOPED,
GLOBAL_SUPPORT,
UNKNOWN
}

View File

@ -0,0 +1,480 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeModuleKeys;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleContext;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleStatus;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingPipelineExecutor;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleRegistry;
import at.procon.eventhub.processing.eventprocessing.module.DriverActivityIntervalsModule;
import at.procon.eventhub.processing.eventprocessing.module.DriverVehicleUsageIntervalsModule;
import at.procon.eventhub.processing.eventprocessing.module.DriverVehicleUsageMergeModule;
import at.procon.eventhub.processing.eventprocessing.module.VehicleEvidenceAttachmentModule;
import at.procon.eventhub.processing.eventprocessing.module.SupportEvidenceNormalizationModule;
import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeDerivedProjectionsModule;
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessingPlan {
public static final String PLAN_KEY = "driver-working-time-v1";
public static final String LEGACY_PROFILE_ALIAS = "tachograph-driver-esper-v1";
private final RuntimeProcessingPipelineExecutor pipelineExecutor;
private final boolean includeRuntimeEventAssemblyModule;
@Autowired
public DriverWorkingTimeRuntimeProcessingPlan(RuntimeProcessingPipelineExecutor pipelineExecutor) {
this.pipelineExecutor = pipelineExecutor;
this.includeRuntimeEventAssemblyModule = true;
}
/**
* Compatibility constructor for older unit tests and legacy adapters that instantiated the
* driver-working-time plan directly from the old scope service. This constructor builds a
* small local module registry around the existing scope-processing adapter. It intentionally
* omits the runtime-event-assembly module because the scope-processing adapter performs that
* assembly internally.
*/
public DriverWorkingTimeRuntimeProcessingPlan(
RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService
) {
this(new RuntimeProcessingPipelineExecutor(new RuntimeProcessingModuleRegistry(List.of(
new DriverActivityIntervalsModule(),
new DriverVehicleUsageIntervalsModule(),
new DriverVehicleUsageMergeModule(),
new VehicleEvidenceAttachmentModule(),
new SupportEvidenceNormalizationModule(),
new DriverWorkingTimeDerivedProjectionsModule(scopeProcessingService)
))), false);
}
private DriverWorkingTimeRuntimeProcessingPlan(
RuntimeProcessingPipelineExecutor pipelineExecutor,
boolean includeRuntimeEventAssemblyModule
) {
this.pipelineExecutor = pipelineExecutor;
this.includeRuntimeEventAssemblyModule = includeRuntimeEventAssemblyModule;
}
@Override
public String processingPlanKey() {
return PLAN_KEY;
}
@Override
public Set<String> aliases() {
return Set.of(LEGACY_PROFILE_ALIAS);
}
@Override
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
return RuntimeEventPartitioningStrategy.DRIVER;
}
@Override
public String displayName() {
return "Driver working-time processing";
}
@Override
public String description() {
return "Loads canonical EventHub runtime events from selected sources, partitions them by driver, "
+ "attaches vehicle-only evidence by vehicle/time, normalizes mixed-source support evidence, "
+ "then executes source-neutral driver working-time processing modules for driving intervals, "
+ "interruptions, rest candidates, overnight candidates, trips, vehicle usage, and card-absence coverage.";
}
@Override
public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
return List.of(RuntimeEventPartitioningStrategy.DRIVER);
}
@Override
public List<RuntimeProcessingModuleDescriptorDto> modules() {
List<RuntimeProcessingModuleDescriptorDto> descriptors = new java.util.ArrayList<>();
if (includeRuntimeEventAssemblyModule) {
descriptors.add(new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY,
"Runtime event assembly",
"Loads and merges canonical runtime events from the selected source scope before plan-specific processing.",
"JAVA",
Set.of("UnifiedRuntimeEventBundle")
));
}
descriptors.addAll(List.of(
new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.EVENT_TO_ACTIVITY_INTERVALS,
"Event to activity intervals",
"Pairs canonical DRIVER_ACTIVITY START/END point events into source-neutral activity intervals using a first-class EPL module.",
"ESPER",
Set.of("DriverActivityIntervalEvent")
),
new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS,
"Event to vehicle usage intervals",
"Pairs canonical driver/card INSERT/WITHDRAW point events into source-neutral vehicle-usage intervals using a first-class EPL module.",
"ESPER",
Set.of("DriverVehicleUsageIntervalEvent")
),
new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE,
"Vehicle usage merge",
"Merges adjacent same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations. Currently delegated to the derived projections adapter.",
"JAVA/ESPER",
Set.of("DriverVehicleUsageIntervalEvent")
),
new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
"Vehicle evidence attachment",
"Attaches vehicle-only events to driver partitions when they overlap driver vehicle-usage intervals.",
"JAVA",
Set.of("RuntimeDriverPartitionDebugDto")
),
new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
"Support evidence normalization",
"Normalizes attached mixed-source support evidence for driver working-time processing.",
"JAVA",
Set.of("RuntimeSupportEvidenceNormalizationDebugDto")
),
new RuntimeProcessingModuleDescriptorDto(
DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
"Driving-derived projections",
"Runs the shared driver working-time derived projection module for driving interruptions, rest candidates, trips, and overnight candidates.",
"ESPER+JAVA",
Set.of("DriverWorkingTimeProcessingResultDto")
)
));
return List.copyOf(descriptors);
}
@Override
public Set<String> optionalParameters() {
return Set.of(
"significantDrivingMinutes",
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes",
"includePartitionDebug"
);
}
@Override
public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) {
boolean includePartitionDebug = booleanParameter(
request.parameters(),
"includePartitionDebug",
request.partitioning() != null && request.partitioning().includeDebugOrDefault()
);
UnifiedRuntimeProcessingApiRequest scopeRequest = applyExecutionRequest(
request.sourceSelection(),
request.partitioning(),
request.parameters()
);
Map<String, Object> attributes = new LinkedHashMap<>();
attributes.put("runtimeScopeApiRequest", scopeRequest);
attributes.put("includePartitionDebug", includePartitionDebug);
RuntimeProcessingModuleContext initialContext = new RuntimeProcessingModuleContext(
request,
List.of(),
attributes,
Map.of()
);
List<String> executedModules = requestedOrDefaultModules(request.modules());
Map<String, RuntimeProcessingModuleResult> moduleResults = pipelineExecutor.execute(
request,
executedModules,
initialContext
);
UnifiedRuntimeDriverWorkingTimeScopeResultDto workingTimeResult = extractWorkingTimeResult(moduleResults);
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>();
workingTimeResult.driverResults().forEach((driverKey, driverResult) -> {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "DriverWorkingTimeProcessingResultDto");
metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
metadata.put("mergedEventCount", driverResult.mergedEventCount());
if (driverResult.supportEvidenceNormalization() != null) {
metadata.put("supportEvidenceNormalization", driverResult.supportEvidenceNormalization());
}
if (driverResult.partitionDebug() != null) {
metadata.put("partitionDebug", driverResult.partitionDebug());
}
partitionResults.put(
driverKey,
new RuntimeEventProcessingPartitionResultDto(
"DRIVER",
driverKey,
"UnifiedRuntimeDerivedProjectionResultDto",
driverResult,
metadata,
partitionModuleResults(driverResult)
)
);
});
return new RuntimeProcessingExecutionResultDto(
processingPlanKey(),
executedModules,
RuntimeEventPartitioningStrategy.DRIVER,
workingTimeResult.request(),
workingTimeResult.inputEventCount(),
workingTimeResult.selectedDriverCount(),
workingTimeResult.discoveredVehicleCount(),
workingTimeResult.discoveredVehicles(),
sanitizeExecutionModuleResults(moduleResults),
partitionResults,
workingTimeResult.notes(),
workingTimeResult.warnings()
);
}
private UnifiedRuntimeDriverWorkingTimeScopeResultDto extractWorkingTimeResult(
Map<String, RuntimeProcessingModuleResult> moduleResults
) {
RuntimeProcessingModuleResult finalResult = moduleResults.get(DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
if (finalResult == null) {
throw new IllegalArgumentException("Processing plan " + PLAN_KEY
+ " requires module " + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS + ".");
}
if (finalResult.status() == RuntimeProcessingModuleStatus.FAILED) {
throw new IllegalStateException("Processing module " + finalResult.moduleKey() + " failed: " + finalResult.warnings());
}
Object output = finalResult.output();
if (output instanceof UnifiedRuntimeDriverWorkingTimeScopeResultDto result) {
return result;
}
throw new IllegalStateException("Processing module " + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS
+ " did not return UnifiedRuntimeDriverWorkingTimeScopeResultDto. Actual: "
+ (output == null ? "null" : output.getClass().getName()));
}
private Map<String, RuntimeProcessingModuleResult> sanitizeExecutionModuleResults(
Map<String, RuntimeProcessingModuleResult> moduleResults
) {
LinkedHashMap<String, RuntimeProcessingModuleResult> sanitized = new LinkedHashMap<>();
for (Map.Entry<String, RuntimeProcessingModuleResult> entry : moduleResults.entrySet()) {
RuntimeProcessingModuleResult result = entry.getValue();
sanitized.put(entry.getKey(), new RuntimeProcessingModuleResult(
result.moduleKey(),
result.status(),
null,
result.metadata(),
result.warnings()
));
}
return sanitized;
}
private Map<String, RuntimeProcessingModuleResult> partitionModuleResults(
UnifiedRuntimeDerivedProjectionResultDto driverResult
) {
LinkedHashMap<String, RuntimeProcessingModuleResult> results = new LinkedHashMap<>();
Map<String, Object> attachmentMetadata = new LinkedHashMap<>();
attachmentMetadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
attachmentMetadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
attachmentMetadata.put("mergedEventCount", driverResult.mergedEventCount());
results.put(
DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
new RuntimeProcessingModuleResult(
DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT,
RuntimeProcessingModuleStatus.SUCCESS,
driverResult.partitionDebug(),
attachmentMetadata,
List.of()
)
);
Map<String, Object> normalizationMetadata = new LinkedHashMap<>();
if (driverResult.supportEvidenceNormalization() != null) {
normalizationMetadata.put("inputEventCount", driverResult.supportEvidenceNormalization().inputEventCount());
normalizationMetadata.put("normalizedSupportEvidenceEventCount", driverResult.supportEvidenceNormalization().normalizedSupportEvidenceEventCount());
normalizationMetadata.put("unchangedEventCount", driverResult.supportEvidenceNormalization().unchangedEventCount());
}
results.put(
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
new RuntimeProcessingModuleResult(
DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION,
RuntimeProcessingModuleStatus.SUCCESS,
driverResult.supportEvidenceNormalization(),
normalizationMetadata,
List.of()
)
);
results.put(
DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
new RuntimeProcessingModuleResult(
DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
RuntimeProcessingModuleStatus.SUCCESS,
driverResult.projection(),
Map.of(
"resultType", driverResult.projection() == null ? "NONE" : "DriverWorkingTimeProcessingResultDto",
"noteCount", driverResult.notes().size()
),
List.of()
)
);
return results;
}
public UnifiedRuntimeProcessingApiRequest applyExecutionRequest(
UnifiedRuntimeProcessingApiRequest sourceSelection,
RuntimeEventPartitioningApiRequest partitioning,
Map<String, Object> parameters
) {
if (sourceSelection == null) {
throw new IllegalArgumentException("sourceSelection must not be null for processing plan " + PLAN_KEY + ".");
}
boolean includeAllDrivers = sourceSelection.includeAllDrivers() != null && sourceSelection.includeAllDrivers();
java.util.Set<String> driverKeys = sourceSelection.driverKeys();
boolean includeAllVehicles = sourceSelection.includeAllVehicles() != null && sourceSelection.includeAllVehicles();
java.util.Set<String> vehicleKeys = sourceSelection.vehicleKeys();
if (partitioning != null) {
RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null
? RuntimeEventPartitioningStrategy.DRIVER
: partitioning.strategy();
if (strategy != RuntimeEventPartitioningStrategy.DRIVER
&& strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) {
throw new IllegalArgumentException("Processing plan " + PLAN_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", sourceSelection.significantDrivingMinutes());
Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", sourceSelection.minimumRestPeriodMinutes());
boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents",
partitioning == null ? sourceSelection.expandVehicleEvents() == null || sourceSelection.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault());
Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes",
partitioning == null
? sourceSelection.vehicleExpansionPaddingMinutes()
: partitioning.vehicleEvidencePaddingMinutesOrDefault(sourceSelection.vehicleExpansionPaddingMinutes() == null ? 0 : sourceSelection.vehicleExpansionPaddingMinutes()));
return new UnifiedRuntimeProcessingApiRequest(
sourceSelection.sessionId(),
sourceSelection.sessionIds(),
sourceSelection.compositeSessionId(),
sourceSelection.tenantKey(),
sourceSelection.sourceFamilies(),
sourceSelection.eventBackend(),
sourceSelection.driverKey(),
driverKeys,
includeAllDrivers,
vehicleKeys,
includeAllVehicles,
sourceSelection.driverSourceEntityId(),
sourceSelection.driverCardNation(),
sourceSelection.driverCardNumber(),
sourceSelection.occurredFrom(),
sourceSelection.occurredTo(),
attachVehicleOnlyEvents,
vehicleEvidencePaddingMinutes,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
private List<String> requestedOrDefaultModules(List<String> requestedModules) {
if (requestedModules != null && !requestedModules.isEmpty()) {
LinkedHashMap<String, String> requested = new LinkedHashMap<>();
for (String module : requestedModules) {
if (module != null && !module.isBlank()) {
requested.put(module.trim(), module.trim());
}
}
requested.putIfAbsent(DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS,
DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS);
return List.copyOf(requested.values());
}
return modules().stream()
.map(RuntimeProcessingModuleDescriptorDto::moduleKey)
.toList();
}
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);
}
}
}

View File

@ -0,0 +1,59 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import com.fasterxml.jackson.annotation.JsonAlias;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record RuntimeProcessingExecutionApiRequest(
String processingPlanKey,
@JsonAlias("scope") UnifiedRuntimeProcessingApiRequest sourceSelection,
RuntimeEventPartitioningApiRequest partitioning,
List<String> modules,
Map<String, Object> parameters
) {
public RuntimeProcessingExecutionApiRequest {
if (processingPlanKey == null || processingPlanKey.isBlank()) {
throw new IllegalArgumentException("processingPlanKey must not be blank");
}
processingPlanKey = processingPlanKey.trim();
if (sourceSelection == null) {
throw new IllegalArgumentException("sourceSelection must not be null");
}
partitioning = partitioning == null
? new RuntimeEventPartitioningApiRequest(null, null, null, null, null, null, null, null, null, null)
: partitioning;
modules = modules == null ? List.of() : List.copyOf(modules);
parameters = parameters == null
? Map.of()
: Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
}
public UnifiedRuntimeProcessingApiRequest scope() {
return sourceSelection;
}
public static RuntimeProcessingExecutionApiRequest driverWorkingTime(UnifiedRuntimeProcessingApiRequest sourceSelection) {
return new RuntimeProcessingExecutionApiRequest(
DriverWorkingTimeRuntimeProcessingPlan.PLAN_KEY,
sourceSelection,
new RuntimeEventPartitioningApiRequest(
at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy.DRIVER,
null,
sourceSelection != null ? sourceSelection.includeAllDrivers() : null,
sourceSelection != null ? sourceSelection.driverKeys() : null,
sourceSelection != null ? sourceSelection.includeAllDrivers() : null,
sourceSelection != null ? sourceSelection.vehicleKeys() : null,
sourceSelection != null ? sourceSelection.includeAllVehicles() : null,
null,
sourceSelection != null ? sourceSelection.vehicleExpansionPaddingMinutes() : null,
null
),
List.of(),
Map.of()
);
}
}

View File

@ -0,0 +1,53 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
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 RuntimeProcessingExecutionResultDto(
String processingPlanKey,
List<String> executedModules,
RuntimeEventPartitioningStrategy partitioningStrategy,
UnifiedRuntimeProcessingRequest request,
int inputEventCount,
int selectedPartitionCount,
int discoveredVehicleCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
Map<String, RuntimeProcessingModuleResult> moduleResults,
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults,
List<String> notes,
List<String> warnings
) {
public RuntimeProcessingExecutionResultDto {
executedModules = executedModules == null ? List.of() : List.copyOf(executedModules);
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
moduleResults = moduleResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(moduleResults));
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);
}
public RuntimeProcessingExecutionResultDto(
String processingPlanKey,
List<String> executedModules,
RuntimeEventPartitioningStrategy partitioningStrategy,
UnifiedRuntimeProcessingRequest request,
int inputEventCount,
int selectedPartitionCount,
int discoveredVehicleCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults,
List<String> notes,
List<String> warnings
) {
this(processingPlanKey, executedModules, partitioningStrategy, request, inputEventCount,
selectedPartitionCount, discoveredVehicleCount, discoveredVehicles, Map.of(), partitionResults,
notes, warnings);
}
}

View File

@ -0,0 +1,24 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class RuntimeProcessingExecutionService {
private final RuntimeProcessingPlanRegistry planRegistry;
public RuntimeProcessingExecutionService(RuntimeProcessingPlanRegistry planRegistry) {
this.planRegistry = planRegistry;
}
public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) {
RuntimeProcessingPlan plan = planRegistry.require(request.processingPlanKey());
plan.validatePartitioning(request.partitioning());
return plan.execute(request);
}
public List<RuntimeProcessingPlanDescriptorDto> listPlans() {
return planRegistry.planDescriptors();
}
}

View File

@ -0,0 +1,22 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import java.util.Set;
public record RuntimeProcessingModuleDescriptorDto(
String moduleKey,
String displayName,
String description,
String engine,
Set<String> producedStreams
) {
public RuntimeProcessingModuleDescriptorDto {
if (moduleKey == null || moduleKey.isBlank()) {
throw new IllegalArgumentException("moduleKey must not be blank");
}
moduleKey = moduleKey.trim();
displayName = displayName == null || displayName.isBlank() ? moduleKey : displayName.trim();
description = description == null ? "" : description;
engine = engine == null || engine.isBlank() ? "UNKNOWN" : engine.trim();
producedStreams = producedStreams == null ? Set.of() : Set.copyOf(producedStreams);
}
}

View File

@ -0,0 +1,55 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import java.util.List;
import java.util.Set;
public interface RuntimeProcessingPlan {
String processingPlanKey();
default Set<String> aliases() {
return Set.of();
}
RuntimeEventPartitioningStrategy defaultPartitioningStrategy();
default String displayName() {
return processingPlanKey();
}
default String description() {
return "";
}
default List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
RuntimeEventPartitioningStrategy defaultStrategy = defaultPartitioningStrategy();
return defaultStrategy == null ? List.of() : List.of(defaultStrategy);
}
default List<RuntimeProcessingModuleDescriptorDto> modules() {
return List.of();
}
default Set<String> requiredParameters() {
return Set.of();
}
default Set<String> optionalParameters() {
return Set.of();
}
RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request);
default void validatePartitioning(RuntimeEventPartitioningApiRequest partitioning) {
RuntimeEventPartitioningStrategy requested = partitioning == null || partitioning.strategy() == null
? defaultPartitioningStrategy()
: partitioning.strategy();
if (requested != null && !supportedPartitioningStrategies().contains(requested)) {
throw new IllegalArgumentException("Processing plan " + processingPlanKey()
+ " does not support partitioning strategy " + requested
+ ". Supported: " + supportedPartitioningStrategies());
}
}
}

View File

@ -0,0 +1,44 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import java.util.List;
import java.util.Set;
public record RuntimeProcessingPlanDescriptorDto(
String processingPlanKey,
Set<String> aliases,
String displayName,
String description,
RuntimeEventPartitioningStrategy defaultPartitioningStrategy,
List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies,
List<RuntimeProcessingModuleDescriptorDto> modules,
Set<String> requiredParameters,
Set<String> optionalParameters
) {
public RuntimeProcessingPlanDescriptorDto {
supportedPartitioningStrategies = supportedPartitioningStrategies == null
? List.of()
: List.copyOf(supportedPartitioningStrategies);
modules = modules == null ? List.of() : List.copyOf(modules);
aliases = aliases == null ? Set.of() : Set.copyOf(aliases);
requiredParameters = requiredParameters == null ? Set.of() : Set.copyOf(requiredParameters);
optionalParameters = optionalParameters == null ? Set.of() : Set.copyOf(optionalParameters);
}
public static RuntimeProcessingPlanDescriptorDto from(RuntimeProcessingPlan plan) {
if (plan == null) {
throw new IllegalArgumentException("plan must not be null");
}
return new RuntimeProcessingPlanDescriptorDto(
plan.processingPlanKey(),
plan.aliases(),
plan.displayName(),
plan.description(),
plan.defaultPartitioningStrategy(),
plan.supportedPartitioningStrategies(),
plan.modules(),
plan.requiredParameters(),
plan.optionalParameters()
);
}
}

View File

@ -0,0 +1,72 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
public class RuntimeProcessingPlanRegistry {
private final Map<String, RuntimeProcessingPlan> plansByKey;
private final Map<String, RuntimeProcessingPlan> plansByKeyAndAlias;
public RuntimeProcessingPlanRegistry(List<RuntimeProcessingPlan> plans) {
LinkedHashMap<String, RuntimeProcessingPlan> byKey = new LinkedHashMap<>();
LinkedHashMap<String, RuntimeProcessingPlan> byKeyAndAlias = new LinkedHashMap<>();
for (RuntimeProcessingPlan plan : plans == null ? List.<RuntimeProcessingPlan>of() : plans) {
String key = normalize(plan.processingPlanKey());
if (key == null) {
throw new IllegalStateException("Runtime processing plan returned a blank processingPlanKey: " + plan.getClass().getName());
}
RuntimeProcessingPlan previous = byKey.putIfAbsent(key, plan);
if (previous != null) {
throw new IllegalStateException("Duplicate runtime processingPlanKey: " + key);
}
putAlias(byKeyAndAlias, key, plan);
for (String alias : plan.aliases()) {
String normalizedAlias = normalize(alias);
if (normalizedAlias != null) {
putAlias(byKeyAndAlias, normalizedAlias, plan);
}
}
}
this.plansByKey = Collections.unmodifiableMap(byKey);
this.plansByKeyAndAlias = Collections.unmodifiableMap(byKeyAndAlias);
}
public RuntimeProcessingPlan require(String processingPlanKey) {
String normalized = normalize(processingPlanKey);
RuntimeProcessingPlan plan = normalized == null ? null : plansByKeyAndAlias.get(normalized);
if (plan == null) {
throw new IllegalArgumentException("Unknown runtime processingPlanKey: " + processingPlanKey
+ ". Available plans: " + plansByKey.keySet());
}
return plan;
}
public Map<String, RuntimeProcessingPlan> plansByKey() {
return plansByKey;
}
public List<RuntimeProcessingPlanDescriptorDto> planDescriptors() {
return plansByKey.values().stream()
.map(RuntimeProcessingPlanDescriptorDto::from)
.toList();
}
private void putAlias(Map<String, RuntimeProcessingPlan> target, String key, RuntimeProcessingPlan plan) {
RuntimeProcessingPlan previous = target.putIfAbsent(key, plan);
if (previous != null && previous != plan) {
throw new IllegalStateException("Duplicate runtime processing plan alias/key: " + key);
}
}
private String normalize(String key) {
if (key == null || key.isBlank()) {
return null;
}
return key.trim();
}
}

View File

@ -0,0 +1,16 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
/**
* @deprecated Compatibility adapter. Use {@link DriverWorkingTimeRuntimeProcessingPlan}.
*/
@Deprecated(forRemoval = false)
public class TachographDriverWorkingTimeRuntimeProcessingPlan extends DriverWorkingTimeRuntimeProcessingPlan {
public TachographDriverWorkingTimeRuntimeProcessingPlan(
RuntimeDriverWorkingTimeScopeProcessingService driverWorkingTimeScopeProcessingService
) {
super(driverWorkingTimeScopeProcessingService);
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -0,0 +1,83 @@
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 at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
import at.procon.eventhub.processing.eventprocessing.plan.DriverWorkingTimeRuntimeProcessingPlan;
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile {
/**
* Legacy compatibility key. New clients should use processingPlanKey=driver-working-time-v1
* via /api/eventhub/runtime-processing/executions.
*/
public static final String PROFILE_KEY = DriverWorkingTimeRuntimeProcessingPlan.LEGACY_PROFILE_ALIAS;
private final DriverWorkingTimeRuntimeProcessingPlan plan;
@Autowired
public TachographDriverEsperRuntimeEventProcessingProfile(DriverWorkingTimeRuntimeProcessingPlan plan) {
this.plan = plan;
}
public TachographDriverEsperRuntimeEventProcessingProfile(RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService) {
this(new DriverWorkingTimeRuntimeProcessingPlan(scopeProcessingService));
}
@Override
public String profileKey() {
return PROFILE_KEY;
}
@Override
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
return plan.defaultPartitioningStrategy();
}
@Override
public String displayName() {
return "Tachograph Driver Esper Processing";
}
@Override
public String description() {
return "Compatibility adapter for legacy profileKey=" + PROFILE_KEY
+ ". New clients should use processingPlanKey=" + plan.processingPlanKey() + ". "
+ plan.description();
}
@Override
public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
return plan.supportedPartitioningStrategies();
}
@Override
public Set<String> optionalParameters() {
return plan.optionalParameters();
}
@Override
public Set<String> requiredParameters() {
return plan.requiredParameters();
}
@Override
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
RuntimeProcessingExecutionResultDto executionResult = plan.execute(new RuntimeProcessingExecutionApiRequest(
plan.processingPlanKey(),
request.scope(),
request.partitioning(),
List.of(),
request.parameters()
));
return RuntimeEventProcessingResultDto.fromExecution(executionResult, profileKey());
}
}

View File

@ -0,0 +1,40 @@
package at.procon.eventhub.processing.eventprocessing.support;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.util.Map;
/**
* Source-neutral support-evidence view used before adapting mixed provider
* events into a concrete processing profile. The current tachograph Esper
* profile consumes this as geo support evidence when latitude/longitude are
* available, while the original provider semantics remain in rawAttributes.
*/
public record RuntimeSupportEvidenceEvent(
String eventId,
String sourceFamily,
String sourceKind,
String eventDomain,
String eventType,
String lifecycle,
String driverKey,
String vehicleKey,
String registrationKey,
OffsetDateTime occurredAt,
Long occurredAtEpochSecond,
BigDecimal latitude,
BigDecimal longitude,
String countryCode,
String regionCode,
String countryFrom,
String countryTo,
String operation,
Long odometerKm,
BigDecimal speedKmh,
BigDecimal maxSpeedKmh,
Map<String, Object> rawAttributes
) {
public RuntimeSupportEvidenceEvent {
rawAttributes = rawAttributes == null ? Map.of() : Map.copyOf(rawAttributes);
}
}

View File

@ -0,0 +1,19 @@
package at.procon.eventhub.processing.eventprocessing.support;
import at.procon.eventhub.dto.EventHubEventDto;
import java.util.List;
public record RuntimeSupportEvidenceNormalizationResult(
List<EventHubEventDto> normalizedEvents,
int inputEventCount,
int normalizedSupportEvidenceEventCount,
int unchangedEventCount,
List<String> notes
) {
public RuntimeSupportEvidenceNormalizationResult {
normalizedEvents = normalizedEvents == null ? List.of() : List.copyOf(normalizedEvents);
normalizedSupportEvidenceEventCount = Math.max(0, normalizedSupportEvidenceEventCount);
unchangedEventCount = Math.max(0, unchangedEventCount);
notes = notes == null ? List.of() : List.copyOf(notes);
}
}

View File

@ -0,0 +1,436 @@
package at.procon.eventhub.processing.eventprocessing.support;
import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventDetailsDto;
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.GeoPointDto;
import at.procon.eventhub.dto.VehicleRefDto;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class RuntimeSupportEvidenceNormalizer {
private static final LinkedHashSet<EventDomain> DIRECT_TACHOGRAPH_SUPPORT_DOMAINS = new LinkedHashSet<>(List.of(
EventDomain.POSITION,
EventDomain.PLACE,
EventDomain.BORDER_CROSSING,
EventDomain.LOAD_UNLOAD
));
private final ObjectMapper objectMapper;
public RuntimeSupportEvidenceNormalizer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public RuntimeSupportEvidenceNormalizationResult normalizeForTachographDriver(
String driverKey,
List<EventHubEventDto> events
) {
List<EventHubEventDto> safeEvents = events == null ? List.of() : List.copyOf(events);
List<EventHubEventDto> normalizedEvents = new ArrayList<>(safeEvents.size());
int normalizedSupportEvidence = 0;
int unchanged = 0;
for (EventHubEventDto event : safeEvents) {
EventHubEventDto normalized = normalizeOneForTachographDriver(driverKey, event);
normalizedEvents.add(normalized);
if (normalized != event) {
normalizedSupportEvidence++;
} else {
unchanged++;
}
}
List<String> notes = new ArrayList<>();
notes.add("Runtime support evidence normalization inspected " + safeEvents.size() + " event(s).");
notes.add("Runtime support evidence normalization adapted " + normalizedSupportEvidence
+ " support/vehicle event(s) for the tachograph Esper profile.");
return new RuntimeSupportEvidenceNormalizationResult(
normalizedEvents,
safeEvents.size(),
normalizedSupportEvidence,
unchanged,
notes
);
}
public RuntimeSupportEvidenceEvent toSupportEvidenceEvent(String fallbackDriverKey, EventHubEventDto event) {
if (event == null || isDriverActivityOrCardUsage(event)) {
return null;
}
JsonNode raw = rawPayload(event);
GeoPointDto position = event.position();
BigDecimal latitude = position == null ? decimal(raw, "latitude") : position.latitude();
BigDecimal longitude = position == null ? decimal(raw, "longitude") : position.longitude();
Long odometerKm = firstNonNull(longValue(raw, "odometerKm"), toKilometers(event.odometerM()));
return new RuntimeSupportEvidenceEvent(
firstNonBlank(text(raw, "supportEventId"), text(raw, "sourceRowId"), event.externalSourceEventId()),
sourceFamily(event),
firstNonBlank(text(raw, "sourceKind"), sourceKind(event)),
event.eventDomain() == null ? null : event.eventDomain().name(),
event.eventType() == null ? null : event.eventType().name(),
event.lifecycle() == null ? null : event.lifecycle().name(),
firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(event)),
firstNonBlank(text(raw, "vehicleKey"), vehicleKey(event)),
firstNonBlank(text(raw, "registrationKey"), registrationKey(event)),
event.occurredAt(),
event.occurredAt() == null ? null : event.occurredAt().toEpochSecond(),
latitude,
longitude,
firstNonBlank(text(raw, "country"), detailText(event, "country")),
firstNonBlank(text(raw, "region"), detailText(event, "region")),
firstNonBlank(text(raw, "countryFrom"), detailText(event, "countryFrom")),
firstNonBlank(text(raw, "countryTo"), detailText(event, "countryTo")),
firstNonBlank(text(raw, "operation"), detailText(event, "operation")),
odometerKm,
decimal(raw, "avgSpeedKmh"),
decimal(raw, "maxSpeedKmh"),
rawAttributes(event, raw)
);
}
private EventHubEventDto normalizeOneForTachographDriver(String fallbackDriverKey, EventHubEventDto event) {
if (event == null || isDriverActivityOrCardUsage(event)) {
return event;
}
RuntimeSupportEvidenceEvent support = toSupportEvidenceEvent(fallbackDriverKey, event);
if (support == null || !hasGeoOrOdometerEvidence(support)) {
return event;
}
EventDomain normalizedDomain = normalizedDomain(event);
EventType normalizedType = normalizedType(normalizedDomain, event.eventType());
EventLifecycle normalizedLifecycle = normalizedLifecycle(normalizedDomain, event.lifecycle());
JsonNode payload = normalizedPayload(fallbackDriverKey, event, support, normalizedDomain);
EventDetailsDto details = normalizedDetails(event, support, normalizedDomain);
DriverRefDto driverRef = event.driverRef();
if ((driverRef == null || !driverRef.hasAnyReference()) && support.driverKey() != null) {
driverRef = new DriverRefDto(support.driverKey(), null);
}
return new EventHubEventDto(
event.eventId(),
event.externalSourceEventId(),
driverRef,
event.vehicleRef(),
event.occurredAt(),
event.receivedPartnerAt(),
event.receivedHubAt(),
normalizedDomain,
normalizedType,
normalizedLifecycle,
event.odometerM(),
normalizedPosition(event, support),
details,
event.sourcePackageRef(),
payload,
event.manualEntry(),
event.packageInfo()
);
}
private GeoPointDto normalizedPosition(EventHubEventDto original, RuntimeSupportEvidenceEvent support) {
if (original != null && original.position() != null) {
return original.position();
}
if (support == null || support.latitude() == null || support.longitude() == null) {
return null;
}
return new GeoPointDto(support.latitude(), support.longitude());
}
private boolean isDriverActivityOrCardUsage(EventHubEventDto event) {
if (event == null) {
return true;
}
if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
return true;
}
return event.eventDomain() == EventDomain.DRIVER_CARD
&& (event.eventType() == EventType.CARD_INSERTED || event.eventType() == EventType.CARD_WITHDRAWN);
}
private boolean hasGeoOrOdometerEvidence(RuntimeSupportEvidenceEvent support) {
return support != null
&& ((support.latitude() != null && support.longitude() != null) || support.odometerKm() != null);
}
private EventDomain normalizedDomain(EventHubEventDto event) {
if (event.eventDomain() != null && DIRECT_TACHOGRAPH_SUPPORT_DOMAINS.contains(event.eventDomain())) {
return event.eventDomain();
}
return EventDomain.POSITION;
}
private EventType normalizedType(EventDomain normalizedDomain, EventType originalType) {
if (normalizedDomain == EventDomain.POSITION) {
return EventType.POSITION_RECORDED;
}
if (normalizedDomain == EventDomain.PLACE) {
return EventType.WORKING_DAY_PLACE_RECORDED;
}
if (normalizedDomain == EventDomain.BORDER_CROSSING) {
return originalType == null ? EventType.BORDER_INBOUND : originalType;
}
if (normalizedDomain == EventDomain.LOAD_UNLOAD) {
return originalType == null ? EventType.LOAD_UNLOAD : originalType;
}
return originalType == null ? EventType.UNKNOWN_EVENT : originalType;
}
private EventLifecycle normalizedLifecycle(EventDomain normalizedDomain, EventLifecycle originalLifecycle) {
if (normalizedDomain == EventDomain.POSITION || normalizedDomain == EventDomain.PLACE) {
return EventLifecycle.SNAPSHOT;
}
return originalLifecycle == null ? EventLifecycle.SNAPSHOT : originalLifecycle;
}
private EventDetailsDto normalizedDetails(
EventHubEventDto original,
RuntimeSupportEvidenceEvent support,
EventDomain normalizedDomain
) {
Map<String, Object> attributes = new LinkedHashMap<>();
put(attributes, "normalizedSupportEvidence", true);
put(attributes, "normalizedForProfile", "tachograph-driver-esper-v1");
put(attributes, "originalEventDomain", support.eventDomain());
put(attributes, "originalEventType", support.eventType());
put(attributes, "originalLifecycle", support.lifecycle());
put(attributes, "sourceFamily", support.sourceFamily());
put(attributes, "sourceKind", support.sourceKind());
put(attributes, "country", support.countryCode());
put(attributes, "region", support.regionCode());
put(attributes, "countryFrom", support.countryFrom());
put(attributes, "countryTo", support.countryTo());
put(attributes, "operation", support.operation());
if (original.eventDetails() != null && original.eventDetails().attributes() != null) {
attributes.put("originalAttributes", original.eventDetails().attributes());
}
String type = normalizedDomain == EventDomain.POSITION ? "POSITION" : normalizedDomain.name();
return new EventDetailsDto(type, objectMapper.valueToTree(attributes));
}
private JsonNode normalizedPayload(
String fallbackDriverKey,
EventHubEventDto original,
RuntimeSupportEvidenceEvent support,
EventDomain normalizedDomain
) {
ObjectNode root = objectMapper.createObjectNode();
ObjectNode raw = root.putObject("raw");
JsonNode originalRaw = rawPayload(original);
if (originalRaw != null && originalRaw.isObject()) {
originalRaw.fields().forEachRemaining(entry -> raw.set(entry.getKey(), entry.getValue()));
}
put(raw, "normalizedSupportEvidence", true);
put(raw, "normalizedForProfile", "tachograph-driver-esper-v1");
put(raw, "supportEventId", support.eventId());
put(raw, "supportEventDomain", support.eventDomain());
put(raw, "supportEventType", support.eventType());
put(raw, "supportEventLifecycle", support.lifecycle());
put(raw, "originalEventDomain", support.eventDomain());
put(raw, "originalEventType", support.eventType());
put(raw, "originalLifecycle", support.lifecycle());
put(raw, "normalizedEventDomain", normalizedDomain.name());
put(raw, "driverKey", firstNonBlank(support.driverKey(), fallbackDriverKey));
put(raw, "vehicleKey", support.vehicleKey());
put(raw, "registrationKey", support.registrationKey());
put(raw, "sourceFamily", support.sourceFamily());
put(raw, "sourceKind", support.sourceKind());
put(raw, "country", support.countryCode());
put(raw, "region", support.regionCode());
put(raw, "countryFrom", support.countryFrom());
put(raw, "countryTo", support.countryTo());
put(raw, "operation", support.operation());
put(raw, "odometerKm", support.odometerKm());
put(raw, "avgSpeedKmh", support.speedKmh());
put(raw, "maxSpeedKmh", support.maxSpeedKmh());
put(raw, "latitude", support.latitude());
put(raw, "longitude", support.longitude());
put(raw, "rawRecordPath", firstNonBlank(text(originalRaw, "rawRecordPath"), original.externalSourceEventId()));
return root;
}
private Map<String, Object> rawAttributes(EventHubEventDto event, JsonNode raw) {
Map<String, Object> result = new LinkedHashMap<>();
put(result, "externalSourceEventId", event == null ? null : event.externalSourceEventId());
put(result, "eventDomain", event == null || event.eventDomain() == null ? null : event.eventDomain().name());
put(result, "eventType", event == null || event.eventType() == null ? null : event.eventType().name());
put(result, "lifecycle", event == null || event.lifecycle() == null ? null : event.lifecycle().name());
if (raw != null) {
result.put("raw", raw);
}
return result;
}
private String sourceFamily(EventHubEventDto event) {
if (event == null || event.packageInfo() == null) {
return null;
}
return event.packageInfo().eventFamily();
}
private String sourceKind(EventHubEventDto event) {
if (event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null) {
return null;
}
return event.packageInfo().eventSource().sourceKind();
}
private JsonNode rawPayload(EventHubEventDto event) {
if (event == null || event.payload() == null || event.payload().isNull() || event.payload().isMissingNode()) {
return null;
}
JsonNode raw = event.payload().get("raw");
return raw == null || raw.isNull() ? event.payload() : raw;
}
private String driverKey(EventHubEventDto event) {
if (event == null || event.driverRef() == null || !event.driverRef().hasAnyReference()) {
return null;
}
return event.driverRef().stableKey();
}
private String vehicleKey(EventHubEventDto event) {
JsonNode raw = rawPayload(event);
String rawVehicleKey = text(raw, "vehicleKey");
if (rawVehicleKey != null) {
return rawVehicleKey;
}
VehicleRefDto vehicleRef = event == null ? null : event.vehicleRef();
if (vehicleRef == null) {
return null;
}
if (vehicleRef.vin() != null) {
return vehicleRef.vin();
}
if (vehicleRef.sourceVehicleEntityId() != null) {
return vehicleRef.sourceVehicleEntityId();
}
return null;
}
private String registrationKey(EventHubEventDto event) {
JsonNode raw = rawPayload(event);
String rawRegistrationKey = text(raw, "registrationKey");
if (rawRegistrationKey != null) {
return rawRegistrationKey;
}
VehicleRefDto vehicleRef = event == null ? null : event.vehicleRef();
if (vehicleRef == null) {
return null;
}
if (vehicleRef.vehicleRegistration() != null && vehicleRef.vehicleRegistration().hasValue()) {
return vehicleRef.vehicleRegistration().stableKey();
}
return vehicleRef.sourceRegistrationEntityId();
}
private String detailText(EventHubEventDto event, String field) {
if (event == null || event.eventDetails() == null || event.eventDetails().attributes() == null) {
return null;
}
JsonNode value = event.eventDetails().attributes().get(field);
return value == null || value.isNull() ? null : value.asText(null);
}
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();
}
private Long longValue(JsonNode node, String field) {
if (node == null || field == null) {
return null;
}
JsonNode value = node.get(field);
if (value == null || value.isNull()) {
return null;
}
try {
return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
} catch (NumberFormatException ex) {
return null;
}
}
private BigDecimal decimal(JsonNode node, String field) {
if (node == null || field == null) {
return null;
}
JsonNode value = node.get(field);
if (value == null || value.isNull()) {
return null;
}
if (value.isNumber()) {
return value.decimalValue();
}
String text = value.asText(null);
if (text == null || text.isBlank()) {
return null;
}
try {
return new BigDecimal(text.trim());
} catch (NumberFormatException ex) {
return null;
}
}
private Long toKilometers(Long meters) {
return meters == null ? null : meters / 1_000L;
}
@SafeVarargs
private final <T> T firstNonNull(T... values) {
if (values == null) {
return null;
}
for (T value : values) {
if (value != null) {
return value;
}
}
return null;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value.trim();
}
}
return null;
}
private void put(ObjectNode node, String field, Object value) {
if (node != null && field != null && value != null) {
node.set(field, objectMapper.valueToTree(value));
}
}
private void put(Map<String, Object> target, String field, Object value) {
if (target != null && field != null && value != null) {
target.put(field, value);
}
}
}

View File

@ -0,0 +1,23 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.util.List;
public record RuntimeMixedSourceEvidencePartitionValidationDto(
String partitionKey,
String status,
int directDriverEventCount,
int attachedVehicleEvidenceEventCount,
int ignoredVehicleEvidenceEventCount,
int mergedEventCount,
int normalizedSupportEvidenceEventCount,
int unchangedEventCount,
int supportGeoEventCount,
boolean partitionDebugPresent,
List<String> notes,
List<String> warnings
) {
public RuntimeMixedSourceEvidencePartitionValidationDto {
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,37 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import java.util.Set;
public record RuntimeMixedSourceEvidenceValidationApiRequest(
RuntimeEventProcessingApiRequest processingRequest,
Set<String> expectedPartitionKeys,
Integer minimumAttachedVehicleEvidenceEvents,
Integer minimumNormalizedSupportEvidenceEvents,
Boolean failWhenPartitionDebugMissing
) {
public RuntimeMixedSourceEvidenceValidationApiRequest {
if (processingRequest == null) {
throw new IllegalArgumentException("processingRequest must not be null.");
}
expectedPartitionKeys = expectedPartitionKeys == null ? Set.of() : Set.copyOf(expectedPartitionKeys);
if (minimumAttachedVehicleEvidenceEvents != null && minimumAttachedVehicleEvidenceEvents < 0) {
throw new IllegalArgumentException("minimumAttachedVehicleEvidenceEvents must not be negative.");
}
if (minimumNormalizedSupportEvidenceEvents != null && minimumNormalizedSupportEvidenceEvents < 0) {
throw new IllegalArgumentException("minimumNormalizedSupportEvidenceEvents must not be negative.");
}
}
public int minimumAttachedVehicleEvidenceEventsOrDefault() {
return minimumAttachedVehicleEvidenceEvents == null ? 0 : Math.max(0, minimumAttachedVehicleEvidenceEvents);
}
public int minimumNormalizedSupportEvidenceEventsOrDefault() {
return minimumNormalizedSupportEvidenceEvents == null ? 0 : Math.max(0, minimumNormalizedSupportEvidenceEvents);
}
public boolean failWhenPartitionDebugMissingOrDefault() {
return failWhenPartitionDebugMissing == null || failWhenPartitionDebugMissing;
}
}

View File

@ -0,0 +1,25 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public record RuntimeMixedSourceEvidenceValidationResultDto(
String profileKey,
String status,
int partitionCount,
int totalAttachedVehicleEvidenceEventCount,
int totalIgnoredVehicleEvidenceEventCount,
int totalNormalizedSupportEvidenceEventCount,
int totalSupportGeoEventCount,
Map<String, RuntimeMixedSourceEvidencePartitionValidationDto> partitions,
List<String> notes,
List<String> warnings
) {
public RuntimeMixedSourceEvidenceValidationResultDto {
partitions = partitions == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitions));
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,174 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
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 java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Service;
@Service
public class RuntimeMixedSourceEvidenceValidationService {
private final RuntimeEventProcessingService runtimeEventProcessingService;
public RuntimeMixedSourceEvidenceValidationService(RuntimeEventProcessingService runtimeEventProcessingService) {
this.runtimeEventProcessingService = runtimeEventProcessingService;
}
public RuntimeMixedSourceEvidenceValidationResultDto validate(RuntimeMixedSourceEvidenceValidationApiRequest request) {
RuntimeEventProcessingApiRequest debugRequest = withDebugEnabled(request.processingRequest());
RuntimeEventProcessingResultDto runtimeResult = runtimeEventProcessingService.process(debugRequest);
LinkedHashMap<String, RuntimeMixedSourceEvidencePartitionValidationDto> partitionResults = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>(runtimeResult.warnings());
List<String> notes = new ArrayList<>(runtimeResult.notes());
notes.add("Mixed-source evidence validation executed runtime event processing with partition debug enabled.");
notes.add("Validation checks vehicle-only evidence attachment and support-evidence normalization counts; it does not compare legal tachograph result semantics.");
int totalAttached = 0;
int totalIgnored = 0;
int totalNormalized = 0;
int totalSupportGeo = 0;
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : runtimeResult.partitionResults().entrySet()) {
RuntimeMixedSourceEvidencePartitionValidationDto partition = partitionValidation(
entry.getKey(),
entry.getValue(),
request.failWhenPartitionDebugMissingOrDefault()
);
partitionResults.put(entry.getKey(), partition);
totalAttached += partition.attachedVehicleEvidenceEventCount();
totalIgnored += partition.ignoredVehicleEvidenceEventCount();
totalNormalized += partition.normalizedSupportEvidenceEventCount();
totalSupportGeo += partition.supportGeoEventCount();
warnings.addAll(partition.warnings());
}
Set<String> missingPartitions = new LinkedHashSet<>(request.expectedPartitionKeys());
missingPartitions.removeAll(partitionResults.keySet());
for (String missingPartition : missingPartitions) {
warnings.add("Expected partition " + missingPartition + " was not returned by runtime event processing.");
}
if (totalAttached < request.minimumAttachedVehicleEvidenceEventsOrDefault()) {
warnings.add("Attached vehicle evidence count " + totalAttached + " is below expected minimum "
+ request.minimumAttachedVehicleEvidenceEventsOrDefault() + ".");
}
if (totalNormalized < request.minimumNormalizedSupportEvidenceEventsOrDefault()) {
warnings.add("Normalized support evidence count " + totalNormalized + " is below expected minimum "
+ request.minimumNormalizedSupportEvidenceEventsOrDefault() + ".");
}
boolean failed = !missingPartitions.isEmpty()
|| totalAttached < request.minimumAttachedVehicleEvidenceEventsOrDefault()
|| totalNormalized < request.minimumNormalizedSupportEvidenceEventsOrDefault()
|| partitionResults.values().stream().anyMatch(partition -> "FAILED".equals(partition.status()));
return new RuntimeMixedSourceEvidenceValidationResultDto(
runtimeResult.profileKey(),
failed ? "FAILED" : "PASSED",
partitionResults.size(),
totalAttached,
totalIgnored,
totalNormalized,
totalSupportGeo,
partitionResults,
notes,
warnings
);
}
private RuntimeMixedSourceEvidencePartitionValidationDto partitionValidation(
String partitionKey,
RuntimeEventProcessingPartitionResultDto partitionResult,
boolean failWhenPartitionDebugMissing
) {
UnifiedRuntimeDerivedProjectionResultDto result = partitionResult.result() instanceof UnifiedRuntimeDerivedProjectionResultDto derived
? derived
: null;
RuntimeDriverPartitionDebugDto debug = result == null ? null : result.partitionDebug();
RuntimeSupportEvidenceNormalizationDebugDto normalization = result == null ? null : result.supportEvidenceNormalization();
List<String> notes = new ArrayList<>();
List<String> warnings = new ArrayList<>();
if (result == null) {
warnings.add("Partition " + partitionKey + " did not return UnifiedRuntimeDerivedProjectionResultDto; resultType="
+ partitionResult.resultType() + ".");
}
if (debug == null) {
String message = "Partition " + partitionKey + " has no partition debug metadata.";
if (failWhenPartitionDebugMissing) {
warnings.add(message);
} else {
notes.add(message);
}
} else {
notes.addAll(debug.notes());
warnings.addAll(debug.warnings());
}
if (normalization == null) {
warnings.add("Partition " + partitionKey + " has no support-evidence normalization metadata.");
} else {
notes.addAll(normalization.notes());
}
int directDriverEventCount = debug == null ? (result == null ? 0 : result.driverSeedEventCount()) : debug.directDriverEventCount();
int attached = debug == null ? (result == null ? 0 : result.expandedVehicleEventCount()) : debug.attachedVehicleEvidenceEventCount();
int ignored = debug == null ? 0 : debug.ignoredVehicleEvidenceEventCount();
int merged = debug == null ? (result == null ? 0 : result.mergedEventCount()) : debug.mergedEventCount();
int normalized = normalization == null ? 0 : normalization.normalizedSupportEvidenceEventCount();
int unchanged = normalization == null ? 0 : normalization.unchangedEventCount();
int supportGeo = result == null || result.projection() == null ? 0 : result.projection().supportGeoEventCount();
String status = warnings.isEmpty() || (!failWhenPartitionDebugMissing && debug == null && warnings.size() == 1)
? "PASSED"
: "FAILED";
return new RuntimeMixedSourceEvidencePartitionValidationDto(
partitionKey,
status,
directDriverEventCount,
attached,
ignored,
merged,
normalized,
unchanged,
supportGeo,
debug != null,
notes,
warnings
);
}
private RuntimeEventProcessingApiRequest withDebugEnabled(RuntimeEventProcessingApiRequest request) {
RuntimeEventPartitioningApiRequest partitioning = request.partitioning();
RuntimeEventPartitioningApiRequest debugPartitioning = new RuntimeEventPartitioningApiRequest(
partitioning.strategy(),
partitioning.partitionKeys(),
partitioning.includeAllPartitions(),
partitioning.driverKeys(),
partitioning.includeAllDrivers(),
partitioning.vehicleKeys(),
partitioning.includeAllVehicles(),
partitioning.attachVehicleEvidence(),
partitioning.vehicleEvidencePaddingMinutes(),
true
);
Map<String, Object> parameters = new LinkedHashMap<>(request.parameters());
parameters.put("includePartitionDebug", true);
return new RuntimeEventProcessingApiRequest(
request.profileKey(),
request.scope(),
debugPartitioning,
parameters
);
}
}

View File

@ -0,0 +1,20 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.util.List;
public record RuntimeTachographDriverParityResultDto(
String driverKey,
String status,
String referenceMode,
int referenceSessionCount,
boolean runtimePartitionPresent,
List<RuntimeTachographParityCategoryComparisonDto> comparisons,
List<String> notes,
List<String> warnings
) {
public RuntimeTachographDriverParityResultDto {
comparisons = comparisons == null ? List.of() : List.copyOf(comparisons);
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,9 @@
package at.procon.eventhub.processing.eventprocessing.validation;
public record RuntimeTachographParityCategoryComparisonDto(
String category,
int fileSessionCount,
int runtimeCount,
boolean equal
) {
}

View File

@ -0,0 +1,61 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.time.OffsetDateTime;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public record RuntimeTachographParityValidationApiRequest(
UUID sessionId,
List<UUID> sessionIds,
UUID compositeSessionId,
String driverKey,
Set<String> driverKeys,
Boolean includeAllDrivers,
OffsetDateTime occurredFrom,
OffsetDateTime occurredTo,
Boolean expandVehicleEvents,
Integer vehicleExpansionPaddingMinutes,
Integer significantDrivingMinutes,
Integer minimumRestPeriodMinutes,
Boolean includeDebug
) {
public RuntimeTachographParityValidationApiRequest {
sessionIds = sessionIds == null ? List.of() : List.copyOf(sessionIds);
driverKeys = driverKeys == null ? Set.of() : Set.copyOf(driverKeys);
vehicleExpansionPaddingMinutes = vehicleExpansionPaddingMinutes == null
? null
: Math.max(0, vehicleExpansionPaddingMinutes);
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
minimumRestPeriodMinutes = minimumRestPeriodMinutes == null ? null : Math.max(1, minimumRestPeriodMinutes);
}
public Set<String> requestedDriverKeys() {
LinkedHashSet<String> result = new LinkedHashSet<>();
if (driverKey != null && !driverKey.isBlank()) {
result.add(driverKey.trim());
}
driverKeys.stream()
.filter(value -> value != null && !value.isBlank())
.map(String::trim)
.forEach(result::add);
return Set.copyOf(result);
}
public boolean includeAllDriversOrDefault() {
return includeAllDrivers == null ? requestedDriverKeys().isEmpty() : includeAllDrivers;
}
public boolean expandVehicleEventsOrDefault() {
return expandVehicleEvents == null || expandVehicleEvents;
}
public boolean includeDebugOrDefault() {
return includeDebug != null && includeDebug;
}
public int vehicleExpansionPaddingMinutesOrDefault() {
return vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes);
}
}

View File

@ -0,0 +1,23 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public record RuntimeTachographParityValidationResultDto(
String profileKey,
String status,
List<UUID> sessionIds,
int selectedDriverCount,
Map<String, RuntimeTachographDriverParityResultDto> driverResults,
List<String> notes,
List<String> warnings
) {
public RuntimeTachographParityValidationResultDto {
sessionIds = sessionIds == null ? List.of() : List.copyOf(sessionIds);
driverResults = driverResults == null ? Map.of() : Map.copyOf(new LinkedHashMap<>(driverResults));
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
}

View File

@ -0,0 +1,393 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
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.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.springframework.stereotype.Service;
@Service
public class RuntimeTachographParityValidationService {
private final TachographFileSessionRepository fileSessionRepository;
private final TachographCompositeSessionRepository compositeSessionRepository;
private final TachographFileSessionProcessingService fileSessionProcessingService;
private final RuntimeEventProcessingService runtimeEventProcessingService;
public RuntimeTachographParityValidationService(
TachographFileSessionRepository fileSessionRepository,
TachographCompositeSessionRepository compositeSessionRepository,
TachographFileSessionProcessingService fileSessionProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService
) {
this.fileSessionRepository = fileSessionRepository;
this.compositeSessionRepository = compositeSessionRepository;
this.fileSessionProcessingService = fileSessionProcessingService;
this.runtimeEventProcessingService = runtimeEventProcessingService;
}
public RuntimeTachographParityValidationResultDto validate(RuntimeTachographParityValidationApiRequest request) {
RuntimeTachographParityValidationApiRequest effectiveRequest = request == null
? new RuntimeTachographParityValidationApiRequest(null, List.of(), null, null, Set.of(), true,
null, null, true, 0, null, null, false)
: request;
List<UUID> sessionIds = resolveSessionIds(effectiveRequest);
Set<String> driverKeys = resolveDriverKeys(effectiveRequest, sessionIds);
if (driverKeys.isEmpty()) {
throw new IllegalArgumentException("No driver keys could be resolved for tachograph parity validation.");
}
RuntimeEventProcessingResultDto runtimeResult = runtimeEventProcessingService.process(genericRuntimeRequest(
effectiveRequest,
sessionIds,
driverKeys
));
LinkedHashMap<String, RuntimeTachographDriverParityResultDto> driverResults = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>(runtimeResult.warnings());
for (String driverKey : driverKeys) {
RuntimeTachographDriverParityResultDto driverResult = compareDriver(
effectiveRequest,
sessionIds,
driverKey,
runtimeResult
);
driverResults.put(driverKey, driverResult);
warnings.addAll(driverResult.warnings());
}
String status = driverResults.values().stream().allMatch(result -> "EQUAL".equals(result.status()))
? "EQUAL"
: "DIFFERENT";
List<String> notes = new ArrayList<>(runtimeResult.notes());
notes.add("Validation compares the legacy tachograph file-session esper-events path with the generic runtime event-processing profile '"
+ TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY + "'.");
if (sessionIds.size() > 1) {
notes.add("For multiple sessions, file-session reference counts are summed per driver. Runtime processing may intentionally merge or deduplicate intervals across session boundaries, so category differences need domain review.");
}
return new RuntimeTachographParityValidationResultDto(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
status,
sessionIds,
driverResults.size(),
driverResults,
notes,
warnings
);
}
private RuntimeTachographDriverParityResultDto compareDriver(
RuntimeTachographParityValidationApiRequest request,
List<UUID> sessionIds,
String driverKey,
RuntimeEventProcessingResultDto runtimeResult
) {
RuntimeProjectionCounts runtimeCounts = runtimeCounts(runtimeResult, driverKey);
ReferenceProjectionCounts referenceCounts = referenceCounts(request, sessionIds, driverKey);
List<RuntimeTachographParityCategoryComparisonDto> comparisons = comparisons(referenceCounts.counts(), runtimeCounts.counts());
boolean runtimePresent = runtimeCounts.present();
boolean referencePresent = referenceCounts.referenceSessionCount() > 0;
boolean allEqual = comparisons.stream().allMatch(RuntimeTachographParityCategoryComparisonDto::equal);
String status;
if (!referencePresent) {
status = "REFERENCE_MISSING";
} else if (!runtimePresent) {
status = "RUNTIME_MISSING";
} else if (allEqual) {
status = "EQUAL";
} else {
status = "DIFFERENT";
}
List<String> notes = new ArrayList<>();
notes.add("Reference mode: " + referenceCounts.referenceMode() + ".");
notes.add("Reference session count: " + referenceCounts.referenceSessionCount() + ".");
if (sessionIds.size() > 1) {
notes.add("Reference counts are summed from individual file-session endpoint results for this driver.");
}
List<String> warnings = new ArrayList<>();
if (!referencePresent) {
warnings.add("Driver " + driverKey + " was not found in any selected tachograph file session.");
}
if (!runtimePresent) {
warnings.add("Generic runtime event-processing profile did not return a partition for driver " + driverKey + ".");
}
if (runtimePresent && referencePresent && !allEqual) {
warnings.add("Runtime result differs from file-session reference result for driver " + driverKey + ".");
}
return new RuntimeTachographDriverParityResultDto(
driverKey,
status,
referenceCounts.referenceMode(),
referenceCounts.referenceSessionCount(),
runtimePresent,
comparisons,
notes,
warnings
);
}
private RuntimeEventProcessingApiRequest genericRuntimeRequest(
RuntimeTachographParityValidationApiRequest request,
List<UUID> sessionIds,
Set<String> driverKeys
) {
UnifiedRuntimeProcessingApiRequest scope = new UnifiedRuntimeProcessingApiRequest(
null,
sessionIds,
null,
null,
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
UnifiedRuntimeEventBackend.SOURCE_DB,
null,
driverKeys,
request.includeAllDriversOrDefault(),
null,
false,
null,
null,
null,
request.occurredFrom(),
request.occurredTo(),
request.expandVehicleEventsOrDefault(),
request.vehicleExpansionPaddingMinutesOrDefault(),
request.significantDrivingMinutes(),
request.minimumRestPeriodMinutes()
);
RuntimeEventPartitioningApiRequest partitioning = new RuntimeEventPartitioningApiRequest(
RuntimeEventPartitioningStrategy.DRIVER,
null,
request.includeAllDriversOrDefault(),
driverKeys,
request.includeAllDriversOrDefault(),
null,
false,
request.expandVehicleEventsOrDefault(),
request.vehicleExpansionPaddingMinutesOrDefault(),
request.includeDebugOrDefault()
);
Map<String, Object> parameters = new LinkedHashMap<>();
if (request.significantDrivingMinutes() != null) {
parameters.put("significantDrivingMinutes", request.significantDrivingMinutes());
}
if (request.minimumRestPeriodMinutes() != null) {
parameters.put("minimumRestPeriodMinutes", request.minimumRestPeriodMinutes());
}
parameters.put("attachVehicleOnlyEvents", request.expandVehicleEventsOrDefault());
parameters.put("vehicleEvidencePaddingMinutes", request.vehicleExpansionPaddingMinutesOrDefault());
parameters.put("includePartitionDebug", request.includeDebugOrDefault());
return new RuntimeEventProcessingApiRequest(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
scope,
partitioning,
parameters
);
}
private List<UUID> resolveSessionIds(RuntimeTachographParityValidationApiRequest request) {
LinkedHashSet<UUID> result = new LinkedHashSet<>();
if (request.sessionId() != null) {
result.add(request.sessionId());
}
result.addAll(request.sessionIds());
if (request.compositeSessionId() != null) {
TachographCompositeSession compositeSession = compositeSessionRepository.find(request.compositeSessionId())
.orElseThrow(() -> new TachographCompositeSessionNotFoundException(request.compositeSessionId()));
result.addAll(compositeSession.memberSessionIds());
}
if (result.isEmpty()) {
throw new IllegalArgumentException("sessionId, sessionIds, or compositeSessionId is required for tachograph parity validation.");
}
result.forEach(this::requireFileSession);
return List.copyOf(result);
}
private Set<String> resolveDriverKeys(RuntimeTachographParityValidationApiRequest request, List<UUID> sessionIds) {
LinkedHashSet<String> result = new LinkedHashSet<>(request.requestedDriverKeys());
if (request.includeAllDriversOrDefault() || result.isEmpty()) {
for (UUID sessionId : sessionIds) {
result.addAll(requireFileSession(sessionId).driversByKey().keySet());
}
}
return Set.copyOf(result);
}
private TachographFileSession requireFileSession(UUID sessionId) {
return fileSessionRepository.find(sessionId)
.orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
}
private ReferenceProjectionCounts referenceCounts(
RuntimeTachographParityValidationApiRequest request,
List<UUID> sessionIds,
String driverKey
) {
ProjectionCounts aggregate = ProjectionCounts.empty();
int referenceSessionCount = 0;
TachographEsperEventsProcessingRequest fileSessionRequest = new TachographEsperEventsProcessingRequest(
request.occurredFrom(),
request.occurredTo(),
request.significantDrivingMinutes(),
request.minimumRestPeriodMinutes()
);
for (UUID sessionId : sessionIds) {
TachographFileSession session = requireFileSession(sessionId);
if (!session.driversByKey().containsKey(driverKey)) {
continue;
}
TachographEsperDriverProcessingResultDto projection = fileSessionProcessingService.getEsperDriverProcessingResults(
sessionId,
driverKey,
fileSessionRequest
);
aggregate = aggregate.plus(ProjectionCounts.from(projection));
referenceSessionCount++;
}
String referenceMode = sessionIds.size() == 1 ? "SINGLE_FILE_SESSION" : "SUM_OF_FILE_SESSION_RESULTS";
return new ReferenceProjectionCounts(aggregate, referenceMode, referenceSessionCount);
}
private RuntimeProjectionCounts runtimeCounts(RuntimeEventProcessingResultDto runtimeResult, String driverKey) {
RuntimeEventProcessingPartitionResultDto partitionResult = runtimeResult.partitionResults().get(driverKey);
if (partitionResult == null) {
return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
}
Object result = partitionResult.result();
if (result instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) {
return new RuntimeProjectionCounts(ProjectionCounts.from(projectionResult.projection()), projectionResult.projection() != null);
}
return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
}
private List<RuntimeTachographParityCategoryComparisonDto> comparisons(
ProjectionCounts reference,
ProjectionCounts runtime
) {
return List.of(
comparison("activityIntervals", reference.activityIntervalCount(), runtime.activityIntervalCount()),
comparison("drivingIntervals", reference.drivingIntervalCount(), runtime.drivingIntervalCount()),
comparison("drivingInterruptionIntervals", reference.drivingInterruptionIntervalCount(), runtime.drivingInterruptionIntervalCount()),
comparison("drivingInterruptionVehicleChangeIntervals", reference.drivingInterruptionVehicleChangeIntervalCount(), runtime.drivingInterruptionVehicleChangeIntervalCount()),
comparison("dailyWeeklyRestCandidateIntervals", reference.dailyWeeklyRestCandidateIntervalCount(), runtime.dailyWeeklyRestCandidateIntervalCount()),
comparison("dailyWeeklyRestCandidateCoverageIntervals", reference.dailyWeeklyRestCandidateCoverageIntervalCount(), runtime.dailyWeeklyRestCandidateCoverageIntervalCount()),
comparison("unclassifiedDailyWeeklyRestCandidateCoverageIntervals", reference.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(), runtime.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()),
comparison("potentialHomeOvernightStayIntervals", reference.potentialHomeOvernightStayIntervalCount(), runtime.potentialHomeOvernightStayIntervalCount()),
comparison("potentialInVehicleOvernightStayIntervals", reference.potentialInVehicleOvernightStayIntervalCount(), runtime.potentialInVehicleOvernightStayIntervalCount()),
comparison("potentialInVehicleTripIntervals", reference.potentialInVehicleTripIntervalCount(), runtime.potentialInVehicleTripIntervalCount()),
comparison("vehicleUsageIntervals", reference.vehicleUsageIntervalCount(), runtime.vehicleUsageIntervalCount()),
comparison("vuCardAbsentIntervals", reference.vuCardAbsentIntervalCount(), runtime.vuCardAbsentIntervalCount()),
comparison("supportGeoEvents", reference.supportGeoEventCount(), runtime.supportGeoEventCount())
);
}
private RuntimeTachographParityCategoryComparisonDto comparison(String category, int fileSessionCount, int runtimeCount) {
return new RuntimeTachographParityCategoryComparisonDto(
category,
fileSessionCount,
runtimeCount,
fileSessionCount == runtimeCount
);
}
private record ReferenceProjectionCounts(
ProjectionCounts counts,
String referenceMode,
int referenceSessionCount
) {
}
private record RuntimeProjectionCounts(
ProjectionCounts counts,
boolean present
) {
}
private record ProjectionCounts(
int activityIntervalCount,
int drivingIntervalCount,
int drivingInterruptionIntervalCount,
int drivingInterruptionVehicleChangeIntervalCount,
int dailyWeeklyRestCandidateIntervalCount,
int dailyWeeklyRestCandidateCoverageIntervalCount,
int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
int potentialHomeOvernightStayIntervalCount,
int potentialInVehicleOvernightStayIntervalCount,
int potentialInVehicleTripIntervalCount,
int vehicleUsageIntervalCount,
int vuCardAbsentIntervalCount,
int supportGeoEventCount
) {
static ProjectionCounts empty() {
return new ProjectionCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) {
return projection == null ? empty() : from(projection.toDriverWorkingTime());
}
static ProjectionCounts from(DriverWorkingTimeProcessingResultDto projection) {
if (projection == null) {
return empty();
}
return new ProjectionCounts(
projection.activityIntervalCount(),
projection.drivingIntervalCount(),
projection.drivingInterruptionIntervalCount(),
projection.drivingInterruptionVehicleChangeIntervalCount(),
projection.dailyWeeklyRestCandidateIntervalCount(),
projection.dailyWeeklyRestCandidateCoverageIntervalCount(),
projection.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(),
projection.potentialHomeOvernightStayIntervalCount(),
projection.potentialInVehicleOvernightStayIntervalCount(),
projection.potentialInVehicleTripIntervalCount(),
projection.vehicleUsageIntervalCount(),
projection.vuCardAbsentIntervalCount(),
projection.supportGeoEventCount()
);
}
ProjectionCounts plus(ProjectionCounts other) {
return new ProjectionCounts(
activityIntervalCount + other.activityIntervalCount,
drivingIntervalCount + other.drivingIntervalCount,
drivingInterruptionIntervalCount + other.drivingInterruptionIntervalCount,
drivingInterruptionVehicleChangeIntervalCount + other.drivingInterruptionVehicleChangeIntervalCount,
dailyWeeklyRestCandidateIntervalCount + other.dailyWeeklyRestCandidateIntervalCount,
dailyWeeklyRestCandidateCoverageIntervalCount + other.dailyWeeklyRestCandidateCoverageIntervalCount,
unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount + other.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
potentialHomeOvernightStayIntervalCount + other.potentialHomeOvernightStayIntervalCount,
potentialInVehicleOvernightStayIntervalCount + other.potentialInVehicleOvernightStayIntervalCount,
potentialInVehicleTripIntervalCount + other.potentialInVehicleTripIntervalCount,
vehicleUsageIntervalCount + other.vehicleUsageIntervalCount,
vuCardAbsentIntervalCount + other.vuCardAbsentIntervalCount,
supportGeoEventCount + other.supportGeoEventCount
);
}
}
}

View File

@ -0,0 +1,47 @@
package at.procon.eventhub.processing.model;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
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<RuntimeVehicleUsageIntervalDebugDto> vehicleUsageIntervals,
List<RuntimeVehicleEvidenceAttachmentDecisionDto> vehicleEvidenceDecisions,
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);
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
vehicleEvidenceDecisions = vehicleEvidenceDecisions == null ? List.of() : List.copyOf(vehicleEvidenceDecisions);
notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}
public RuntimeDriverPartitionDebugDto toPartitionDebug() {
return new RuntimeDriverPartitionDebugDto(
driverKey,
directDriverEvents.size(),
vehicleUsageIntervalCount,
candidateVehicleEvidenceEventCount,
attachedVehicleEvidenceEvents.size(),
ignoredVehicleEvidenceEventCount,
mergedEvents.size(),
vehicleUsageIntervals,
vehicleEvidenceDecisions,
notes,
warnings
);
}
}

View File

@ -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,

View File

@ -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();
}

View File

@ -0,0 +1,399 @@
package at.procon.eventhub.processing.service;
import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.dto.VehicleRefDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
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
) {
return attachVehicleEvidence(
driverKey,
directDriverEvents,
runtimeScopeEvents,
attachVehicleOnlyEvents,
vehicleEvidencePaddingMinutes,
false
);
}
public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
String driverKey,
List<EventHubEventDto> directDriverEvents,
List<EventHubEventDto> runtimeScopeEvents,
boolean attachVehicleOnlyEvents,
int vehicleEvidencePaddingMinutes,
boolean includeDebug
) {
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 + ".");
List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions = includeDebug
? disabledDecisions(safeScopeEvents)
: List.of();
return new RuntimeDriverVehicleEvidenceAttachmentResult(
driverKey,
safeDriverEvents,
List.of(),
deduplicateAndSort(safeDriverEvents, List.of()),
0,
disabledDecisions.size(),
disabledDecisions.size(),
List.of(),
disabledDecisions,
notes,
warnings
);
}
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents);
List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals());
List<RuntimeVehicleUsageIntervalDebugDto> usageIntervalDebug = includeDebug
? usageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).toList()
: List.of();
List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
? new ArrayList<>(directDriverDecisions(safeDriverEvents))
: new ArrayList<>();
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++;
if (includeDebug) {
decisions.add(decision(
"IGNORED_NO_OVERLAPPING_VEHICLE_USAGE",
"Vehicle-scoped event did not overlap any reconstructed vehicle-usage interval for driver " + driverKey + ".",
vehicleEvent,
List.of()
));
}
continue;
}
attached.add(vehicleEvent);
if (includeDebug) {
decisions.add(decision(
"ATTACHED_VEHICLE_EVIDENCE",
"Vehicle-scoped event overlapped driver vehicle usage interval(s).",
vehicleEvent,
matchingIntervals
));
}
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,
usageIntervalDebug,
includeDebug ? decisions : List.of(),
notes,
warnings
);
}
private List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions(List<EventHubEventDto> runtimeScopeEvents) {
return (runtimeScopeEvents == null ? List.<EventHubEventDto>of() : runtimeScopeEvents).stream()
.filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
.map(event -> decision(
"IGNORED_ATTACHMENT_DISABLED",
"Vehicle evidence attachment was disabled for this partition.",
event,
List.of()
))
.toList();
}
private List<RuntimeVehicleEvidenceAttachmentDecisionDto> directDriverDecisions(List<EventHubEventDto> directDriverEvents) {
return (directDriverEvents == null ? List.<EventHubEventDto>of() : directDriverEvents).stream()
.map(event -> decision(
"DIRECT_DRIVER_EVENT",
"Event already carries the selected driver reference and belongs directly to the driver partition.",
event,
List.of()
))
.toList();
}
private RuntimeVehicleEvidenceAttachmentDecisionDto decision(
String decision,
String reason,
EventHubEventDto event,
List<ResolvedVehicleUsageInterval> matchingIntervals
) {
List<String> intervalIds = (matchingIntervals == null ? List.<ResolvedVehicleUsageInterval>of() : matchingIntervals).stream()
.map(ResolvedVehicleUsageInterval::intervalId)
.filter(Objects::nonNull)
.toList();
return new RuntimeVehicleEvidenceAttachmentDecisionDto(
decision,
reason,
dedupKey(event),
event == null ? null : event.externalSourceEventId(),
event == null ? null : event.occurredAt(),
event == null || event.eventDomain() == null ? null : event.eventDomain().name(),
event == null || event.eventType() == null ? null : event.eventType().name(),
event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
scopeClassifier.classify(event),
vehicleKeys(event),
intervalIds
);
}
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();
}
}

View File

@ -0,0 +1,319 @@
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.RuntimeDriverPartitionDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
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 RuntimeDriverWorkingTimeScopeProcessingService {
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
private final UnifiedRuntimeDerivedProjectionService derivedProjectionService;
private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;
public RuntimeDriverWorkingTimeScopeProcessingService(
UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDerivedProjectionService derivedProjectionService,
RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
) {
this.eventAssemblyService = eventAssemblyService;
this.derivedProjectionService = derivedProjectionService;
this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
}
public UnifiedRuntimeDriverWorkingTimeScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
return processScope(apiRequest, false);
}
public UnifiedRuntimeDriverWorkingTimeScopeResultDto processScope(
UnifiedRuntimeProcessingApiRequest apiRequest,
boolean includePartitionDebug
) {
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, RuntimeDriverPartitionDebugDto> partitionDebugByDriver = new LinkedHashMap<>();
Map<String, List<String>> attachedVehicleEvidenceByEvent = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>();
for (String driverKey : selectedDriverKeys) {
DriverPartition driverPartition = partitionForDriver(request, broadBundle, driverKey, includePartitionDebug);
UnifiedRuntimeEventBundle driverBundle = driverPartition.bundle();
if (driverPartition.debug() != null) {
partitionDebugByDriver.put(driverKey, driverPartition.debug());
}
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, driverPartition.debug() == null ? driverResult : driverResult.withPartitionDebug(driverPartition.debug()));
}
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 driver working-time processing used Java-side driver partitioning before calling the common event-input processing 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 UnifiedRuntimeDriverWorkingTimeScopeResultDto(
request,
broadBundle.mergedEvents().size(),
driverResults.size(),
broadBundle.discoveredVehicles().size(),
broadBundle.discoveredVehicles(),
driverResults,
partitionDebugByDriver,
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 DriverPartition partitionForDriver(
UnifiedRuntimeProcessingRequest request,
UnifiedRuntimeEventBundle broadBundle,
String driverKey,
boolean includePartitionDebug
) {
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(),
includePartitionDebug
);
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));
UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle(
request.withDriverKey(driverKey),
attachmentResult.directDriverEvents(),
driverVehicles,
attachmentResult.attachedVehicleEvidenceEvents(),
attachmentResult.mergedEvents(),
notes
);
return new DriverPartition(
bundle,
includePartitionDebug ? attachmentResult.toPartitionDebug() : null
);
}
private record DriverPartition(
UnifiedRuntimeEventBundle bundle,
RuntimeDriverPartitionDebugDto debug
) {
}
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();
}
}

View File

@ -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(
UnifiedDriverEventsRequest.forTachographFileSession(
request.sessionId(),
request.driverKey(),
request.occurredFrom(),
request.occurredTo()
)
);
List<EventHubEventDto> result = new ArrayList<>();
for (UUID sessionId : resolveSessionIds(request)) {
result.addAll(driverEventSourceService.loadDriverEvents(
UnifiedDriverEventsRequest.forTachographFileSession(
sessionId,
request.driverKey(),
request.occurredFrom(),
request.occurredTo()
)
));
}
return deduplicateBySignatureAndSort(result);
}
@Override
@ -47,16 +67,37 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
UnifiedRuntimeProcessingRequest request,
UnifiedDiscoveredVehicleRef vehicleRef
) {
return vehicleEventSourceService.loadVehicleEvents(
UnifiedVehicleEventsRequest.forTachographFileSession(
request.sessionId(),
vehicleRef.sourceVehicleEntityId(),
vehicleRef.vin(),
vehicleRef.registrationNation(),
vehicleRef.registrationNumber(),
request.vehicleOccurredFrom(),
request.vehicleOccurredTo()
)
);
List<EventHubEventDto> result = new ArrayList<>();
for (UUID sessionId : resolveSessionIds(request)) {
result.addAll(vehicleEventSourceService.loadVehicleEvents(
UnifiedVehicleEventsRequest.forTachographFileSession(
sessionId,
vehicleRef.sourceVehicleEntityId(),
vehicleRef.vin(),
vehicleRef.registrationNation(),
vehicleRef.registrationNumber(),
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()));
}
}

View File

@ -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());

View File

@ -0,0 +1,494 @@
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.RuntimeSupportEvidenceNormalizationDebugDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
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.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
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 DriverWorkingTimeProcessingCore workingTimeProcessingCore;
private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer;
private final EventHubProperties properties;
public UnifiedRuntimeDerivedProjectionService(
UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
UnifiedEventTimelineReconstructor timelineReconstructor,
DriverTimelineBuilder driverTimelineBuilder,
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
EventHubProperties properties,
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
) {
this(
runtimeEventAssemblyService,
timelineReconstructor,
driverTimelineBuilder,
reusableProjectionBuilder,
properties,
new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)),
supportEvidenceNormalizer
);
}
@Autowired
public UnifiedRuntimeDerivedProjectionService(
UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
UnifiedEventTimelineReconstructor timelineReconstructor,
DriverTimelineBuilder driverTimelineBuilder,
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
EventHubProperties properties,
DriverWorkingTimeProcessingCore workingTimeProcessingCore,
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
) {
this.runtimeEventAssemblyService = runtimeEventAssemblyService;
this.timelineReconstructor = timelineReconstructor;
this.driverTimelineBuilder = driverTimelineBuilder;
this.reusableProjectionBuilder = reusableProjectionBuilder;
this.properties = properties;
this.workingTimeProcessingCore = workingTimeProcessingCore;
this.supportEvidenceNormalizer = supportEvidenceNormalizer;
}
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;
RuntimeSupportEvidenceNormalizationResult normalizationResult = supportEvidenceNormalizer.normalizeForTachographDriver(
driverKey,
eventBundle.mergedEvents()
);
List<EventHubEventDto> normalizedEvents = normalizationResult.normalizedEvents();
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(
runtimeSessionId(request),
driverKey,
normalizedEvents
);
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.addAll(normalizationResult.notes());
notes.add("Runtime derived projections were evaluated from the unified merged event stream using normalized support evidence and the shared driver working-time 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.");
}
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(TachographEsperProcessingInput.fromEvents(
runtimeSessionId(request),
driverKey,
timeline,
normalizedEvents,
requestedFrom,
requestedTo,
significantDrivingMinutes,
minimumRestPeriodMinutes,
notes
));
notes = projection.notes();
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
normalizationResult.inputEventCount(),
normalizationResult.normalizedSupportEvidenceEventCount(),
normalizationResult.unchangedEventCount(),
normalizationResult.notes()
);
return new UnifiedRuntimeDerivedProjectionResultDto(
request,
eventBundle.driverSeedEvents().size(),
eventBundle.discoveredVehicles().size(),
eventBundle.expandedVehicleEvents().size(),
eventBundle.mergedEvents().size(),
eventBundle.discoveredVehicles(),
projection,
notes,
normalizationDebug
);
}
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);
}
}

View File

@ -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() + ".");
@ -133,10 +142,10 @@ public class UnifiedRuntimeEventAssemblyService {
appendDeduplicated(byKey, right);
return byKey.values().stream()
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(event -> event.eventDomain().name())
.thenComparing(event -> event.eventType().name())
.thenComparing(event -> event.lifecycle().name())
.thenComparing(EventHubEventDto::externalSourceEventId))
.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();
}

View File

@ -0,0 +1,48 @@
package at.procon.eventhub.processing.service;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
/**
* @deprecated Compatibility adapter. Use {@link RuntimeDriverWorkingTimeScopeProcessingService};
* tachograph files/databases are only one possible source for the common driver working-time runtime plan.
*/
@Deprecated(forRemoval = false)
public class UnifiedRuntimeTachographEsperScopeProcessingService {
private final RuntimeDriverWorkingTimeScopeProcessingService delegate;
public UnifiedRuntimeTachographEsperScopeProcessingService(
RuntimeDriverWorkingTimeScopeProcessingService delegate
) {
this.delegate = delegate;
}
public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
return toLegacy(delegate.processScope(apiRequest));
}
public UnifiedRuntimeTachographEsperScopeResultDto processScope(
UnifiedRuntimeProcessingApiRequest apiRequest,
boolean includePartitionDebug
) {
return toLegacy(delegate.processScope(apiRequest, includePartitionDebug));
}
private UnifiedRuntimeTachographEsperScopeResultDto toLegacy(
UnifiedRuntimeDriverWorkingTimeScopeResultDto result
) {
return new UnifiedRuntimeTachographEsperScopeResultDto(
result.request(),
result.inputEventCount(),
result.selectedDriverCount(),
result.discoveredVehicleCount(),
result.discoveredVehicles(),
result.driverResults(),
result.partitionDebugByDriver(),
result.notes(),
result.warnings()
);
}
}

View File

@ -34,7 +34,7 @@ public final class TachographNationRegistry {
continue;
}
String[] parts = line.split(";", -1);
if (parts.length < 5) {
if (parts.length < 4) {
continue;
}
Integer numericCode = parseInteger(parts[3]);
@ -43,7 +43,7 @@ public final class TachographNationRegistry {
}
String name = normalizeNullable(parts[1]);
String alphaCode = normalizeAlpha(parts[2]);
String defaultLanguageCode = normalizeNullable(parts[4]);
String defaultLanguageCode = parts.length > 4 ? normalizeNullable(parts[4]) : null;
NationRecord record = new NationRecord(numericCode, alphaCode, name, defaultLanguageCode, true);
byNumeric.put(numericCode, record);
if (alphaCode != null) {

View File

@ -22,7 +22,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@ -44,6 +43,8 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
CardSlot cardSlot = cardSlot(rs);
CardStatus cardStatus = cardStatus(rs);
DrivingStatus drivingStatus = drivingStatus(rs);
EventType eventType = eventType(rs);
EventLifecycle lifecycle = lifecycle(rs);
String externalSourceEventId = string(rs, "external_source_event_id");
if (externalSourceEventId == null) {
@ -59,13 +60,13 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
offsetDateTime(rs, "received_partner_at"),
OffsetDateTime.now(),
EventDomain.DRIVER_ACTIVITY,
eventType(rs),
lifecycle(rs),
eventType,
lifecycle,
longValue(rs, "odometer_m"),
null,
detailsFactory.driverActivity(cardSlot, cardStatus, drivingStatus),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs, context)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle, cardSlot, cardStatus, drivingStatus)),
isManualEntry(cardStatus, drivingStatus),
context.packageInfo()
);
@ -115,9 +116,24 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
);
}
private Map<String, Object> payload(ResultSet rs, ExtractionContext<TachographImportRequest> context) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType,
EventLifecycle lifecycle,
CardSlot cardSlot,
CardStatus cardStatus,
DrivingStatus drivingStatus
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, eventType, lifecycle
);
put(raw, "activityType", eventType == null ? null : eventType.name());
put(raw, "slot", cardSlot == null ? null : cardSlot.name());
put(raw, "cardStatus", cardStatus == null ? null : cardStatus.name());
put(raw, "drivingStatus", drivingStatus == null ? null : drivingStatus.name());
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -21,7 +21,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
@ -60,7 +59,7 @@ abstract class AbstractTachographBorderCrossingRowMapper implements ExtractionRo
position(rs),
detailsFactory.borderCrossing(string(rs, "country_from"), string(rs, "country_to")),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef)),
false,
context.packageInfo()
);
@ -112,9 +111,18 @@ abstract class AbstractTachographBorderCrossingRowMapper implements ExtractionRo
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, EventType.BORDER_OUTBOUND, EventLifecycle.OUTBOUND
);
put(raw, "supportEventType", EventType.BORDER_OUTBOUND.name());
put(raw, "countryFrom", string(rs, "country_from"));
put(raw, "countryTo", string(rs, "country_to"));
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -21,7 +21,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@ -44,6 +43,7 @@ abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapp
EventType eventType = eventType(rs);
EventLifecycle lifecycle = lifecycle(rs);
CardStatus cardStatus = cardStatus(lifecycle);
CardSlot cardSlot = cardSlot(rs);
String externalSourceEventId = string(rs, "external_source_event_id");
if (externalSourceEventId == null) {
@ -63,9 +63,9 @@ abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapp
lifecycle,
longValue(rs, "odometer_m"),
null,
detailsFactory.driverCard(cardSlot(rs), cardStatus, driverCard),
detailsFactory.driverCard(cardSlot, cardStatus, driverCard),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle, cardStatus, cardSlot)),
false,
context.packageInfo()
);
@ -111,9 +111,22 @@ abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapp
);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType,
EventLifecycle lifecycle,
CardStatus cardStatus,
CardSlot cardSlot
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, eventType, lifecycle
);
put(raw, "supportEventType", eventType == null ? null : eventType.name());
put(raw, "slot", cardSlot == null ? null : cardSlot.name());
put(raw, "cardStatus", cardStatus == null ? null : cardStatus.name());
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -21,7 +21,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@ -62,7 +61,7 @@ abstract class AbstractTachographLoadUnloadRowMapper implements ExtractionRowMap
position(rs),
detailsFactory.loadUnload(string(rs, "operation")),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType)),
false,
context.packageInfo()
);
@ -114,9 +113,18 @@ abstract class AbstractTachographLoadUnloadRowMapper implements ExtractionRowMap
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, eventType, EventLifecycle.SNAPSHOT
);
put(raw, "supportEventType", eventType == null ? null : eventType.name());
put(raw, "operation", string(rs, "operation"));
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -21,7 +21,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@ -63,7 +62,7 @@ abstract class AbstractTachographPlaceRowMapper implements ExtractionRowMapper<T
position(rs),
detailsFactory.place(string(rs, "country"), string(rs, "region")),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle)),
booleanValue(rs, "manual_entry"),
context.packageInfo()
);
@ -115,9 +114,20 @@ abstract class AbstractTachographPlaceRowMapper implements ExtractionRowMapper<T
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType,
EventLifecycle lifecycle
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, eventType, lifecycle
);
put(raw, "supportEventType", eventType == null ? null : eventType.name());
put(raw, "country", string(rs, "country"));
put(raw, "region", string(rs, "region"));
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -21,7 +21,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
@ -60,7 +59,7 @@ abstract class AbstractTachographPositionRowMapper implements ExtractionRowMappe
position(rs),
detailsFactory.position("GNSS_ACCUMULATED_DRIVING"),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef)),
false,
context.packageInfo()
);
@ -112,9 +111,16 @@ abstract class AbstractTachographPositionRowMapper implements ExtractionRowMappe
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, EventType.POSITION_RECORDED, EventLifecycle.SNAPSHOT
);
put(raw, "supportEventType", EventType.POSITION_RECORDED.name());
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -19,7 +19,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@ -62,7 +61,7 @@ abstract class AbstractTachographSpecificConditionRowMapper implements Extractio
null,
detailsFactory.specificCondition(),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle)),
false,
context.packageInfo()
);
@ -108,9 +107,18 @@ abstract class AbstractTachographSpecificConditionRowMapper implements Extractio
);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType,
EventLifecycle lifecycle
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, eventType, lifecycle
);
put(raw, "supportEventType", eventType == null ? null : eventType.name());
raw.putAll(sourceSpecificPayload(rs));
return Map.of("raw", raw);
}

View File

@ -20,7 +20,6 @@ import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
@ -63,7 +62,7 @@ public class SpeedingEventRowMapper implements ExtractionRowMapper<TachographImp
null,
detailsFactory.speeding(decimal(rs, "avg_speed_kmh"), decimal(rs, "max_speed_kmh"), null),
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
detailsFactory.payloadFromMap(payload(rs)),
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, lifecycle)),
false,
context.packageInfo()
);
@ -105,9 +104,19 @@ public class SpeedingEventRowMapper implements ExtractionRowMapper<TachographImp
);
}
private Map<String, Object> payload(ResultSet rs) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
put(raw, "sourceRowId", string(rs, "source_row_id"));
private Map<String, Object> payload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventLifecycle lifecycle
) throws SQLException {
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
rs, context, driverRef, vehicleRef, EventType.SPEEDING, lifecycle
);
put(raw, "supportEventType", EventType.SPEEDING.name());
put(raw, "avgSpeedKmh", decimal(rs, "avg_speed_kmh"));
put(raw, "maxSpeedKmh", decimal(rs, "max_speed_kmh"));
return Map.of("raw", raw);
}

View File

@ -0,0 +1,148 @@
package at.procon.eventhub.tachograph.service;
import at.procon.eventhub.dto.DriverCardRefDto;
import at.procon.eventhub.dto.DriverRefDto;
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.importing.extraction.ExtractionContext;
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
final class TachographRawPayloadSupport {
private TachographRawPayloadSupport() {
}
static Map<String, Object> baseRawPayload(
ResultSet rs,
ExtractionContext<TachographImportRequest> context,
DriverRefDto driverRef,
VehicleRefDto vehicleRef,
EventType eventType,
EventLifecycle lifecycle
) throws SQLException {
Map<String, Object> raw = new LinkedHashMap<>();
String sourceRowId = string(rs, "source_row_id");
put(raw, "sourceRowId", sourceRowId);
if (sourceRowId != null) {
put(raw, "sourceRowIds", List.of(sourceRowId));
put(raw, "intervalId", intervalId(context, sourceRowId));
}
put(raw, "sourceKind", context == null || context.planItem() == null ? null : context.planItem().sourceKind());
put(raw, "extractionCode", context == null || context.planItem() == null ? null : context.planItem().extractionCode());
put(raw, "level", "RAW_INTERVAL");
put(raw, "eventType", eventType == null ? null : eventType.name());
put(raw, "lifecycle", lifecycle == null ? null : lifecycle.name());
enrichDriver(raw, driverRef);
enrichVehicle(raw, vehicleRef);
enrichSourcePackage(raw, rs);
enrichOdometer(raw, rs);
return raw;
}
static void put(Map<String, Object> target, String key, Object value) {
if (value != null) {
target.put(key, value instanceof Enum<?> enumValue ? enumValue.name() : value);
}
}
private static String intervalId(ExtractionContext<TachographImportRequest> context, String sourceRowId) {
String extractionCode = context == null || context.planItem() == null ? null : context.planItem().extractionCode();
return "TACHOGRAPH:" + (extractionCode == null || extractionCode.isBlank() ? "UNKNOWN" : extractionCode) + ":" + sourceRowId;
}
private static void enrichDriver(Map<String, Object> raw, DriverRefDto driverRef) {
if (driverRef == null || !driverRef.hasAnyReference()) {
return;
}
put(raw, "driverKey", driverRef.stableKey());
put(raw, "driverSourceEntityId", driverRef.sourceEntityId());
DriverCardRefDto driverCard = driverRef.driverCard();
if (driverCard != null && driverCard.hasValue()) {
put(raw, "driverCardKey", driverCard.stableKey());
put(raw, "driverCardNation", driverCard.nation());
put(raw, "driverCardNationNumericCode", driverCard.nationNumericCode());
put(raw, "driverCardNumber", driverCard.number());
}
}
private static void enrichVehicle(Map<String, Object> raw, VehicleRefDto vehicleRef) {
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
return;
}
put(raw, "vehicleKey", vehicleRef.stableKey());
put(raw, "vehicleSourceEntityId", vehicleRef.sourceVehicleEntityId());
put(raw, "vehicleVin", vehicleRef.vin());
put(raw, "vehicleRegistrationSourceEntityId", vehicleRef.sourceRegistrationEntityId());
VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration();
if (registration != null && registration.hasValue()) {
put(raw, "registrationKey", registration.stableKey());
put(raw, "vehicleRegistrationNation", registration.nation());
put(raw, "vehicleRegistrationNationNumericCode", registration.nationNumericCode());
put(raw, "vehicleRegistrationNumber", registration.number());
}
}
private static void enrichSourcePackage(Map<String, Object> raw, ResultSet rs) throws SQLException {
put(raw, "sourcePackageKind", string(rs, "source_package_kind"));
put(raw, "sourcePackageId", string(rs, "source_package_id"));
put(raw, "sourcePackageEntityId", string(rs, "source_package_entity_id"));
}
private static void enrichOdometer(Map<String, Object> raw, ResultSet rs) throws SQLException {
Long odometerM = longValue(rs, "odometer_m");
put(raw, "odometerM", odometerM);
if (odometerM != null) {
put(raw, "odometerKm", odometerM / 1_000L);
}
}
private static String string(ResultSet rs, String column) throws SQLException {
try {
String value = rs.getString(column);
return value == null || value.isBlank() ? null : value.trim();
} catch (SQLException ex) {
if (missingColumn(ex)) {
return null;
}
throw ex;
}
}
private static Long longValue(ResultSet rs, String column) throws SQLException {
Object value;
try {
value = rs.getObject(column);
} catch (SQLException ex) {
if (missingColumn(ex)) {
return null;
}
throw ex;
}
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
return Long.parseLong(value.toString());
}
private static boolean missingColumn(SQLException ex) {
String state = ex.getSQLState();
String message = ex.getMessage();
return "S0022".equals(state)
|| "42703".equals(state)
|| (message != null && message.toLowerCase(java.util.Locale.ROOT).contains("column")
&& (message.toLowerCase(java.util.Locale.ROOT).contains("not found")
|| message.toLowerCase(java.util.Locale.ROOT).contains("not valid")
|| message.toLowerCase(java.util.Locale.ROOT).contains("invalid")));
}
}

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.tachographfilesession.dto;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
@ -13,6 +14,7 @@ import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
@Deprecated(forRemoval = false)
public record TachographEsperDriverProcessingResultDto(
UUID sessionId,
String driverKey,
@ -49,4 +51,86 @@ public record TachographEsperDriverProcessingResultDto(
List<TachographEsperSupportGeoEvent> supportGeoEvents,
List<String> notes
) {
public static TachographEsperDriverProcessingResultDto fromDriverWorkingTime(
DriverWorkingTimeProcessingResultDto result
) {
if (result == null) {
return null;
}
return new TachographEsperDriverProcessingResultDto(
result.sessionId(),
result.driverKey(),
result.sourceKind(),
result.loadedFrom(),
result.loadedTo(),
result.requestedFrom(),
result.requestedTo(),
result.activityIntervalCount(),
result.drivingIntervalCount(),
result.drivingInterruptionIntervalCount(),
result.drivingInterruptionVehicleChangeIntervalCount(),
result.dailyWeeklyRestCandidateIntervalCount(),
result.dailyWeeklyRestCandidateCoverageIntervalCount(),
result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(),
result.potentialHomeOvernightStayIntervalCount(),
result.potentialInVehicleOvernightStayIntervalCount(),
result.potentialInVehicleTripIntervalCount(),
result.vehicleUsageIntervalCount(),
result.vuCardAbsentIntervalCount(),
result.supportGeoEventCount(),
result.activityIntervals(),
result.drivingIntervals(),
result.drivingInterruptionIntervals(),
result.drivingInterruptionVehicleChangeIntervals(),
result.dailyWeeklyRestCandidateIntervals(),
result.dailyWeeklyRestCandidateCoverageIntervals(),
result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
result.potentialHomeOvernightStayIntervals(),
result.potentialInVehicleOvernightStayIntervals(),
result.potentialInVehicleTripIntervals(),
result.vehicleUsageIntervals(),
result.vuCardAbsentIntervals(),
result.supportGeoEvents(),
result.notes()
);
}
public DriverWorkingTimeProcessingResultDto toDriverWorkingTime() {
return new DriverWorkingTimeProcessingResultDto(
sessionId,
driverKey,
sourceKind,
loadedFrom,
loadedTo,
requestedFrom,
requestedTo,
activityIntervalCount,
drivingIntervalCount,
drivingInterruptionIntervalCount,
drivingInterruptionVehicleChangeIntervalCount,
dailyWeeklyRestCandidateIntervalCount,
dailyWeeklyRestCandidateCoverageIntervalCount,
unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
potentialHomeOvernightStayIntervalCount,
potentialInVehicleOvernightStayIntervalCount,
potentialInVehicleTripIntervalCount,
vehicleUsageIntervalCount,
vuCardAbsentIntervalCount,
supportGeoEventCount,
activityIntervals,
drivingIntervals,
drivingInterruptionIntervals,
drivingInterruptionVehicleChangeIntervals,
dailyWeeklyRestCandidateIntervals,
dailyWeeklyRestCandidateCoverageIntervals,
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
potentialHomeOvernightStayIntervals,
potentialInVehicleOvernightStayIntervals,
potentialInVehicleTripIntervals,
vehicleUsageIntervals,
vuCardAbsentIntervals,
supportGeoEvents,
notes
);
}
}

View File

@ -20,6 +20,19 @@ public interface DriverTimelineEventBuilder {
ResolvedDriverTimeline timeline
);
/**
* Builds the rawest point-event representation supported by the implementation.
*
* <p>The default preserves the existing interval-backed behavior. Implementations that can
* emit point events directly from source records should override this method.</p>
*/
default TachographTimelineEventBundle buildRawEventBundle(
TachographFileSession session,
DriverExtractionSession driverSession
) {
return buildEventBundle(session, driverSession);
}
default List<EventHubEventDto> buildEvents(
TachographFileSession session,
DriverExtractionSession driverSession

View File

@ -10,7 +10,13 @@ import com.espertech.esper.runtime.client.EPDeployException;
import com.espertech.esper.runtime.client.EPDeployment;
import com.espertech.esper.runtime.client.EPRuntime;
import com.espertech.esper.runtime.client.EPRuntimeProvider;
import com.fasterxml.jackson.databind.JsonNode;
import at.procon.eventhub.config.EventHubProperties;
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.processing.service.UnifiedEventTimelineReconstructor;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
@ -39,6 +45,7 @@ import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
@ -48,16 +55,42 @@ public class DriverTimelineReusableProjectionBuilder {
private static final AtomicLong RUNTIME_COUNTER = new AtomicLong();
private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE =
loadResource("esper/tachograph-driving-derived-projection-bundle.epl");
loadResource("esper/driver-working-time-derived-projections.epl");
private static final String DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL =
loadResource("esper/runtime-driver-event-interval-preprocessor.epl");
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);
}
public DriverTimelineReusableProjectionBuilder(
DriverTimelineBuilder driverTimelineBuilder,
RawSourceDriverTimelineEventBuilder rawSourceEventBuilder,
EventHubProperties properties
) {
this(driverTimelineBuilder, rawSourceEventBuilder, new UnifiedEventTimelineReconstructor(), properties);
}
@Autowired
public DriverTimelineReusableProjectionBuilder(
DriverTimelineBuilder driverTimelineBuilder,
RawSourceDriverTimelineEventBuilder rawSourceEventBuilder,
UnifiedEventTimelineReconstructor timelineReconstructor,
EventHubProperties properties
) {
this.driverTimelineBuilder = driverTimelineBuilder;
this.rawSourceEventBuilder = rawSourceEventBuilder;
this.timelineReconstructor = timelineReconstructor == null
? new UnifiedEventTimelineReconstructor()
: timelineReconstructor;
this.properties = properties;
}
@ -67,6 +100,18 @@ public class DriverTimelineReusableProjectionBuilder {
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
if (session == null || driverSession == null) {
return emptyBundle();
}
if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) {
return buildEsperDrivingDerivedProjectionBundleFromEvents(
session.sessionId(),
driverSession.driverKey(),
rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents(),
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession);
return buildEsperDrivingDerivedProjectionBundle(
session.sessionId(),
@ -86,6 +131,41 @@ public class DriverTimelineReusableProjectionBuilder {
return emptyBundle();
}
if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) {
List<Map<String, Object>> activityInputEvents = new ArrayList<>();
List<Map<String, Object>> vehicleUsageInputEvents = new ArrayList<>();
List<Map<String, Object>> supportGeoInputEvents = new ArrayList<>();
for (DriverExtractionSession driverSession : session.driversByKey().values()) {
if (driverSession == null || driverSession.driverKey() == null) {
continue;
}
ResolvedDriverTimeline timeline = reconstructMergedTimelineFromEvents(
session.sessionId(),
driverSession.driverKey(),
rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents()
);
if (timeline == null) {
continue;
}
activityInputEvents.addAll(buildActivityIntervalInputEvents(
session.sessionId(),
driverSession.driverKey(),
timeline.activityIntervals()
));
vehicleUsageInputEvents.addAll(buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals()));
supportGeoInputEvents.addAll(buildSupportGeoInputEvents(session.sessionId(), timeline.supportEvents()));
}
return buildEsperDrivingDerivedProjectionBundle(
activityInputEvents,
vehicleUsageInputEvents,
supportGeoInputEvents,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
List<Map<String, Object>> activityInputEvents = new ArrayList<>();
List<Map<String, Object>> vehicleUsageInputEvents = new ArrayList<>();
List<Map<String, Object>> supportGeoInputEvents = new ArrayList<>();
@ -234,6 +314,353 @@ public class DriverTimelineReusableProjectionBuilder {
);
}
public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents(
List<EventHubEventDto> events,
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
return buildEsperDrivingDerivedProjectionBundleFromEvents(
null,
null,
events,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents(
UUID fallbackSessionId,
String fallbackDriverKey,
List<EventHubEventDto> events,
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
if (fallbackDriverKey == null) {
Map<String, List<EventHubEventDto>> eventsByDriver = groupEventsByDriverKey(events);
if (eventsByDriver.size() > 1) {
return buildEsperDrivingDerivedProjectionBundleFromGroupedEvents(
fallbackSessionId,
eventsByDriver,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
if (eventsByDriver.size() == 1) {
Map.Entry<String, List<EventHubEventDto>> onlyDriver = eventsByDriver.entrySet().iterator().next();
fallbackDriverKey = onlyDriver.getKey();
events = onlyDriver.getValue();
}
}
ResolvedDriverTimeline reconstructedTimeline = reconstructMergedTimelineFromEvents(
fallbackSessionId,
fallbackDriverKey,
events
);
return buildEsperDrivingDerivedProjectionBundle(
fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId,
fallbackDriverKey,
reconstructedTimeline,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromGroupedEvents(
UUID fallbackSessionId,
Map<String, List<EventHubEventDto>> eventsByDriver,
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
List<Map<String, Object>> activityInputEvents = new ArrayList<>();
List<Map<String, Object>> vehicleUsageInputEvents = new ArrayList<>();
List<Map<String, Object>> supportGeoInputEvents = new ArrayList<>();
UUID sessionId = fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId;
for (Map.Entry<String, List<EventHubEventDto>> entry : eventsByDriver.entrySet()) {
String driverKey = entry.getKey();
if (driverKey == null || driverKey.isBlank()) {
continue;
}
ResolvedDriverTimeline timeline = reconstructMergedTimelineFromEvents(
sessionId,
driverKey,
entry.getValue()
);
if (timeline == null) {
continue;
}
activityInputEvents.addAll(buildActivityIntervalInputEvents(
sessionId,
driverKey,
timeline.activityIntervals()
));
vehicleUsageInputEvents.addAll(buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals()));
supportGeoInputEvents.addAll(buildSupportGeoInputEvents(sessionId, timeline.supportEvents()));
}
return buildEsperDrivingDerivedProjectionBundle(
activityInputEvents,
vehicleUsageInputEvents,
supportGeoInputEvents,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
private Map<String, List<EventHubEventDto>> groupEventsByDriverKey(List<EventHubEventDto> events) {
Map<String, List<EventHubEventDto>> grouped = new LinkedHashMap<>();
for (EventHubEventDto event : safeList(events)) {
String driverKey = eventDriverKey(event);
if (driverKey == null || driverKey.isBlank()) {
continue;
}
grouped.computeIfAbsent(driverKey, ignored -> new ArrayList<>()).add(event);
}
return grouped;
}
private String eventDriverKey(EventHubEventDto event) {
if (event == null) {
return null;
}
JsonNode raw = rawPayload(event);
return firstNonBlank(text(raw, "driverKey"), driverKey(event));
}
private ResolvedDriverTimeline reconstructMergedTimelineFromEvents(
UUID fallbackSessionId,
String fallbackDriverKey,
List<EventHubEventDto> events
) {
ResolvedDriverTimeline reconstructed = timelineReconstructor.reconstruct(
fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId,
fallbackDriverKey,
safeList(events)
);
return new ResolvedDriverTimeline(
reconstructed.sourceKind(),
reconstructed.loadedFrom(),
reconstructed.loadedTo(),
mergeVehicleUsageIntervals(reconstructed.vehicleUsageIntervals(), reconstructed.sourceKind()),
reconstructed.activityIntervals(),
reconstructed.supportEvents(),
reconstructed.warnings()
);
}
private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(
List<ResolvedVehicleUsageInterval> intervals,
String sourceKind
) {
List<ResolvedVehicleUsageInterval> sorted = safeList(intervals).stream()
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from)
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())))
.toList();
if (sorted.isEmpty()) {
return List.of();
}
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
ResolvedVehicleUsageInterval current = sorted.getFirst();
List<String> currentSources = new ArrayList<>(current.sourceIntervalIds());
for (int i = 1; i < sorted.size(); i++) {
ResolvedVehicleUsageInterval next = sorted.get(i);
if (canMergeVehicleUsage(current, next)) {
currentSources.addAll(next.sourceIntervalIds());
current = ResolvedVehicleUsageInterval.resolved(
current.sessionId(),
current.driverKey(),
current.intervalId() + "+" + next.intervalId(),
current.from(),
mergedTo(current.to(), next.to()),
current.odometerBeginKm(),
mergedOdometerEnd(current, next),
current.registrationKey(),
current.vehicleKey(),
sourceKind,
currentSources
);
} else {
result.add(current);
current = next;
currentSources = new ArrayList<>(current.sourceIntervalIds());
}
}
result.add(current);
return result;
}
private boolean canMergeVehicleUsage(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
return Objects.equals(left.registrationKey(), right.registrationKey())
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
&& !right.from().isAfter(mergeBoundary(left.to()));
}
private Long mergedOdometerEnd(
ResolvedVehicleUsageInterval current,
ResolvedVehicleUsageInterval next
) {
if (current == null) {
return next == null ? null : next.odometerEndKm();
}
if (next == null) {
return current.odometerEndKm();
}
if (current.to() == null || next.to() == null) {
return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm();
}
if (next.to().isAfter(current.to()) || next.to().isEqual(current.to())) {
return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm();
}
return current.odometerEndKm() != null ? current.odometerEndKm() : next.odometerEndKm();
}
private OffsetDateTime mergedTo(OffsetDateTime left, OffsetDateTime right) {
if (left == null || right == null) {
return null;
}
return left.isAfter(right) ? left : right;
}
private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) {
return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1);
}
private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromPointInput(
List<Map<String, Object>> activityPointInputEvents,
List<Map<String, Object>> vehicleUsagePointInputEvents,
List<Map<String, Object>> supportGeoInputEvents,
int significantDrivingMinutes,
int minimumRestPeriodMinutes
) {
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(
List<Map<String, Object>> vehicleUsagePointInputEvents
) {
Map<String, Map<String, Object>> byDriver = new LinkedHashMap<>();
for (Map<String, Object> point : safeList(vehicleUsagePointInputEvents)) {
String driverKey = Objects.toString(point.get("driverKey"), null);
if (driverKey == null || driverKey.isBlank()) {
continue;
}
Map<String, Object> finalizeEvent = byDriver.computeIfAbsent(driverKey, ignored -> {
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", point.get("sessionId"));
event.put("driverKey", driverKey);
event.put("finalizedAtEpochSecond", 0L);
return event;
});
Long occurredAtEpochSecond = (Long) point.get("occurredAtEpochSecond");
Long finalizedAtEpochSecond = (Long) finalizeEvent.get("finalizedAtEpochSecond");
if (occurredAtEpochSecond != null
&& (finalizedAtEpochSecond == null || occurredAtEpochSecond > finalizedAtEpochSecond)) {
finalizeEvent.put("finalizedAtEpochSecond", occurredAtEpochSecond);
}
}
return new ArrayList<>(byDriver.values());
}
private List<Map<String, Object>> buildActivityIntervalInputEvents(
UUID sessionId,
String driverKey,
@ -324,6 +751,56 @@ public class DriverTimelineReusableProjectionBuilder {
}
}
private Map<String, Object> activityPointInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("eventId", String.class);
definition.put("intervalId", String.class);
definition.put("sourceRowId", String.class);
definition.put("sourceRowIds", java.util.List.class);
definition.put("activityType", String.class);
definition.put("lifecycle", String.class);
definition.put("occurredAt", OffsetDateTime.class);
definition.put("occurredAtEpochSecond", long.class);
definition.put("cardSlot", String.class);
definition.put("cardStatus", String.class);
definition.put("drivingStatus", String.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("synthetic", boolean.class);
definition.put("clippedToRequestedPeriod", boolean.class);
definition.put("level", String.class);
return definition;
}
private Map<String, Object> vehicleUsagePointInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("eventId", String.class);
definition.put("intervalId", String.class);
definition.put("sourceRowId", String.class);
definition.put("sourceRowIds", java.util.List.class);
definition.put("lifecycle", String.class);
definition.put("occurredAt", OffsetDateTime.class);
definition.put("occurredAtEpochSecond", long.class);
definition.put("registrationKey", String.class);
definition.put("vehicleKey", String.class);
definition.put("sourceKind", String.class);
definition.put("odometerKm", Long.class);
return definition;
}
private Map<String, Object> projectionFinalizeInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
definition.put("driverKey", String.class);
definition.put("finalizedAtEpochSecond", long.class);
return definition;
}
private Map<String, Object> activityIntervalInputDefinition() {
Map<String, Object> definition = new LinkedHashMap<>();
definition.put("sessionId", UUID.class);
@ -470,6 +947,120 @@ public class DriverTimelineReusableProjectionBuilder {
return event;
}
private Map<String, Object> toActivityPointInputMap(
UUID fallbackSessionId,
String fallbackDriverKey,
EventHubEventDto sourceEvent
) {
if (sourceEvent == null
|| sourceEvent.eventDomain() != EventDomain.DRIVER_ACTIVITY
|| (sourceEvent.lifecycle() != EventLifecycle.START && sourceEvent.lifecycle() != EventLifecycle.END)
|| sourceEvent.occurredAt() == null) {
return null;
}
JsonNode raw = rawPayload(sourceEvent);
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent));
if (driverKey == null || intervalId == null) {
return null;
}
JsonNode attributes = attributes(sourceEvent);
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent));
event.put("driverKey", driverKey);
event.put("eventId", sourceEvent.externalSourceEventId());
event.put("intervalId", intervalId);
event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
event.put("activityType", firstNonBlank(text(raw, "activityType"), eventTypeAsActivity(sourceEvent.eventType())));
event.put("lifecycle", sourceEvent.lifecycle().name());
event.put("occurredAt", sourceEvent.occurredAt());
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
event.put("cardSlot", firstNonBlank(text(raw, "cardSlot"), text(attributes, "cardSlot")));
event.put("cardStatus", firstNonBlank(text(raw, "cardStatus"), text(attributes, "cardStatus")));
event.put("drivingStatus", firstNonBlank(text(raw, "drivingStatus"), text(attributes, "drivingStatus")));
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
event.put("synthetic", booleanValue(raw, "synthetic", false));
event.put("clippedToRequestedPeriod", booleanValue(raw, "clippedToRequestedPeriod", false));
event.put("level", firstNonBlank(text(raw, "level"), "RAW_EVENT"));
return event;
}
private Map<String, Object> toVehicleUsagePointInputMap(
UUID fallbackSessionId,
String fallbackDriverKey,
EventHubEventDto sourceEvent
) {
if (sourceEvent == null
|| sourceEvent.eventDomain() != EventDomain.DRIVER_CARD
|| (sourceEvent.lifecycle() != EventLifecycle.INSERT && sourceEvent.lifecycle() != EventLifecycle.WITHDRAW)
|| sourceEvent.occurredAt() == null) {
return null;
}
boolean supportedType = sourceEvent.eventType() == EventType.CARD_INSERTED
|| sourceEvent.eventType() == EventType.CARD_WITHDRAWN;
if (!supportedType) {
return null;
}
JsonNode raw = rawPayload(sourceEvent);
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent));
if (driverKey == null || intervalId == null) {
return null;
}
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent));
event.put("driverKey", driverKey);
event.put("eventId", sourceEvent.externalSourceEventId());
event.put("intervalId", intervalId);
event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
event.put("lifecycle", sourceEvent.lifecycle().name());
event.put("occurredAt", sourceEvent.occurredAt());
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
event.put("odometerKm", odometerKm(sourceEvent, raw));
return event;
}
private Map<String, Object> toSupportGeoEvidenceInputMap(
UUID fallbackSessionId,
String fallbackDriverKey,
EventHubEventDto sourceEvent
) {
if (sourceEvent == null || sourceEvent.occurredAt() == null || sourceEvent.position() == null) {
return null;
}
String eventDomain = sourceEvent.eventDomain() == null ? null : sourceEvent.eventDomain().name();
int priority = supportGeoPriority(eventDomain);
if (priority <= 0) {
return null;
}
JsonNode raw = rawPayload(sourceEvent);
String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent));
if (driverKey == null) {
return null;
}
Map<String, Object> event = new LinkedHashMap<>();
event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent));
event.put("driverKey", driverKey);
event.put("eventId", firstNonBlank(text(raw, "supportEventId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId()));
event.put("eventDomain", eventDomain);
event.put("occurredAt", sourceEvent.occurredAt());
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
event.put("latitude", sourceEvent.position().latitude().doubleValue());
event.put("longitude", sourceEvent.position().longitude().doubleValue());
event.put("odometerKm", odometerKm(sourceEvent, raw));
event.put("priority", priority);
return event;
}
private int supportGeoPriority(String eventDomain) {
if (eventDomain == null || eventDomain.isBlank()) {
return 0;
@ -483,6 +1074,186 @@ public class DriverTimelineReusableProjectionBuilder {
};
}
private Comparator<Map<String, Object>> pointEventComparator() {
return Comparator
.comparing((Map<String, Object> event) -> (Long) event.get("occurredAtEpochSecond"))
.thenComparing(event -> lifecycleOrder(Objects.toString(event.get("lifecycle"), "")))
.thenComparing(event -> Objects.toString(event.get("driverKey"), ""))
.thenComparing(event -> Objects.toString(event.get("intervalId"), ""))
.thenComparing(event -> Objects.toString(event.get("eventId"), ""));
}
private int lifecycleOrder(String lifecycle) {
return switch (lifecycle) {
case "INSERT", "START" -> 0;
case "WITHDRAW", "END" -> 1;
default -> 2;
};
}
private EventHubProperties.DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode() {
return properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode();
}
private RawSourceDriverTimelineEventBuilder rawSourceEventBuilder() {
if (rawSourceEventBuilder == null) {
throw new IllegalStateException(
"Driving-derived projection input mode EVENTS requires RawSourceDriverTimelineEventBuilder"
);
}
return rawSourceEventBuilder;
}
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 JsonNode attributes(EventHubEventDto event) {
return event.eventDetails() == null ? null : event.eventDetails().attributes();
}
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;
}
private boolean booleanValue(JsonNode node, String field, boolean fallback) {
if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
return fallback;
}
return node.get(field).asBoolean(fallback);
}
private Long longValue(JsonNode node, String field) {
if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
return null;
}
JsonNode value = node.get(field);
if (value.isNumber()) {
return value.asLong();
}
try {
return Long.parseLong(value.asText());
} catch (NumberFormatException ignored) {
return null;
}
}
private List<String> stringList(JsonNode node, String field, String fallback) {
JsonNode value = node == null || field == null ? null : node.get(field);
if (value == null || value.isNull()) {
return fallback == null ? List.of() : List.of(fallback);
}
if (value.isArray()) {
List<String> result = new ArrayList<>();
value.forEach(item -> {
if (item != null && !item.isNull()) {
String text = item.asText(null);
if (text != null && !text.isBlank()) {
result.add(text);
}
}
});
return result.isEmpty() && fallback != null ? List.of(fallback) : List.copyOf(result);
}
String text = value.asText(null);
return text == null || text.isBlank() ? (fallback == null ? List.of() : List.of(fallback)) : List.of(text);
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value.trim();
}
}
return null;
}
private String eventTypeAsActivity(EventType eventType) {
if (eventType == null) {
return "UNKNOWN";
}
return switch (eventType) {
case DRIVE -> "DRIVE";
case WORK -> "WORK";
case AVAILABILITY -> "AVAILABILITY";
case BREAK_REST -> "BREAK_REST";
default -> eventType.name();
};
}
private UUID sessionId(UUID fallbackSessionId, JsonNode raw, EventHubEventDto event) {
String rawSessionId = firstNonBlank(
text(raw, "sessionId"),
event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId()
);
if (rawSessionId != null) {
try {
return UUID.fromString(rawSessionId);
} catch (IllegalArgumentException ignored) {
// DB-acquired source packages need not be UUID-based file sessions.
}
}
return fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId;
}
private String driverKey(EventHubEventDto event) {
if (event.driverRef() == null) {
return null;
}
if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) {
return event.driverRef().driverCard().stableKey();
}
return event.driverRef().sourceEntityId();
}
private String registrationKey(EventHubEventDto event) {
if (event.vehicleRef() == null || event.vehicleRef().vehicleRegistration() == null) {
return null;
}
return event.vehicleRef().vehicleRegistration().stableKey();
}
private String vehicleKey(EventHubEventDto event) {
if (event.vehicleRef() == null) {
return null;
}
return firstNonBlank(event.vehicleRef().vin(), event.vehicleRef().sourceVehicleEntityId());
}
private String sourceKind(EventHubEventDto event) {
return event.packageInfo() == null || event.packageInfo().eventSource() == null
? null
: event.packageInfo().eventSource().sourceKind();
}
private Long odometerKm(EventHubEventDto event, JsonNode raw) {
Long explicit = longValue(raw, event.lifecycle() == EventLifecycle.WITHDRAW ? "odometerEndKm" : "odometerBeginKm");
if (explicit != null) {
return explicit;
}
explicit = longValue(raw, "odometerKm");
if (explicit != null) {
return explicit;
}
return event.odometerM() == null ? null : event.odometerM() / 1_000L;
}
private String firstSourceIntervalId(ResolvedActivityInterval interval) {
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
}
@ -901,6 +1672,12 @@ public class DriverTimelineReusableProjectionBuilder {
.toList();
}
private String renderDrivingDerivedProjectionEventsEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) {
return DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL
+ "\n\n"
+ renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes);
}
private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) {
return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE
.replace(

View File

@ -95,6 +95,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
List<EventHubEventDto> activityEvents = buildActivityEvents(
session,
driverSession.driverKey(),
timeline.activityIntervals(),
driverRef,
registrationsByKey,
@ -104,6 +105,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
);
List<EventHubEventDto> vehicleUsageEvents = buildVehicleUsageEvents(
session,
driverSession.driverKey(),
timeline.vehicleUsageIntervals(),
driverRef,
registrationsByKey,
@ -125,6 +127,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
private List<EventHubEventDto> buildActivityEvents(
TachographFileSession session,
String driverKey,
List<ResolvedActivityInterval> intervals,
DriverRefDto driverRef,
Map<String, ExtractedVehicleRegistration> registrationsByKey,
@ -147,6 +150,8 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
Map<String, Object> raw = new LinkedHashMap<>();
raw.put("intervalId", interval.intervalId());
raw.put("sourceRowId", interval.intervalId());
raw.put("driverKey", driverKey);
raw.put("activityType", interval.activityType());
raw.put("sourceRowIds", interval.sourceIntervalIds());
raw.put("startedAt", timeText(interval.from()));
raw.put("endedAt", timeText(interval.to()));
@ -200,6 +205,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
private List<EventHubEventDto> buildVehicleUsageEvents(
TachographFileSession session,
String driverKey,
List<ResolvedVehicleUsageInterval> intervals,
DriverRefDto driverRef,
Map<String, ExtractedVehicleRegistration> registrationsByKey,
@ -226,6 +232,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
Map<String, Object> raw = new LinkedHashMap<>();
raw.put("intervalId", interval.intervalId());
raw.put("sourceRowId", interval.intervalId());
raw.put("driverKey", driverKey);
raw.put("sourceRowIds", interval.sourceIntervalIds());
raw.put("startedAt", timeText(interval.from()));
raw.put("endedAt", timeText(interval.to()));

View File

@ -0,0 +1,211 @@
package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
import java.time.OffsetDateTime;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import org.springframework.stereotype.Component;
/**
* Builds EventHub point events directly from the extracted tachograph source records.
*
* <p>This builder intentionally does not run the normal {@link DriverTimelineBuilder} interval
* resolution/merging path. It only wraps each raw source interval as a START/END or
* INSERT/WITHDRAW point-event pair. Any subsequent pairing, coalescing, or derived interval logic
* belongs to the event-input EPL projection pipeline.</p>
*/
@Component
public class RawSourceDriverTimelineEventBuilder {
private final IntervalBackedDriverTimelineEventBuilder intervalEventBuilder;
public RawSourceDriverTimelineEventBuilder(IntervalBackedDriverTimelineEventBuilder intervalEventBuilder) {
this.intervalEventBuilder = intervalEventBuilder;
}
public TachographTimelineEventBundle buildRawEventBundle(
TachographFileSession session,
DriverExtractionSession driverSession
) {
if (session == null || driverSession == null) {
return new TachographTimelineEventBundle(List.of(), List.of(), List.of());
}
return intervalEventBuilder.buildEventBundle(session, driverSession, buildRawTimeline(session, driverSession));
}
public ResolvedDriverTimeline buildRawTimeline(
TachographFileSession session,
DriverExtractionSession driverSession
) {
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = rawVehicleUsageIntervals(
session,
driverSession,
sourceKind
);
List<ResolvedActivityInterval> activityIntervals = rawActivityIntervals(driverSession, sourceKind);
List<ExtractedSupportEvent> supportEvents = sortedSupportEvents(driverSession.supportEvents());
return new ResolvedDriverTimeline(
sourceKind,
minTimestamp(vehicleUsageIntervals, activityIntervals, supportEvents),
maxTimestamp(vehicleUsageIntervals, activityIntervals, supportEvents),
vehicleUsageIntervals,
activityIntervals,
supportEvents,
mergeWarnings(session.warnings(), driverSession.warnings())
);
}
private List<ResolvedVehicleUsageInterval> rawVehicleUsageIntervals(
TachographFileSession session,
DriverExtractionSession driverSession,
String sourceKind
) {
if (driverSession.cardVehicleUsageIntervals() == null || driverSession.cardVehicleUsageIntervals().isEmpty()) {
return List.of();
}
return driverSession.cardVehicleUsageIntervals().stream()
.filter(interval -> interval.from() != null && (interval.to() == null || interval.to().isAfter(interval.from())))
.map(interval -> ResolvedVehicleUsageInterval.resolved(
session.sessionId(),
driverSession.driverKey(),
interval.intervalId(),
interval.from(),
interval.to(),
interval.odometerBeginKm(),
interval.odometerEndKm(),
interval.registrationKey(),
interval.vehicleKey(),
sourceKind,
List.of(interval.intervalId())
))
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from)
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(ResolvedVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<ResolvedActivityInterval> rawActivityIntervals(
DriverExtractionSession driverSession,
String sourceKind
) {
if (driverSession.cardActivityIntervals() == null || driverSession.cardActivityIntervals().isEmpty()) {
return List.of();
}
return driverSession.cardActivityIntervals().stream()
.filter(interval -> interval.from() != null && interval.to() != null && interval.to().isAfter(interval.from()))
.map(interval -> ResolvedActivityInterval.raw(
interval.intervalId(),
interval.from(),
interval.to(),
normalizeActivity(interval.activityType()),
interval.slot(),
interval.cardStatus(),
interval.drivingStatus(),
interval.registrationKey(),
interval.vehicleKey(),
sourceKind,
List.of(interval.intervalId())
))
.sorted(Comparator.comparing(ResolvedActivityInterval::from)
.thenComparing(ResolvedActivityInterval::to)
.thenComparing(ResolvedActivityInterval::intervalId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private List<ExtractedSupportEvent> sortedSupportEvents(List<ExtractedSupportEvent> supportEvents) {
if (supportEvents == null || supportEvents.isEmpty()) {
return List.of();
}
return supportEvents.stream()
.sorted(Comparator.comparing(ExtractedSupportEvent::occurredAt)
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)))
.toList();
}
private String normalizeActivity(String activityType) {
if (activityType == null || activityType.isBlank()) {
return "UNKNOWN";
}
if ("UNKNOWN_ACTIVITY".equals(activityType)) {
return "UNKNOWN";
}
return activityType;
}
private OffsetDateTime minTimestamp(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
List<ResolvedActivityInterval> activityIntervals,
List<ExtractedSupportEvent> supportEvents
) {
OffsetDateTime min = null;
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
min = min(min, interval.from());
}
for (ResolvedActivityInterval interval : activityIntervals) {
min = min(min, interval.from());
}
for (ExtractedSupportEvent supportEvent : supportEvents) {
min = min(min, supportEvent.occurredAt());
}
return min;
}
private OffsetDateTime maxTimestamp(
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
List<ResolvedActivityInterval> activityIntervals,
List<ExtractedSupportEvent> supportEvents
) {
OffsetDateTime max = null;
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
max = max(max, interval.to());
}
for (ResolvedActivityInterval interval : activityIntervals) {
max = max(max, interval.to());
}
for (ExtractedSupportEvent supportEvent : supportEvents) {
max = max(max, supportEvent.occurredAt());
}
return max;
}
private List<ExtractionWarning> mergeWarnings(List<ExtractionWarning> sessionWarnings, List<ExtractionWarning> driverWarnings) {
LinkedHashSet<ExtractionWarning> merged = new LinkedHashSet<>();
if (sessionWarnings != null) {
merged.addAll(sessionWarnings);
}
if (driverWarnings != null) {
merged.addAll(driverWarnings);
}
return List.copyOf(merged);
}
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;
}
}

Some files were not shown because too many files have changed in this diff Show More