Add runtime processing pipeline and validation flows

This commit is contained in:
trifonovt 2026-05-26 15:27:29 +02:00
parent 5c887e8cb2
commit 82e2bd0860
65 changed files with 6143 additions and 879 deletions

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

@ -1,64 +1,67 @@
# Runtime Event Processing # Runtime event processing
Runtime Processing is now source-neutral. The API receives a runtime scope, selects a processing profile, partitions the normalized EventHub-style events, and delegates execution to the selected profile. 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 tachograph Esper processing is no longer the root concept. It is one profile: The preferred endpoint is:
```text
tachograph-driver-esper-v1
```
## Profile discovery endpoint
```http ```http
GET /api/eventhub/runtime-processing/event-processing/profiles POST /api/eventhub/runtime-processing/executions
``` ```
Example response: List available plans:
```json ```http
[ GET /api/eventhub/runtime-processing/executions/plans
{
"profileKey": "tachograph-driver-esper-v1",
"displayName": "Tachograph Driver Esper Processing",
"description": "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes.",
"defaultPartitioningStrategy": "DRIVER",
"supportedPartitioningStrategies": ["DRIVER"],
"requiredParameters": [],
"optionalParameters": [
"significantDrivingMinutes",
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents",
"vehicleEvidencePaddingMinutes",
"includePartitionDebug"
]
}
]
``` ```
Clients should prefer this endpoint instead of hardcoding profile metadata. The older endpoint is still available for compatibility:
## Generic execution endpoint
```http ```http
POST /api/eventhub/runtime-processing/event-processing POST /api/eventhub/runtime-processing/event-processing
``` ```
## Request shape 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 ```json
{ {
"profileKey": "tachograph-driver-esper-v1", "processingPlanKey": "driver-working-time-v1",
"scope": { "sourceSelection": {
"sessionIds": [ "sessionIds": [
"11111111-1111-1111-1111-111111111111", "11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222" "22222222-2222-2222-2222-222222222222"
], ],
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"], "sourceFamilies": ["TACHOGRAPH_FILE_SESSION", "YELLOWFOX_DB"],
"occurredFrom": "2026-05-01T00:00:00Z", "occurredFrom": "2026-05-01T00:00:00Z",
"occurredTo": "2026-05-31T23:59:59Z", "occurredTo": "2026-05-31T23:59:59Z",
"expandVehicleEvents": true, "expandVehicleEvents": true
"vehicleExpansionPaddingMinutes": 15
}, },
"partitioning": { "partitioning": {
"strategy": "DRIVER", "strategy": "DRIVER",
@ -70,175 +73,44 @@ POST /api/eventhub/runtime-processing/event-processing
"parameters": { "parameters": {
"significantDrivingMinutes": 3, "significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720, "minimumRestPeriodMinutes": 720,
"attachVehicleOnlyEvents": true,
"vehicleEvidencePaddingMinutes": 15,
"includePartitionDebug": true "includePartitionDebug": true
} }
} }
``` ```
## Response shape ## Conceptual flow
```json
{
"profileKey": "tachograph-driver-esper-v1",
"partitioningStrategy": "DRIVER",
"inputEventCount": 1234,
"selectedPartitionCount": 2,
"discoveredVehicleCount": 3,
"partitionResults": {
"12:12345678901234": {
"partitionType": "DRIVER",
"partitionKey": "12:12345678901234",
"resultType": "UnifiedRuntimeDerivedProjectionResultDto",
"result": {
"projection": {
"activityIntervals": [],
"drivingIntervals": [],
"drivingInterruptionIntervals": [],
"dailyWeeklyRestCandidateIntervals": [],
"dailyWeeklyRestCandidateCoverageIntervals": [],
"unclassifiedDailyWeeklyRestCandidateCoverageIntervals": [],
"potentialHomeOvernightStayIntervals": [],
"potentialInVehicleOvernightStayIntervals": [],
"potentialInVehicleTripIntervals": [],
"vehicleUsageIntervals": [],
"vuCardAbsentIntervals": [],
"supportGeoEvents": []
}
}
}
},
"notes": [],
"warnings": []
}
```
## Concepts
### Scope
`scope` is the existing runtime selection model. It can select events from:
```text ```text
TACHOGRAPH_FILE_SESSION source selection
TACHOGRAPH_DB -> runtime event loaders
YELLOWFOX_DB -> 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
``` ```
and can use: ## 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 ```text
SOURCE_DB
EVENTHUB_DB
```
where supported.
For uploaded tachograph files, the scope can use:
```text
sessionId
sessionIds
compositeSessionId
```
### Profile
A profile owns domain-specific processing semantics. It defines:
```text
profile key
expected partitioning strategy
profile-specific parameters
result type
```
Current profile:
```text
tachograph-driver-esper-v1
```
Future profiles can include:
```text
vehicle-stop-detection-v1
vehicle-trip-detection-v1 vehicle-trip-detection-v1
telematics-poi-clustering-v1 vehicle-stop-detection-v1
driver-settlement-v1 driver-settlement-v1
mixed-driver-vehicle-correlation-v1 mixed-driver-vehicle-correlation-v1
telematics-poi-clustering-v1
``` ```
### Partitioning Each plan may use different partitioning and modules, but the source loading and canonical event model remain common.
The common API supports generic partitioning options: ## Compatibility
```text Legacy profile endpoint:
NONE
DRIVER
VEHICLE
DRIVER_VEHICLE
SOURCE_FAMILY
CUSTOM_PROFILE
```
The first tachograph profile currently supports `DRIVER` partitioning. The service partitions mixed event scopes in Java before invoking Esper so that existing single-driver EPL windows cannot mix driver states.
## Partition debug / audit output
For mixed-source scopes, request debug output when validating why events were or were not attached to a driver partition:
```json
{
"partitioning": {
"strategy": "DRIVER",
"includeAllPartitions": true,
"attachVehicleEvidence": true,
"vehicleEvidencePaddingMinutes": 15,
"includeDebug": true
},
"parameters": {
"includePartitionDebug": true
}
}
```
When enabled, each generic `partitionResults[*].metadata.partitionDebug` contains:
```text
directDriverEventCount
vehicleUsageIntervalCount
candidateVehicleEvidenceEventCount
attachedVehicleEvidenceEventCount
ignoredVehicleEvidenceEventCount
mergedEventCount
vehicleUsageIntervals
vehicleEvidenceDecisions
notes
warnings
```
`vehicleEvidenceDecisions` explains every relevant decision, for example:
```text
DIRECT_DRIVER_EVENT
ATTACHED_VEHICLE_EVIDENCE
IGNORED_NO_OVERLAPPING_VEHICLE_USAGE
IGNORED_ATTACHMENT_DISABLED
```
The compatibility response type also has `partitionDebugByDriver`; use the generic endpoint when you need to enable debug output explicitly. Keep debug disabled for high-volume production requests unless you need attribution diagnostics, because the decision list can be large.
## Compatibility endpoint
The old tachograph endpoint remains available:
```http
POST /api/eventhub/runtime-processing/tachograph/esper-processing
```
It now acts as a compatibility adapter for:
```http ```http
POST /api/eventhub/runtime-processing/event-processing POST /api/eventhub/runtime-processing/event-processing
@ -246,143 +118,72 @@ POST /api/eventhub/runtime-processing/event-processing
with: with:
```text
profileKey = tachograph-driver-esper-v1
partitioning.strategy = DRIVER
```
## Tachograph profile processing flow
```text
runtime event scope
-> broad event assembly
-> driver partition discovery
-> for each driver:
direct driver events
reconstruct driver vehicle-usage intervals
attach only vehicle-scoped events whose vehicle matches and whose timestamp falls inside a usage interval plus configured padding
shared event-input Esper projection pipeline
-> generic partitionResults map
```
The tachograph profile reuses the same `TachographEsperProcessingCore` used by the file-session endpoint. This prevents the file-session API and runtime-processing API from drifting into separate rule chains.
## Vehicle-only evidence attachment
For driver-partitioned profiles, vehicle-only events are no longer attached only by vehicle identity. They are attached to a driver partition only when there is temporal evidence:
```text
vehicle-only event.vehicleKey/registrationKey matches a reconstructed driver vehicle-usage interval
and event.occurredAt is inside [usage.from - padding, usage.to + padding]
```
This prevents unrelated vehicle events from being copied into a driver result simply because the driver used the same vehicle on another day. The tachograph profile currently uses:
```json ```json
{ {
"partitioning": { "profileKey": "tachograph-driver-esper-v1",
"attachVehicleEvidence": true, "scope": {},
"vehicleEvidencePaddingMinutes": 15, "partitioning": {},
"includeDebug": true "parameters": {}
},
"parameters": {
"attachVehicleOnlyEvents": true,
"vehicleEvidencePaddingMinutes": 15,
"includePartitionDebug": true
}
} }
``` ```
`parameters` take precedence in the tachograph profile. The compatibility endpoint maps these values to `expandVehicleEvents` and `vehicleExpansionPaddingMinutes`. internally delegates to:
## Tachograph parity validation
A validation endpoint is available to compare the legacy tachograph file-session Esper endpoint with the generic runtime event-processing profile:
```http
POST /api/eventhub/runtime-processing/event-processing/validation/tachograph-parity
```
This endpoint runs both paths for the selected session(s) and driver(s):
```text ```text
legacy file-session path processingPlanKey = driver-working-time-v1
/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events
runtime event-processing path
/api/eventhub/runtime-processing/event-processing
profileKey = tachograph-driver-esper-v1
``` ```
Example request: ## Source-neutral driver working-time modules
```json The former tachograph-named processing artifacts are now represented by source-neutral driver working-time names:
{
"sessionId": "{{sessionId}}",
"driverKey": "{{driverKey}}",
"occurredFrom": "2026-05-01T00:00:00Z",
"occurredTo": "2026-05-31T23:59:59Z",
"expandVehicleEvents": true,
"vehicleExpansionPaddingMinutes": 15,
"significantDrivingMinutes": 3,
"minimumRestPeriodMinutes": 720,
"includeDebug": true
}
```
For multiple uploaded tachograph sessions, use `sessionIds` or `compositeSessionId`. The validation service resolves the selected drivers, executes the generic runtime profile, then compares count-level parity per driver.
Compared categories include:
```text ```text
activityIntervals runtime-driver-event-interval-preprocessor.epl
drivingIntervals driver-working-time-derived-projections.epl
drivingInterruptionIntervals DriverWorkingTimeProcessingResultDto
drivingInterruptionVehicleChangeIntervals DriverWorkingTimeProcessingCore
dailyWeeklyRestCandidateIntervals RuntimeDriverWorkingTimeScopeProcessingService
dailyWeeklyRestCandidateCoverageIntervals
unclassifiedDailyWeeklyRestCandidateCoverageIntervals
potentialHomeOvernightStayIntervals
potentialInVehicleOvernightStayIntervals
potentialInVehicleTripIntervals
vehicleUsageIntervals
vuCardAbsentIntervals
supportGeoEvents
``` ```
For a single session, this is a direct parity check against the original file-session endpoint. For multiple sessions, the reference side is the sum of the individual file-session endpoint results per driver; runtime processing may intentionally deduplicate or merge across session boundaries, so differences should be reviewed with the debug/audit output. 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.
## Runtime support evidence normalization ## Module execution results
The tachograph profile now normalizes mixed-source support events before invoking the shared Esper core: `/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly.
Top-level execution response includes:
```text ```text
runtime partition events moduleResults
-> RuntimeSupportEvidenceNormalizer
-> tachograph-consumable support evidence view
-> event-input Esper preprocessor / driving-derived bundle
``` ```
The normalizer does not change driver activity events or driver-card usage events. It only adapts support/vehicle events that carry geo or odometer evidence. Provider-specific semantics are preserved in the payload under `raw.originalEventDomain`, `raw.originalEventType`, `raw.originalLifecycle`, `raw.supportEventDomain`, and `raw.supportEventType`. 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.
Examples: Each partition result also includes:
```text ```text
IGNITION / IGNITION_ON with position partitionResults[*].moduleResults
-> POSITION / POSITION_RECORDED / SNAPSHOT
-> raw.supportEventType = IGNITION_ON
TELEMATICS_DATA with position
-> POSITION / POSITION_RECORDED / SNAPSHOT
-> raw.supportEventType = TELEMATICS_DATA
BORDER_CROSSING with position
-> BORDER_CROSSING, preserving the original border event type/lifecycle
LOAD_UNLOAD with position
-> LOAD_UNLOAD, preserving the original load/unload event type/lifecycle
``` ```
This keeps the EPL rules provider-neutral. YellowFox, tachograph VU, or future telematics events are converted to the common support-evidence shape before the tachograph profile consumes them. For `driver-working-time-v1`, the partition-level module results currently expose:
Runtime result notes include how many events were inspected and how many support events were adapted. Use partition debug together with normalization notes when validating mixed-source attribution. ```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

@ -1,60 +1,53 @@
# Runtime tachograph Esper scope processing # Runtime tachograph Esper scope processing
This document is kept for compatibility with earlier patches. This document is kept for compatibility. The preferred architecture is now the common Runtime Processing execution model.
The preferred endpoint is now the generic Runtime Event Processing endpoint: Preferred endpoint:
```http ```http
POST /api/eventhub/runtime-processing/event-processing POST /api/eventhub/runtime-processing/executions
``` ```
Use the tachograph profile key: Use:
```text ```json
tachograph-driver-esper-v1 {
"processingPlanKey": "driver-working-time-v1"
}
``` ```
The old endpoint remains available as an adapter: Legacy compatibility endpoint:
```http ```http
POST /api/eventhub/runtime-processing/tachograph/esper-processing POST /api/eventhub/runtime-processing/tachograph/esper-processing
``` ```
It delegates to the same profile infrastructure used by the generic endpoint. See: 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 ```text
docs/runtime-event-processing.md event-to-activity-intervals
event-to-vehicle-usage-intervals
vehicle-evidence-attachment
support-evidence-normalization
driving-derived-projections
``` ```
Vehicle-only evidence is now attached through the same common runtime event-processing profile. The old compatibility endpoint maps: 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.
```json
{
"expandVehicleEvents": true,
"vehicleExpansionPaddingMinutes": 15
}
```
to the generic profile behavior:
```json
{
"parameters": {
"attachVehicleOnlyEvents": true,
"vehicleEvidencePaddingMinutes": 15
}
}
```
Attachment is temporal: a vehicle-only event must match a reconstructed driver vehicle-usage interval and occur inside the interval plus configured padding.
## Debugging vehicle evidence attachment
Prefer the generic `/api/eventhub/runtime-processing/event-processing` endpoint with `partitioning.includeDebug=true` or `parameters.includePartitionDebug=true`. The compatibility response type has `partitionDebugByDriver`, but the generic endpoint is the preferred way to enable debug output explicitly. The generic response exposes debug data under `partitionResults[*].metadata.partitionDebug`.
## Support evidence normalization
The tachograph runtime profile is now implemented as a specialization of the generic runtime event-processing framework. Before calling the shared tachograph Esper core, mixed support events are normalized by `RuntimeSupportEvidenceNormalizer`.
This means that attached vehicle-only evidence from sources such as YellowFox ignition/position events can be consumed by the tachograph profile as support geo evidence when the event contains a position or odometer value. The provider-specific event meaning is preserved in the raw payload, while the Esper-facing event domain is adapted to the common tachograph support evidence contract.

View File

@ -2,7 +2,7 @@
"info": { "info": {
"name": "EventHub Runtime Event Processing", "name": "EventHub Runtime Event Processing",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "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." "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": [ "variable": [
{ {
@ -27,6 +27,138 @@
} }
], ],
"item": [ "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", "name": "List runtime event-processing profiles",
"request": { "request": {
@ -189,6 +321,232 @@
"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}" "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

@ -7,6 +7,13 @@ import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingServi
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto; 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.RuntimeTachographParityValidationApiRequest;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto; import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto;
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService; import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService;
@ -14,7 +21,7 @@ import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService; import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService;
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService; import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService; import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService; import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -32,27 +39,29 @@ public class UnifiedRuntimeProcessingController {
private final UnifiedRuntimeEventAssemblyService eventAssemblyService; private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService; private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService;
private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService; private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService;
private final UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService; private final RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService;
private final RuntimeEventProcessingService runtimeEventProcessingService; private final RuntimeEventProcessingService runtimeEventProcessingService;
private final RuntimeProcessingExecutionService runtimeProcessingExecutionService;
private final RuntimeTachographParityValidationService tachographParityValidationService; private final RuntimeTachographParityValidationService tachographParityValidationService;
private final RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService;
public UnifiedRuntimeProcessingController( public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService
) { ) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null); this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null, null, null);
} }
public UnifiedRuntimeProcessingController( public UnifiedRuntimeProcessingController(
UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService, UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService, RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService RuntimeEventProcessingService runtimeEventProcessingService
) { ) {
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
tachographEsperScopeProcessingService, runtimeEventProcessingService, null); tachographEsperScopeProcessingService, runtimeEventProcessingService, null, null, null);
} }
@Autowired @Autowired
@ -60,16 +69,33 @@ public class UnifiedRuntimeProcessingController {
UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeEventAssemblyService eventAssemblyService,
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService, UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService, RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
RuntimeEventProcessingService runtimeEventProcessingService, RuntimeEventProcessingService runtimeEventProcessingService,
RuntimeTachographParityValidationService tachographParityValidationService RuntimeTachographParityValidationService tachographParityValidationService,
RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService,
RuntimeProcessingExecutionService runtimeProcessingExecutionService
) { ) {
this.eventAssemblyService = eventAssemblyService; this.eventAssemblyService = eventAssemblyService;
this.runtimeDriverTimelineService = runtimeDriverTimelineService; this.runtimeDriverTimelineService = runtimeDriverTimelineService;
this.runtimeDerivedProjectionService = runtimeDerivedProjectionService; this.runtimeDerivedProjectionService = runtimeDerivedProjectionService;
this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService; this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService;
this.runtimeEventProcessingService = runtimeEventProcessingService; this.runtimeEventProcessingService = runtimeEventProcessingService;
this.runtimeProcessingExecutionService = runtimeProcessingExecutionService;
this.tachographParityValidationService = tachographParityValidationService; 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") @PostMapping("/driver-events")
@ -93,6 +119,25 @@ public class UnifiedRuntimeProcessingController {
return ResponseEntity.ok(runtimeDerivedProjectionService.loadDriverDerivedProjections(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") @GetMapping("/event-processing/profiles")
public ResponseEntity<List<RuntimeEventProcessingProfileDescriptorDto>> listEventProcessingProfiles() { public ResponseEntity<List<RuntimeEventProcessingProfileDescriptorDto>> listEventProcessingProfiles() {
if (runtimeEventProcessingService == null) { if (runtimeEventProcessingService == null) {
@ -122,6 +167,17 @@ public class UnifiedRuntimeProcessingController {
return ResponseEntity.ok(tachographParityValidationService.validate(request)); 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") @PostMapping("/tachograph/esper-processing")
public ResponseEntity<UnifiedRuntimeTachographEsperScopeResultDto> runTachographEsperProcessing( public ResponseEntity<UnifiedRuntimeTachographEsperScopeResultDto> runTachographEsperProcessing(
@RequestBody UnifiedRuntimeProcessingApiRequest request @RequestBody UnifiedRuntimeProcessingApiRequest request
@ -135,6 +191,8 @@ public class UnifiedRuntimeProcessingController {
if (tachographEsperScopeProcessingService == null) { if (tachographEsperScopeProcessingService == null) {
throw new IllegalStateException("Tachograph Esper scope processing service is not configured."); throw new IllegalStateException("Tachograph Esper scope processing service is not configured.");
} }
return ResponseEntity.ok(tachographEsperScopeProcessingService.processScope(request)); return ResponseEntity.ok(UnifiedRuntimeTachographEsperScopeResultDto.fromDriverWorkingTime(
tachographEsperScopeProcessingService.processScope(request)
));
} }
} }

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

@ -2,7 +2,7 @@ package at.procon.eventhub.processing.dto;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import java.util.List; import java.util.List;
public record UnifiedRuntimeDerivedProjectionResultDto( public record UnifiedRuntimeDerivedProjectionResultDto(
@ -12,8 +12,9 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
int expandedVehicleEventCount, int expandedVehicleEventCount,
int mergedEventCount, int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles, List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
TachographEsperDriverProcessingResultDto projection, DriverWorkingTimeProcessingResultDto projection,
List<String> notes, List<String> notes,
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization,
RuntimeDriverPartitionDebugDto partitionDebug RuntimeDriverPartitionDebugDto partitionDebug
) { ) {
public UnifiedRuntimeDerivedProjectionResultDto { public UnifiedRuntimeDerivedProjectionResultDto {
@ -28,7 +29,7 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
int expandedVehicleEventCount, int expandedVehicleEventCount,
int mergedEventCount, int mergedEventCount,
List<UnifiedDiscoveredVehicleRef> discoveredVehicles, List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
TachographEsperDriverProcessingResultDto projection, DriverWorkingTimeProcessingResultDto projection,
List<String> notes List<String> notes
) { ) {
this( this(
@ -40,10 +41,38 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
discoveredVehicles, discoveredVehicles,
projection, projection,
notes, notes,
null,
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) { public UnifiedRuntimeDerivedProjectionResultDto withPartitionDebug(RuntimeDriverPartitionDebugDto debug) {
return new UnifiedRuntimeDerivedProjectionResultDto( return new UnifiedRuntimeDerivedProjectionResultDto(
request, request,
@ -54,6 +83,7 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
discoveredVehicles, discoveredVehicles,
projection, projection,
notes, notes,
supportEvidenceNormalization,
debug 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

@ -28,6 +28,25 @@ public record UnifiedRuntimeTachographEsperScopeResultDto(
warnings = warnings == null ? List.of() : List.copyOf(warnings); 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( public static UnifiedRuntimeTachographEsperScopeResultDto fromGenericRuntimeEventProcessingResult(
RuntimeEventProcessingResultDto genericResult RuntimeEventProcessingResultDto genericResult
) { ) {

View File

@ -1,5 +1,6 @@
package at.procon.eventhub.processing.eventprocessing.dto; package at.procon.eventhub.processing.eventprocessing.dto;
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -9,9 +10,21 @@ public record RuntimeEventProcessingPartitionResultDto(
String partitionKey, String partitionKey,
String resultType, String resultType,
Object result, Object result,
Map<String, Object> metadata Map<String, Object> metadata,
Map<String, RuntimeProcessingModuleResult> moduleResults
) { ) {
public RuntimeEventProcessingPartitionResultDto { public RuntimeEventProcessingPartitionResultDto {
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata)); 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

@ -2,6 +2,7 @@ package at.procon.eventhub.processing.eventprocessing.dto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -26,4 +27,26 @@ public record RuntimeEventProcessingResultDto(
notes = notes == null ? List.of() : List.copyOf(notes); notes = notes == null ? List.of() : List.copyOf(notes);
warnings = warnings == null ? List.of() : List.copyOf(warnings); 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,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

@ -1,30 +1,35 @@
package at.procon.eventhub.processing.eventprocessing.profile; package at.procon.eventhub.processing.eventprocessing.profile;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService; import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
import java.util.LinkedHashMap; 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.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile { public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile {
public static final String PROFILE_KEY = "tachograph-driver-esper-v1"; /**
* 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 UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService; private final DriverWorkingTimeRuntimeProcessingPlan plan;
public TachographDriverEsperRuntimeEventProcessingProfile( @Autowired
UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService public TachographDriverEsperRuntimeEventProcessingProfile(DriverWorkingTimeRuntimeProcessingPlan plan) {
) { this.plan = plan;
this.tachographScopeProcessingService = tachographScopeProcessingService; }
public TachographDriverEsperRuntimeEventProcessingProfile(RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService) {
this(new DriverWorkingTimeRuntimeProcessingPlan(scopeProcessingService));
} }
@Override @Override
@ -34,7 +39,7 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti
@Override @Override
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() { public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
return RuntimeEventPartitioningStrategy.DRIVER; return plan.defaultPartitioningStrategy();
} }
@Override @Override
@ -44,202 +49,35 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti
@Override @Override
public String description() { public String description() {
return "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes. " return "Compatibility adapter for legacy profileKey=" + PROFILE_KEY
+ "The profile partitions mixed runtime events by driver, attaches vehicle evidence by vehicle/time, " + ". New clients should use processingPlanKey=" + plan.processingPlanKey() + ". "
+ "normalizes mixed-source support evidence, and then invokes the event-input EPL pipeline."; + plan.description();
} }
@Override @Override
public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() { public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
return List.of(RuntimeEventPartitioningStrategy.DRIVER); return plan.supportedPartitioningStrategies();
} }
@Override @Override
public Set<String> optionalParameters() { public Set<String> optionalParameters() {
return Set.of( return plan.optionalParameters();
"significantDrivingMinutes", }
"minimumRestPeriodMinutes",
"attachVehicleOnlyEvents", @Override
"vehicleEvidencePaddingMinutes", public Set<String> requiredParameters() {
"includePartitionDebug" return plan.requiredParameters();
);
} }
@Override @Override
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) { public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
boolean includePartitionDebug = booleanParameter( RuntimeProcessingExecutionResultDto executionResult = plan.execute(new RuntimeProcessingExecutionApiRequest(
request.parameters(), plan.processingPlanKey(),
"includePartitionDebug", request.scope(),
request.partitioning() != null && request.partitioning().includeDebugOrDefault() request.partitioning(),
); List.of(),
UnifiedRuntimeProcessingApiRequest tachographScopeRequest = applyGenericRequest(request.scope(), request.partitioning(), request.parameters()); request.parameters()
UnifiedRuntimeTachographEsperScopeResultDto tachographResult = tachographScopeProcessingService.processScope( ));
tachographScopeRequest, return RuntimeEventProcessingResultDto.fromExecution(executionResult, profileKey());
includePartitionDebug
);
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>();
tachographResult.driverResults().forEach((driverKey, driverResult) -> {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "TachographEsperDriverProcessingResultDto");
metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
metadata.put("mergedEventCount", driverResult.mergedEventCount());
if (driverResult.partitionDebug() != null) {
metadata.put("partitionDebug", driverResult.partitionDebug());
}
partitionResults.put(
driverKey,
new RuntimeEventProcessingPartitionResultDto(
"DRIVER",
driverKey,
"UnifiedRuntimeDerivedProjectionResultDto",
driverResult,
metadata
)
);
});
return new RuntimeEventProcessingResultDto(
profileKey(),
RuntimeEventPartitioningStrategy.DRIVER,
tachographResult.request(),
tachographResult.inputEventCount(),
tachographResult.selectedDriverCount(),
tachographResult.discoveredVehicleCount(),
tachographResult.discoveredVehicles(),
partitionResults,
tachographResult.notes(),
tachographResult.warnings()
);
}
private UnifiedRuntimeProcessingApiRequest applyGenericRequest(
UnifiedRuntimeProcessingApiRequest scope,
RuntimeEventPartitioningApiRequest partitioning,
Map<String, Object> parameters
) {
if (scope == null) {
throw new IllegalArgumentException("scope must not be null for profile " + PROFILE_KEY + ".");
}
boolean includeAllDrivers = scope.includeAllDrivers() != null && scope.includeAllDrivers();
java.util.Set<String> driverKeys = scope.driverKeys();
boolean includeAllVehicles = scope.includeAllVehicles() != null && scope.includeAllVehicles();
java.util.Set<String> vehicleKeys = scope.vehicleKeys();
if (partitioning != null) {
RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null
? RuntimeEventPartitioningStrategy.DRIVER
: partitioning.strategy();
if (strategy != RuntimeEventPartitioningStrategy.DRIVER
&& strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) {
throw new IllegalArgumentException("Profile " + PROFILE_KEY
+ " currently supports DRIVER partitioning only. Requested: " + strategy);
}
includeAllDrivers = includeAllDrivers || partitioning.allPartitions() || partitioning.allDrivers();
if (!partitioning.driverKeys().isEmpty()) {
driverKeys = partitioning.driverKeys();
} else if (!partitioning.partitionKeys().isEmpty()) {
driverKeys = partitioning.partitionKeys();
}
includeAllVehicles = includeAllVehicles || partitioning.allVehicles();
if (!partitioning.vehicleKeys().isEmpty()) {
vehicleKeys = partitioning.vehicleKeys();
}
}
Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", scope.significantDrivingMinutes());
Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", scope.minimumRestPeriodMinutes());
boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents",
partitioning == null ? scope.expandVehicleEvents() == null || scope.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault());
Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes",
partitioning == null
? scope.vehicleExpansionPaddingMinutes()
: partitioning.vehicleEvidencePaddingMinutesOrDefault(scope.vehicleExpansionPaddingMinutes() == null ? 0 : scope.vehicleExpansionPaddingMinutes()));
return new UnifiedRuntimeProcessingApiRequest(
scope.sessionId(),
scope.sessionIds(),
scope.compositeSessionId(),
scope.tenantKey(),
scope.sourceFamilies(),
scope.eventBackend(),
scope.driverKey(),
driverKeys,
includeAllDrivers,
vehicleKeys,
includeAllVehicles,
scope.driverSourceEntityId(),
scope.driverCardNation(),
scope.driverCardNumber(),
scope.occurredFrom(),
scope.occurredTo(),
attachVehicleOnlyEvents,
vehicleEvidencePaddingMinutes,
significantDrivingMinutes,
minimumRestPeriodMinutes
);
}
private boolean booleanParameter(Map<String, Object> parameters, String key, boolean fallback) {
if (parameters == null || !parameters.containsKey(key)) {
return fallback;
}
Object value = parameters.get(key);
if (value == null) {
return fallback;
}
if (value instanceof Boolean booleanValue) {
return booleanValue;
}
String text = value.toString();
if (text == null || text.isBlank()) {
return fallback;
}
return Boolean.parseBoolean(text.trim());
}
private Integer nonNegativeIntegerParameter(Map<String, Object> parameters, String key, Integer fallback) {
if (parameters == null || !parameters.containsKey(key)) {
return fallback;
}
Object value = parameters.get(key);
if (value == null) {
return fallback;
}
if (value instanceof Number number) {
return Math.max(0, number.intValue());
}
String text = value.toString();
if (text == null || text.isBlank()) {
return fallback;
}
try {
return Math.max(0, Integer.parseInt(text.trim()));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
}
}
private Integer integerParameter(Map<String, Object> parameters, String key, Integer fallback) {
if (parameters == null || !parameters.containsKey(key)) {
return fallback;
}
Object value = parameters.get(key);
if (value == null) {
return fallback;
}
if (value instanceof Number number) {
return Math.max(1, number.intValue());
}
String text = value.toString();
if (text == null || text.isBlank()) {
return fallback;
}
try {
return Math.max(1, Integer.parseInt(text.trim()));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
}
} }
} }

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

@ -1,6 +1,7 @@
package at.procon.eventhub.processing.eventprocessing.validation; package at.procon.eventhub.processing.eventprocessing.validation;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; 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.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService; import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
@ -347,6 +348,10 @@ public class RuntimeTachographParityValidationService {
} }
static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) { static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) {
return projection == null ? empty() : from(projection.toDriverWorkingTime());
}
static ProjectionCounts from(DriverWorkingTimeProcessingResultDto projection) {
if (projection == null) { if (projection == null) {
return empty(); return empty();
} }

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

@ -4,12 +4,13 @@ import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.dto.DriverRefDto;
import at.procon.eventhub.dto.EventHubEventDto; import at.procon.eventhub.dto.EventHubEventDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; 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.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult;
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
@ -24,7 +25,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsag
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder; import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder; import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore; import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput; import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
import java.time.Duration; import java.time.Duration;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@ -44,7 +45,7 @@ public class UnifiedRuntimeDerivedProjectionService {
private final UnifiedEventTimelineReconstructor timelineReconstructor; private final UnifiedEventTimelineReconstructor timelineReconstructor;
private final DriverTimelineBuilder driverTimelineBuilder; private final DriverTimelineBuilder driverTimelineBuilder;
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder; private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
private final TachographEsperProcessingCore esperProcessingCore; private final DriverWorkingTimeProcessingCore workingTimeProcessingCore;
private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer; private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer;
private final EventHubProperties properties; private final EventHubProperties properties;
@ -62,7 +63,7 @@ public class UnifiedRuntimeDerivedProjectionService {
driverTimelineBuilder, driverTimelineBuilder,
reusableProjectionBuilder, reusableProjectionBuilder,
properties, properties,
new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties), new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)),
supportEvidenceNormalizer supportEvidenceNormalizer
); );
} }
@ -74,7 +75,7 @@ public class UnifiedRuntimeDerivedProjectionService {
DriverTimelineBuilder driverTimelineBuilder, DriverTimelineBuilder driverTimelineBuilder,
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
EventHubProperties properties, EventHubProperties properties,
TachographEsperProcessingCore esperProcessingCore, DriverWorkingTimeProcessingCore workingTimeProcessingCore,
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
) { ) {
this.runtimeEventAssemblyService = runtimeEventAssemblyService; this.runtimeEventAssemblyService = runtimeEventAssemblyService;
@ -82,7 +83,7 @@ public class UnifiedRuntimeDerivedProjectionService {
this.driverTimelineBuilder = driverTimelineBuilder; this.driverTimelineBuilder = driverTimelineBuilder;
this.reusableProjectionBuilder = reusableProjectionBuilder; this.reusableProjectionBuilder = reusableProjectionBuilder;
this.properties = properties; this.properties = properties;
this.esperProcessingCore = esperProcessingCore; this.workingTimeProcessingCore = workingTimeProcessingCore;
this.supportEvidenceNormalizer = supportEvidenceNormalizer; this.supportEvidenceNormalizer = supportEvidenceNormalizer;
} }
@ -133,14 +134,14 @@ public class UnifiedRuntimeDerivedProjectionService {
List<String> notes = new ArrayList<>(eventBundle.notes()); List<String> notes = new ArrayList<>(eventBundle.notes());
notes.addAll(normalizationResult.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 tachograph Esper processing core."); 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("Significant driving threshold minutes: " + significantDrivingMinutes + ".");
notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + "."); notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + ".");
if (request.occurredFrom() != null || request.occurredTo() != null) { 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."); notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request.");
} }
TachographEsperDriverProcessingResultDto projection = esperProcessingCore.process(TachographEsperProcessingInput.fromEvents( DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(TachographEsperProcessingInput.fromEvents(
runtimeSessionId(request), runtimeSessionId(request),
driverKey, driverKey,
timeline, timeline,
@ -153,6 +154,13 @@ public class UnifiedRuntimeDerivedProjectionService {
)); ));
notes = projection.notes(); notes = projection.notes();
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
normalizationResult.inputEventCount(),
normalizationResult.normalizedSupportEvidenceEventCount(),
normalizationResult.unchangedEventCount(),
normalizationResult.notes()
);
return new UnifiedRuntimeDerivedProjectionResultDto( return new UnifiedRuntimeDerivedProjectionResultDto(
request, request,
eventBundle.driverSeedEvents().size(), eventBundle.driverSeedEvents().size(),
@ -161,7 +169,8 @@ public class UnifiedRuntimeDerivedProjectionService {
eventBundle.mergedEvents().size(), eventBundle.mergedEvents().size(),
eventBundle.discoveredVehicles(), eventBundle.discoveredVehicles(),
projection, projection,
notes notes,
normalizationDebug
); );
} }

View File

@ -142,10 +142,10 @@ public class UnifiedRuntimeEventAssemblyService {
appendDeduplicated(byKey, right); appendDeduplicated(byKey, right);
return byKey.values().stream() return byKey.values().stream()
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder())) .sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(event -> event.eventDomain().name()) .thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
.thenComparing(event -> event.eventType().name()) .thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
.thenComparing(event -> event.lifecycle().name()) .thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
.thenComparing(EventHubEventDto::externalSourceEventId)) .thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
.toList(); .toList();
} }

View File

@ -1,319 +1,48 @@
package at.procon.eventhub.processing.service; package at.procon.eventhub.processing.service;
import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
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.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.springframework.stereotype.Service;
@Service /**
* @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 { public class UnifiedRuntimeTachographEsperScopeProcessingService {
private final UnifiedRuntimeEventAssemblyService eventAssemblyService; private final RuntimeDriverWorkingTimeScopeProcessingService delegate;
private final UnifiedRuntimeDerivedProjectionService derivedProjectionService;
private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;
public UnifiedRuntimeTachographEsperScopeProcessingService( public UnifiedRuntimeTachographEsperScopeProcessingService(
UnifiedRuntimeEventAssemblyService eventAssemblyService, RuntimeDriverWorkingTimeScopeProcessingService delegate
UnifiedRuntimeDerivedProjectionService derivedProjectionService,
RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
) { ) {
this.eventAssemblyService = eventAssemblyService; this.delegate = delegate;
this.derivedProjectionService = derivedProjectionService;
this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
} }
public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) { public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
return processScope(apiRequest, false); return toLegacy(delegate.processScope(apiRequest));
} }
public UnifiedRuntimeTachographEsperScopeResultDto processScope( public UnifiedRuntimeTachographEsperScopeResultDto processScope(
UnifiedRuntimeProcessingApiRequest apiRequest, UnifiedRuntimeProcessingApiRequest apiRequest,
boolean includePartitionDebug boolean includePartitionDebug
) { ) {
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest(); return toLegacy(delegate.processScope(apiRequest, includePartitionDebug));
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 tachograph Esper scope processing used Java-side driver partitioning before calling the common event-input Esper pipeline.");
notes.add("Selected driver partitions: " + selectedDriverKeys.size() + ".");
if (!request.includeAllDrivers() && !request.driverKeys().isEmpty()) {
notes.add("The broad runtime event set was filtered to the requested driverKeys.");
}
if (!request.vehicleKeys().isEmpty() || request.includeAllVehicles()) {
notes.add("vehicleKeys/includeAllVehicles are accepted in the request model for source-neutral scopes; driver partitions are enriched only with vehicle evidence that overlaps reconstructed driver vehicle-usage intervals.");
}
notes.add("Vehicle-only evidence attachment is controlled by expandVehicleEvents/attachVehicleOnlyEvents and vehicleExpansionPaddingMinutes/vehicleEvidencePaddingMinutes.");
private UnifiedRuntimeTachographEsperScopeResultDto toLegacy(
UnifiedRuntimeDriverWorkingTimeScopeResultDto result
) {
return new UnifiedRuntimeTachographEsperScopeResultDto( return new UnifiedRuntimeTachographEsperScopeResultDto(
request, result.request(),
broadBundle.mergedEvents().size(), result.inputEventCount(),
driverResults.size(), result.selectedDriverCount(),
broadBundle.discoveredVehicles().size(), result.discoveredVehicleCount(),
broadBundle.discoveredVehicles(), result.discoveredVehicles(),
driverResults, result.driverResults(),
partitionDebugByDriver, result.partitionDebugByDriver(),
notes, result.notes(),
warnings result.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

@ -1,5 +1,6 @@
package at.procon.eventhub.tachographfilesession.dto; 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.TachographEsperActivityIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
@ -13,6 +14,7 @@ import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@Deprecated(forRemoval = false)
public record TachographEsperDriverProcessingResultDto( public record TachographEsperDriverProcessingResultDto(
UUID sessionId, UUID sessionId,
String driverKey, String driverKey,
@ -49,4 +51,86 @@ public record TachographEsperDriverProcessingResultDto(
List<TachographEsperSupportGeoEvent> supportGeoEvents, List<TachographEsperSupportGeoEvent> supportGeoEvents,
List<String> notes 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

@ -55,9 +55,9 @@ public class DriverTimelineReusableProjectionBuilder {
private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); private static final AtomicLong RUNTIME_COUNTER = new AtomicLong();
private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE = 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 = private static final String DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL =
loadResource("esper/tachograph-driving-derived-projection-events-preprocessor.epl"); loadResource("esper/runtime-driver-event-interval-preprocessor.epl");
private final DriverTimelineBuilder driverTimelineBuilder; private final DriverTimelineBuilder driverTimelineBuilder;
private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder; private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder;

View File

@ -1,6 +1,7 @@
package at.procon.eventhub.tachographfilesession.service; package at.procon.eventhub.tachographfilesession.service;
import at.procon.eventhub.config.EventHubProperties; import at.procon.eventhub.config.EventHubProperties;
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
import at.procon.eventhub.tachographfilesession.model.*; import at.procon.eventhub.tachographfilesession.model.*;
import java.time.Duration; import java.time.Duration;
@ -12,6 +13,7 @@ import java.util.Objects;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@Deprecated(forRemoval = false)
public class TachographEsperProcessingCore { public class TachographEsperProcessingCore {
private final DriverTimelineBuilder driverTimelineBuilder; private final DriverTimelineBuilder driverTimelineBuilder;
@ -29,6 +31,10 @@ public class TachographEsperProcessingCore {
} }
public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) { public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(processDriverWorkingTime(input));
}
public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) {
Objects.requireNonNull(input, "input must not be null"); Objects.requireNonNull(input, "input must not be null");
ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null"); ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null");
String driverKey = input.driverKey(); String driverKey = input.driverKey();
@ -132,7 +138,7 @@ public class TachographEsperProcessingCore {
requestedTo requestedTo
); );
return new TachographEsperDriverProcessingResultDto( return new DriverWorkingTimeProcessingResultDto(
input.sessionId(), input.sessionId(),
driverKey, driverKey,
timeline.sourceKind(), timeline.sourceKind(),

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
/*
* Source-neutral EPL module: DriverActivityPointEvent -> DriverActivityIntervalEvent.
* This module is intended for Runtime Processing plans and is independent of any concrete source provider.
*/
create schema DriverActivityIntervalEvent(
sessionId java.util.UUID,
driverKey string,
intervalId string,
activityType string,
cardSlot string,
cardStatus string,
drivingStatus string,
registrationKey string,
vehicleKey string,
sourceKind string,
firstSourceIntervalId string,
lastSourceIntervalId string,
startedAt java.time.OffsetDateTime,
endedAt java.time.OffsetDateTime,
startedAtEpochSecond long,
endedAtEpochSecond long,
durationSeconds long,
sourceIntervalIds java.util.List,
synthetic boolean,
clippedToRequestedPeriod boolean,
level string
);
create window OpenDriverActivityPoint#unique(driverKey, intervalId) as DriverActivityPointEvent;
insert into OpenDriverActivityPoint
select *
from DriverActivityPointEvent(lifecycle = 'START');
@Priority(20)
on DriverActivityPointEvent(lifecycle = 'END') as endEvent
insert into DriverActivityIntervalEvent
select
startEvent.sessionId as sessionId,
startEvent.driverKey as driverKey,
startEvent.intervalId as intervalId,
startEvent.activityType as activityType,
startEvent.cardSlot as cardSlot,
startEvent.cardStatus as cardStatus,
startEvent.drivingStatus as drivingStatus,
startEvent.registrationKey as registrationKey,
startEvent.vehicleKey as vehicleKey,
startEvent.sourceKind as sourceKind,
startEvent.sourceRowId as firstSourceIntervalId,
endEvent.sourceRowId as lastSourceIntervalId,
startEvent.occurredAt as startedAt,
endEvent.occurredAt as endedAt,
startEvent.occurredAtEpochSecond as startedAtEpochSecond,
endEvent.occurredAtEpochSecond as endedAtEpochSecond,
endEvent.occurredAtEpochSecond - startEvent.occurredAtEpochSecond as durationSeconds,
startEvent.sourceRowIds as sourceIntervalIds,
startEvent.synthetic as synthetic,
startEvent.clippedToRequestedPeriod as clippedToRequestedPeriod,
startEvent.level as level
from OpenDriverActivityPoint as startEvent
where startEvent.driverKey = endEvent.driverKey
and startEvent.intervalId = endEvent.intervalId
and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond;
@Priority(10)
on DriverActivityPointEvent(lifecycle = 'END') as endEvent
delete from OpenDriverActivityPoint as openEvent
where openEvent.driverKey = endEvent.driverKey
and openEvent.intervalId = endEvent.intervalId;
@name('driverActivityIntervals')
select *
from DriverActivityIntervalEvent;

View File

@ -0,0 +1,348 @@
/*
* Source-neutral event-input adapter for driver-working-time-derived-projections.epl.
*
* The old bundle consumes resolved interval input streams. This preprocessor lets the same
* derived rules consume EventHub point events by pairing START/END activity events and
* INSERT/WITHDRAW card-vehicle usage events inside Esper.
*
* Vehicle-usage intervals are additionally coalesced in EPL before they are forwarded to
* the old interval bundle. This keeps event mode equivalent to DriverTimelineBuilder's
* Java merge behavior for consecutive same-vehicle CardVehiclesUsed rows, including the
* common midnight continuation 23:59:59 -> 00:00:00 on the next day.
*/
create window OpenActivityPoint#unique(driverKey, intervalId) as TachographActivityPointInputEvent;
insert into OpenActivityPoint
select *
from TachographActivityPointInputEvent(lifecycle = 'START');
@Priority(20)
on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent
insert into TachographActivityIntervalInputEvent
select
startEvent.sessionId as sessionId,
startEvent.driverKey as driverKey,
startEvent.intervalId as intervalId,
startEvent.activityType as activityType,
startEvent.cardSlot as cardSlot,
startEvent.cardStatus as cardStatus,
startEvent.drivingStatus as drivingStatus,
startEvent.registrationKey as registrationKey,
startEvent.vehicleKey as vehicleKey,
startEvent.sourceKind as sourceKind,
startEvent.sourceRowId as firstSourceIntervalId,
endEvent.sourceRowId as lastSourceIntervalId,
startEvent.occurredAt as startedAt,
endEvent.occurredAt as endedAt,
startEvent.occurredAtEpochSecond as startedAtEpochSecond,
endEvent.occurredAtEpochSecond as endedAtEpochSecond,
endEvent.occurredAtEpochSecond - startEvent.occurredAtEpochSecond as durationSeconds,
startEvent.sourceRowIds as sourceIntervalIds,
startEvent.synthetic as synthetic,
startEvent.clippedToRequestedPeriod as clippedToRequestedPeriod,
startEvent.level as level
from OpenActivityPoint as startEvent
where startEvent.driverKey = endEvent.driverKey
and startEvent.intervalId = endEvent.intervalId
and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond;
@Priority(10)
on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent
delete from OpenActivityPoint as openEvent
where openEvent.driverKey = endEvent.driverKey
and openEvent.intervalId = endEvent.intervalId;
create schema RawVehicleUsageInterval(
sessionId java.util.UUID,
driverKey string,
intervalId string,
firstSourceIntervalId string,
lastSourceIntervalId string,
startedAt java.time.OffsetDateTime,
endedAt java.time.OffsetDateTime,
startedAtEpochSecond long,
endedAtEpochSecond java.lang.Long,
durationSeconds long,
odometerBeginKm java.lang.Long,
odometerEndKm java.lang.Long,
registrationKey string,
vehicleKey string,
sourceKind string,
sourceIntervalIds java.util.List
);
create window OpenVehicleUsagePoint#unique(driverKey, intervalId) as TachographVehicleUsagePointInputEvent;
insert into OpenVehicleUsagePoint
select *
from TachographVehicleUsagePointInputEvent(lifecycle = 'INSERT');
@Priority(20)
on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
insert into RawVehicleUsageInterval
select
insertEvent.sessionId as sessionId,
insertEvent.driverKey as driverKey,
insertEvent.intervalId as intervalId,
insertEvent.sourceRowId as firstSourceIntervalId,
withdrawEvent.sourceRowId as lastSourceIntervalId,
insertEvent.occurredAt as startedAt,
withdrawEvent.occurredAt as endedAt,
insertEvent.occurredAtEpochSecond as startedAtEpochSecond,
withdrawEvent.occurredAtEpochSecond as endedAtEpochSecond,
withdrawEvent.occurredAtEpochSecond - insertEvent.occurredAtEpochSecond as durationSeconds,
insertEvent.odometerKm as odometerBeginKm,
withdrawEvent.odometerKm as odometerEndKm,
insertEvent.registrationKey as registrationKey,
insertEvent.vehicleKey as vehicleKey,
insertEvent.sourceKind as sourceKind,
insertEvent.sourceRowIds as sourceIntervalIds
from OpenVehicleUsagePoint as insertEvent
where insertEvent.driverKey = withdrawEvent.driverKey
and insertEvent.intervalId = withdrawEvent.intervalId
and withdrawEvent.occurredAtEpochSecond > insertEvent.occurredAtEpochSecond;
@Priority(10)
on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
delete from OpenVehicleUsagePoint as openEvent
where openEvent.driverKey = withdrawEvent.driverKey
and openEvent.intervalId = withdrawEvent.intervalId;
create window MergedVehicleUsageAccumulator#unique(driverKey) as RawVehicleUsageInterval;
create window MergedVehicleUsageNext#unique(driverKey) as RawVehicleUsageInterval;
/*
* Case 1: first vehicle-usage interval for this driver. Start a new accumulator.
*/
@Priority(70)
on RawVehicleUsageInterval as next
insert into MergedVehicleUsageNext
select
next.sessionId as sessionId,
next.driverKey as driverKey,
next.intervalId as intervalId,
next.firstSourceIntervalId as firstSourceIntervalId,
next.lastSourceIntervalId as lastSourceIntervalId,
next.startedAt as startedAt,
next.endedAt as endedAt,
next.startedAtEpochSecond as startedAtEpochSecond,
next.endedAtEpochSecond as endedAtEpochSecond,
next.durationSeconds as durationSeconds,
next.odometerBeginKm as odometerBeginKm,
next.odometerEndKm as odometerEndKm,
next.registrationKey as registrationKey,
next.vehicleKey as vehicleKey,
next.sourceKind as sourceKind,
next.sourceIntervalIds as sourceIntervalIds
where not exists (
select * from MergedVehicleUsageAccumulator as current
where current.driverKey = next.driverKey
);
/*
* Case 2: same vehicle and registration, and the next interval starts at or before
* current.to + 1 second. This includes 23:59:59 -> 00:00:00. Keep one accumulated
* interval and extend its end/last-source/ending odometer.
*/
@Priority(70)
on RawVehicleUsageInterval as next
insert into MergedVehicleUsageNext
select
current.sessionId as sessionId,
current.driverKey as driverKey,
current.intervalId as intervalId,
current.firstSourceIntervalId as firstSourceIntervalId,
next.lastSourceIntervalId as lastSourceIntervalId,
current.startedAt as startedAt,
next.endedAt as endedAt,
current.startedAtEpochSecond as startedAtEpochSecond,
next.endedAtEpochSecond as endedAtEpochSecond,
next.endedAtEpochSecond - current.startedAtEpochSecond as durationSeconds,
current.odometerBeginKm as odometerBeginKm,
next.odometerEndKm as odometerEndKm,
current.registrationKey as registrationKey,
current.vehicleKey as vehicleKey,
current.sourceKind as sourceKind,
current.sourceIntervalIds as sourceIntervalIds
from MergedVehicleUsageAccumulator as current
where current.driverKey = next.driverKey
and current.endedAtEpochSecond is not null
and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L
and (
(current.registrationKey is null and next.registrationKey is null)
or (
current.registrationKey is not null
and next.registrationKey is not null
and current.registrationKey = next.registrationKey
)
)
and (
(current.vehicleKey is null and next.vehicleKey is null)
or (
current.vehicleKey is not null
and next.vehicleKey is not null
and current.vehicleKey = next.vehicleKey
)
);
/*
* Case 3a: next interval is not mergeable. Emit the accumulated interval to the old
* interval-based bundle.
*/
@Priority(70)
on RawVehicleUsageInterval as next
insert into TachographVehicleUsageIntervalInputEvent
select
current.sessionId as sessionId,
current.driverKey as driverKey,
current.intervalId as intervalId,
current.firstSourceIntervalId as firstSourceIntervalId,
current.lastSourceIntervalId as lastSourceIntervalId,
current.startedAt as startedAt,
current.endedAt as endedAt,
current.startedAtEpochSecond as startedAtEpochSecond,
current.endedAtEpochSecond as endedAtEpochSecond,
current.durationSeconds as durationSeconds,
current.odometerBeginKm as odometerBeginKm,
current.odometerEndKm as odometerEndKm,
current.registrationKey as registrationKey,
current.vehicleKey as vehicleKey,
current.sourceKind as sourceKind,
current.sourceIntervalIds as sourceIntervalIds
from MergedVehicleUsageAccumulator as current
where current.driverKey = next.driverKey
and not (
current.endedAtEpochSecond is not null
and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L
and (
(current.registrationKey is null and next.registrationKey is null)
or (
current.registrationKey is not null
and next.registrationKey is not null
and current.registrationKey = next.registrationKey
)
)
and (
(current.vehicleKey is null and next.vehicleKey is null)
or (
current.vehicleKey is not null
and next.vehicleKey is not null
and current.vehicleKey = next.vehicleKey
)
)
);
/*
* Case 3b: next interval is not mergeable. Start a new accumulator with it.
*/
@Priority(70)
on RawVehicleUsageInterval as next
insert into MergedVehicleUsageNext
select
next.sessionId as sessionId,
next.driverKey as driverKey,
next.intervalId as intervalId,
next.firstSourceIntervalId as firstSourceIntervalId,
next.lastSourceIntervalId as lastSourceIntervalId,
next.startedAt as startedAt,
next.endedAt as endedAt,
next.startedAtEpochSecond as startedAtEpochSecond,
next.endedAtEpochSecond as endedAtEpochSecond,
next.durationSeconds as durationSeconds,
next.odometerBeginKm as odometerBeginKm,
next.odometerEndKm as odometerEndKm,
next.registrationKey as registrationKey,
next.vehicleKey as vehicleKey,
next.sourceKind as sourceKind,
next.sourceIntervalIds as sourceIntervalIds
where exists (
select * from MergedVehicleUsageAccumulator as current
where current.driverKey = next.driverKey
and not (
current.endedAtEpochSecond is not null
and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L
and (
(current.registrationKey is null and next.registrationKey is null)
or (
current.registrationKey is not null
and next.registrationKey is not null
and current.registrationKey = next.registrationKey
)
)
and (
(current.vehicleKey is null and next.vehicleKey is null)
or (
current.vehicleKey is not null
and next.vehicleKey is not null
and current.vehicleKey = next.vehicleKey
)
)
)
);
@Priority(60)
on RawVehicleUsageInterval as next
delete from MergedVehicleUsageAccumulator as current
where current.driverKey = next.driverKey;
@Priority(50)
on RawVehicleUsageInterval as next
insert into MergedVehicleUsageAccumulator
select
candidate.sessionId as sessionId,
candidate.driverKey as driverKey,
candidate.intervalId as intervalId,
candidate.firstSourceIntervalId as firstSourceIntervalId,
candidate.lastSourceIntervalId as lastSourceIntervalId,
candidate.startedAt as startedAt,
candidate.endedAt as endedAt,
candidate.startedAtEpochSecond as startedAtEpochSecond,
candidate.endedAtEpochSecond as endedAtEpochSecond,
candidate.durationSeconds as durationSeconds,
candidate.odometerBeginKm as odometerBeginKm,
candidate.odometerEndKm as odometerEndKm,
candidate.registrationKey as registrationKey,
candidate.vehicleKey as vehicleKey,
candidate.sourceKind as sourceKind,
candidate.sourceIntervalIds as sourceIntervalIds
from MergedVehicleUsageNext as candidate
where candidate.driverKey = next.driverKey;
@Priority(40)
on RawVehicleUsageInterval as next
delete from MergedVehicleUsageNext as candidate
where candidate.driverKey = next.driverKey;
/*
* The last accumulated interval cannot be emitted by looking at the next interval, so Java
* sends one TachographProjectionFinalizeEvent per driver after all vehicle-usage point
* events and before activity point events.
*/
@Priority(20)
on TachographProjectionFinalizeEvent as finalizeEvent
insert into TachographVehicleUsageIntervalInputEvent
select
current.sessionId as sessionId,
current.driverKey as driverKey,
current.intervalId as intervalId,
current.firstSourceIntervalId as firstSourceIntervalId,
current.lastSourceIntervalId as lastSourceIntervalId,
current.startedAt as startedAt,
current.endedAt as endedAt,
current.startedAtEpochSecond as startedAtEpochSecond,
current.endedAtEpochSecond as endedAtEpochSecond,
current.durationSeconds as durationSeconds,
current.odometerBeginKm as odometerBeginKm,
current.odometerEndKm as odometerEndKm,
current.registrationKey as registrationKey,
current.vehicleKey as vehicleKey,
current.sourceKind as sourceKind,
current.sourceIntervalIds as sourceIntervalIds
from MergedVehicleUsageAccumulator as current
where current.driverKey = finalizeEvent.driverKey;
@Priority(10)
on TachographProjectionFinalizeEvent as finalizeEvent
delete from MergedVehicleUsageAccumulator as current
where current.driverKey = finalizeEvent.driverKey;

View File

@ -0,0 +1,64 @@
/*
* Source-neutral EPL module: DriverVehicleUsagePointEvent -> DriverVehicleUsageIntervalEvent.
* Pairs INSERT/WITHDRAW point events for the same driver and interval id.
*/
create schema DriverVehicleUsageIntervalEvent(
sessionId java.util.UUID,
driverKey string,
intervalId string,
firstSourceIntervalId string,
lastSourceIntervalId string,
startedAt java.time.OffsetDateTime,
endedAt java.time.OffsetDateTime,
startedAtEpochSecond long,
endedAtEpochSecond java.lang.Long,
durationSeconds long,
odometerBeginKm java.lang.Long,
odometerEndKm java.lang.Long,
registrationKey string,
vehicleKey string,
sourceKind string,
sourceIntervalIds java.util.List
);
create window OpenDriverVehicleUsagePoint#unique(driverKey, intervalId) as DriverVehicleUsagePointEvent;
insert into OpenDriverVehicleUsagePoint
select *
from DriverVehicleUsagePointEvent(lifecycle = 'INSERT');
@Priority(20)
on DriverVehicleUsagePointEvent(lifecycle = 'WITHDRAW') as withdrawEvent
insert into DriverVehicleUsageIntervalEvent
select
insertEvent.sessionId as sessionId,
insertEvent.driverKey as driverKey,
insertEvent.intervalId as intervalId,
insertEvent.sourceRowId as firstSourceIntervalId,
withdrawEvent.sourceRowId as lastSourceIntervalId,
insertEvent.occurredAt as startedAt,
withdrawEvent.occurredAt as endedAt,
insertEvent.occurredAtEpochSecond as startedAtEpochSecond,
withdrawEvent.occurredAtEpochSecond as endedAtEpochSecond,
withdrawEvent.occurredAtEpochSecond - insertEvent.occurredAtEpochSecond as durationSeconds,
insertEvent.odometerKm as odometerBeginKm,
withdrawEvent.odometerKm as odometerEndKm,
insertEvent.registrationKey as registrationKey,
insertEvent.vehicleKey as vehicleKey,
insertEvent.sourceKind as sourceKind,
insertEvent.sourceRowIds as sourceIntervalIds
from OpenDriverVehicleUsagePoint as insertEvent
where insertEvent.driverKey = withdrawEvent.driverKey
and insertEvent.intervalId = withdrawEvent.intervalId
and withdrawEvent.occurredAtEpochSecond >= insertEvent.occurredAtEpochSecond;
@Priority(10)
on DriverVehicleUsagePointEvent(lifecycle = 'WITHDRAW') as withdrawEvent
delete from OpenDriverVehicleUsagePoint as openEvent
where openEvent.driverKey = withdrawEvent.driverKey
and openEvent.intervalId = withdrawEvent.intervalId;
@name('driverVehicleUsageIntervals')
select *
from DriverVehicleUsageIntervalEvent;

View File

@ -244,7 +244,7 @@ class UnifiedRuntimeProcessingControllerTest {
1, 1,
3, 3,
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")), List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
projection, projection.toDriverWorkingTime(),
List.of("runtime derived") List.of("runtime derived")
)); ));
@ -296,7 +296,7 @@ class UnifiedRuntimeProcessingControllerTest {
.thenReturn(List.of(new RuntimeProcessingPlanDescriptorDto( .thenReturn(List.of(new RuntimeProcessingPlanDescriptorDto(
"driver-working-time-v1", "driver-working-time-v1",
Set.of("tachograph-driver-esper-v1"), Set.of("tachograph-driver-esper-v1"),
"Driver working-time and tachograph-derived processing", "Driver working-time processing",
"Runs common runtime event processing modules.", "Runs common runtime event processing modules.",
RuntimeEventPartitioningStrategy.DRIVER, RuntimeEventPartitioningStrategy.DRIVER,
List.of(RuntimeEventPartitioningStrategy.DRIVER), List.of(RuntimeEventPartitioningStrategy.DRIVER),
@ -576,9 +576,7 @@ class UnifiedRuntimeProcessingControllerTest {
derivedProjectionService, derivedProjectionService,
null, null,
runtimeEventProcessingService, runtimeEventProcessingService,
parityValidationService, parityValidationService
null,
null
)) ))
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())

View File

@ -0,0 +1,60 @@
package at.procon.eventhub.processing.eventprocessing.plan;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class RuntimeProcessingPlanRegistryTest {
@Test
void resolvesPlansByPrimaryKeyAndAlias() {
RuntimeProcessingPlanRegistry registry = new RuntimeProcessingPlanRegistry(List.of(new TestPlan("plan-a", Set.of("legacy-a"))));
assertThat(registry.require("plan-a").processingPlanKey()).isEqualTo("plan-a");
assertThat(registry.require("legacy-a").processingPlanKey()).isEqualTo("plan-a");
assertThat(registry.planDescriptors()).hasSize(1);
assertThat(registry.planDescriptors().get(0).aliases()).contains("legacy-a");
}
@Test
void rejectsDuplicatePlanKeys() {
assertThatThrownBy(() -> new RuntimeProcessingPlanRegistry(List.of(
new TestPlan("plan-a", Set.of()),
new TestPlan("plan-a", Set.of())
)))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Duplicate runtime processingPlanKey");
}
@Test
void rejectsDuplicateAliasesPointingToDifferentPlans() {
assertThatThrownBy(() -> new RuntimeProcessingPlanRegistry(List.of(
new TestPlan("plan-a", Set.of("alias")),
new TestPlan("plan-b", Set.of("alias"))
)))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Duplicate runtime processing plan alias/key");
}
private record TestPlan(String key, Set<String> aliases) implements RuntimeProcessingPlan {
@Override
public String processingPlanKey() {
return key;
}
@Override
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
return RuntimeEventPartitioningStrategy.DRIVER;
}
@Override
public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) {
throw new UnsupportedOperationException("not needed");
}
}
}

View File

@ -8,13 +8,13 @@ import static org.mockito.Mockito.when;
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService; import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -28,7 +28,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
@Test @Test
void exposesDiscoveryMetadata() { void exposesDiscoveryMetadata() {
TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile( TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(
org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class) org.mockito.Mockito.mock(RuntimeDriverWorkingTimeScopeProcessingService.class)
); );
assertThat(profile.profileKey()).isEqualTo("tachograph-driver-esper-v1"); assertThat(profile.profileKey()).isEqualTo("tachograph-driver-esper-v1");
@ -46,7 +46,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
@Test @Test
void delegatesToTachographScopeServiceAndMapsPartitionResults() { void delegatesToTachographScopeServiceAndMapsPartitionResults() {
UnifiedRuntimeTachographEsperScopeProcessingService scopeService = org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class); RuntimeDriverWorkingTimeScopeProcessingService scopeService = org.mockito.Mockito.mock(RuntimeDriverWorkingTimeScopeProcessingService.class);
TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(scopeService); TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(scopeService);
UUID sessionId = UUID.randomUUID(); UUID sessionId = UUID.randomUUID();
@ -127,7 +127,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
List.of("driver processed") List.of("driver processed")
); );
when(scopeService.processScope(any(), anyBoolean())) when(scopeService.processScope(any(), anyBoolean()))
.thenReturn(new UnifiedRuntimeTachographEsperScopeResultDto( .thenReturn(new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
processedRequest, processedRequest,
5, 5,
1, 1,

View File

@ -0,0 +1,135 @@
package at.procon.eventhub.processing.eventprocessing.validation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import at.procon.eventhub.processing.dto.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 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.processing.dto.UnifiedRuntimeProcessingApiRequest;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
class RuntimeMixedSourceEvidenceValidationServiceTest {
@Test
void validatesAttachedAndNormalizedEvidenceCounts() {
RuntimeEventProcessingService processingService = Mockito.mock(RuntimeEventProcessingService.class);
RuntimeMixedSourceEvidenceValidationService service = new RuntimeMixedSourceEvidenceValidationService(processingService);
UnifiedRuntimeDerivedProjectionResultDto driverResult = new UnifiedRuntimeDerivedProjectionResultDto(
null,
2,
1,
1,
3,
List.of(),
null,
List.of("projection note"),
new RuntimeSupportEvidenceNormalizationDebugDto(3, 1, 2, List.of("normalization note")),
new RuntimeDriverPartitionDebugDto(
"DRIVER:1",
2,
1,
1,
1,
0,
3,
List.of(),
List.of(),
List.of("debug note"),
List.of()
)
);
when(processingService.process(any())).thenReturn(new RuntimeEventProcessingResultDto(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
RuntimeEventPartitioningStrategy.DRIVER,
null,
3,
1,
1,
List.of(),
Map.of("DRIVER:1", new RuntimeEventProcessingPartitionResultDto(
"DRIVER",
"DRIVER:1",
"UnifiedRuntimeDerivedProjectionResultDto",
driverResult,
Map.of()
)),
List.of("runtime note"),
List.of()
));
RuntimeMixedSourceEvidenceValidationResultDto result = service.validate(new RuntimeMixedSourceEvidenceValidationApiRequest(
new RuntimeEventProcessingApiRequest(
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
new UnifiedRuntimeProcessingApiRequest(
null,
List.of(),
null,
"default",
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
UnifiedRuntimeEventBackend.SOURCE_DB,
"DRIVER:1",
Set.of(),
false,
Set.of(),
false,
null,
null,
null,
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
true,
15,
3,
720
),
new RuntimeEventPartitioningApiRequest(
RuntimeEventPartitioningStrategy.DRIVER,
null,
null,
Set.of("DRIVER:1"),
null,
null,
null,
true,
15,
false
),
Map.of()
),
Set.of("DRIVER:1"),
1,
1,
true
));
assertThat(result.status()).isEqualTo("PASSED");
assertThat(result.totalAttachedVehicleEvidenceEventCount()).isEqualTo(1);
assertThat(result.totalNormalizedSupportEvidenceEventCount()).isEqualTo(1);
assertThat(result.partitions().get("DRIVER:1").partitionDebugPresent()).isTrue();
assertThat(result.partitions().get("DRIVER:1").normalizedSupportEvidenceEventCount()).isEqualTo(1);
ArgumentCaptor<RuntimeEventProcessingApiRequest> captor = ArgumentCaptor.forClass(RuntimeEventProcessingApiRequest.class);
verify(processingService).process(captor.capture());
assertThat(captor.getValue().partitioning().includeDebugOrDefault()).isTrue();
assertThat(captor.getValue().parameters()).containsEntry("includePartitionDebug", true);
}
}

View File

@ -65,7 +65,7 @@ class RuntimeTachographParityValidationServiceTest {
0, 0,
4, 4,
List.of(), List.of(),
projection(sessionId, driverKey, 2, 1, 1), projection(sessionId, driverKey, 2, 1, 1).toDriverWorkingTime(),
List.of() List.of()
); );
when(runtimeEventProcessingService.process(any())) when(runtimeEventProcessingService.process(any()))