Compare commits
8 Commits
dd9c33c5fd
...
82e2bd0860
| Author | SHA1 | Date |
|---|---|---|
|
|
82e2bd0860 | |
|
|
5c887e8cb2 | |
|
|
27b411e647 | |
|
|
471726c4cc | |
|
|
e68047feab | |
|
|
b04b333db7 | |
|
|
3bccda20e8 | |
|
|
8a75db58fd |
|
|
@ -0,0 +1,17 @@
|
|||
# EventHub fix-list patch
|
||||
|
||||
This patch implements the requested remaining items from the fix list, excluding build verification and SQL Server 2008 SQL rewriting.
|
||||
|
||||
## Apply notes
|
||||
|
||||
1. Copy the files from this archive into the project root.
|
||||
2. Delete the old migration files listed in `DELETE_FILES.txt`.
|
||||
3. Run the test suite locally with Maven/Java 21.
|
||||
|
||||
## Main changes
|
||||
|
||||
- Renumbered Flyway migrations to remove duplicate `V9` and `V10` versions.
|
||||
- Removed the duplicated Timescale/Event source record migration.
|
||||
- Switched local Docker Compose DB from plain PostgreSQL to a TimescaleDB/PostGIS-capable image.
|
||||
- Added normalized raw tachograph payload metadata for DB-extracted EventHub events.
|
||||
- Added tests for Flyway version uniqueness and tachograph DB mapper → timeline reconstruction metadata.
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
# The Flyway migrations enable both TimescaleDB and PostGIS. The TimescaleDB HA
|
||||
# image is used for local development because it includes these PostgreSQL
|
||||
# extensions, unlike the plain postgres image.
|
||||
image: timescale/timescaledb-ha:pg16
|
||||
container_name: eventhub-postgres
|
||||
environment:
|
||||
POSTGRES_DB: eventhub
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
# Runtime-derived tachograph projections
|
||||
|
||||
Runtime Processing now exposes the tachograph driving-derived Esper bundle through the unified runtime event assembly layer.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/driver-derived-projections
|
||||
```
|
||||
|
||||
The request body uses the same selector fields as the existing runtime endpoints:
|
||||
|
||||
- `sessionId` for one uploaded tachograph file session
|
||||
- `sessionIds` for multiple uploaded tachograph file sessions
|
||||
- `compositeSessionId` for an existing tachograph composite session
|
||||
- `tenantKey` + driver selector for tachograph DB / YellowFox DB runtime sources
|
||||
- `eventBackend` with `SOURCE_DB` or `EVENTHUB_DB` where supported
|
||||
- `sourceFamilies`, for example `TACHOGRAPH_FILE_SESSION`, `TACHOGRAPH_DB`, `YELLOWFOX_DB`
|
||||
- `driverKey`, `driverSourceEntityId`, `driverCardNation`, `driverCardNumber`
|
||||
- `occurredFrom`, `occurredTo`
|
||||
- `expandVehicleEvents`
|
||||
- `vehicleExpansionPaddingMinutes`
|
||||
|
||||
Additional Esper thresholds are optional:
|
||||
|
||||
- `significantDrivingMinutes`
|
||||
- `minimumRestPeriodMinutes`
|
||||
|
||||
When omitted, the defaults are read from:
|
||||
|
||||
```yaml
|
||||
eventhub:
|
||||
tachograph-file-session:
|
||||
processing:
|
||||
significant-driving-minutes: 3
|
||||
minimum-rest-period-minutes: 720
|
||||
```
|
||||
|
||||
## Flow
|
||||
|
||||
```text
|
||||
runtime request
|
||||
-> UnifiedRuntimeEventAssemblyService
|
||||
-> driver seed events
|
||||
-> discovered vehicles
|
||||
-> optional vehicle-expanded events
|
||||
-> merged runtime event stream
|
||||
-> UnifiedEventTimelineReconstructor
|
||||
-> DriverTimelineReusableProjectionBuilder.buildEsperDrivingDerivedProjectionBundleFromEvents(...)
|
||||
-> TachographEsperDriverProcessingResultDto
|
||||
```
|
||||
|
||||
The derived part always uses the event-input Esper path. This means the runtime stream is passed as point events to Esper, where activity and card-vehicle-usage intervals are paired and vehicle-usage intervals are merged before the existing driving-derived rules run.
|
||||
|
||||
## Example: composite tachograph file session
|
||||
|
||||
```json
|
||||
{
|
||||
"compositeSessionId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"driverKey": "12:12345678901234",
|
||||
"occurredFrom": "2026-05-01T00:00:00Z",
|
||||
"occurredTo": "2026-05-31T23:59:59Z",
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15,
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
The response contains runtime assembly metadata and the tachograph Esper result:
|
||||
|
||||
```text
|
||||
request
|
||||
driverSeedEventCount
|
||||
discoveredVehicleCount
|
||||
expandedVehicleEventCount
|
||||
mergedEventCount
|
||||
discoveredVehicles
|
||||
projection
|
||||
notes
|
||||
```
|
||||
|
||||
`projection` is the same high-level structure returned by the tachograph file-session Esper endpoint, including:
|
||||
|
||||
- activity interval count/list
|
||||
- driving interval count/list
|
||||
- driving interruption intervals
|
||||
- daily/weekly rest candidate intervals
|
||||
- daily/weekly rest candidate coverage intervals
|
||||
- unclassified rest candidate coverage intervals
|
||||
- potential home overnight stays
|
||||
- potential in-vehicle overnight stays
|
||||
- potential in-vehicle trips
|
||||
- vehicle usage intervals
|
||||
- VU card absent intervals
|
||||
- support geo events
|
||||
|
||||
## Boundary note
|
||||
|
||||
Runtime processing works with point events. If `occurredFrom`/`occurredTo` cuts through a source interval, the matching START/END or INSERT/WITHDRAW point may be outside the selected window. For evaluations near boundaries, request a wider event window or use vehicle expansion padding so Esper receives enough point events to reconstruct the interval.
|
||||
|
|
@ -0,0 +1,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.
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
# Runtime event processing
|
||||
|
||||
Runtime processing is now centered on **processing executions**. An execution loads canonical EventHub runtime events from selected sources, optionally partitions the event set, then runs a configured processing plan made of common EPL and/or Java modules.
|
||||
|
||||
The preferred endpoint is:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/executions
|
||||
```
|
||||
|
||||
List available plans:
|
||||
|
||||
```http
|
||||
GET /api/eventhub/runtime-processing/executions/plans
|
||||
```
|
||||
|
||||
The older endpoint is still available for compatibility:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
but it should be treated as a legacy profile adapter. New clients should use `processingPlanKey`, not `profileKey`.
|
||||
|
||||
## First predefined plan
|
||||
|
||||
The first predefined plan is:
|
||||
|
||||
```text
|
||||
driver-working-time-v1
|
||||
```
|
||||
|
||||
Legacy alias:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
This plan runs the source-neutral driver working-time chain over canonical runtime events. Tachograph file/database data is only one possible source of those canonical events. Its modules include:
|
||||
|
||||
```text
|
||||
event-to-activity-intervals
|
||||
event-to-vehicle-usage-intervals
|
||||
vehicle-evidence-attachment
|
||||
support-evidence-normalization
|
||||
driving-derived-projections
|
||||
```
|
||||
|
||||
The important design point is that runtime processing is not tachograph-specific. Tachograph is now treated as a source/compatibility layer; the working-time plan consumes canonical driver activity, vehicle usage, and support-evidence events from any supported source.
|
||||
|
||||
## Example execution
|
||||
|
||||
```json
|
||||
{
|
||||
"processingPlanKey": "driver-working-time-v1",
|
||||
"sourceSelection": {
|
||||
"sessionIds": [
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
"22222222-2222-2222-2222-222222222222"
|
||||
],
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION", "YELLOWFOX_DB"],
|
||||
"occurredFrom": "2026-05-01T00:00:00Z",
|
||||
"occurredTo": "2026-05-31T23:59:59Z",
|
||||
"expandVehicleEvents": true
|
||||
},
|
||||
"partitioning": {
|
||||
"strategy": "DRIVER",
|
||||
"includeAllPartitions": true,
|
||||
"attachVehicleEvidence": true,
|
||||
"vehicleEvidencePaddingMinutes": 15,
|
||||
"includeDebug": true
|
||||
},
|
||||
"parameters": {
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720,
|
||||
"includePartitionDebug": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conceptual flow
|
||||
|
||||
```text
|
||||
source selection
|
||||
-> runtime event loaders
|
||||
-> canonical EventHub events
|
||||
-> partitioning
|
||||
-> vehicle evidence attachment
|
||||
-> support evidence normalization
|
||||
-> event-to-interval EPL modules
|
||||
-> driver working-time derived projection EPL/Java modules
|
||||
-> partition result map
|
||||
```
|
||||
|
||||
## Why processing plans instead of profiles
|
||||
|
||||
A plan describes **which modules should run over a runtime event set**. This matches the EventHub goal better than domain-oriented profiles.
|
||||
|
||||
Future plans can reuse the same event-loading infrastructure:
|
||||
|
||||
```text
|
||||
vehicle-trip-detection-v1
|
||||
vehicle-stop-detection-v1
|
||||
driver-settlement-v1
|
||||
mixed-driver-vehicle-correlation-v1
|
||||
telematics-poi-clustering-v1
|
||||
```
|
||||
|
||||
Each plan may use different partitioning and modules, but the source loading and canonical event model remain common.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Legacy profile endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"scope": {},
|
||||
"partitioning": {},
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
internally delegates to:
|
||||
|
||||
```text
|
||||
processingPlanKey = driver-working-time-v1
|
||||
```
|
||||
|
||||
## Source-neutral driver working-time modules
|
||||
|
||||
The former tachograph-named processing artifacts are now represented by source-neutral driver working-time names:
|
||||
|
||||
```text
|
||||
runtime-driver-event-interval-preprocessor.epl
|
||||
driver-working-time-derived-projections.epl
|
||||
DriverWorkingTimeProcessingResultDto
|
||||
DriverWorkingTimeProcessingCore
|
||||
RuntimeDriverWorkingTimeScopeProcessingService
|
||||
```
|
||||
|
||||
Legacy tachograph names are kept as compatibility adapters for existing file-session endpoints and Postman requests. New runtime code should use the `driver-working-time-*` classes/resources and the `driver-working-time-v1` processing plan.
|
||||
|
||||
## Module execution results
|
||||
|
||||
`/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly.
|
||||
|
||||
Top-level execution response includes:
|
||||
|
||||
```text
|
||||
moduleResults
|
||||
```
|
||||
|
||||
This map contains one entry per executed module. The top-level entries are intentionally sanitized: they expose status, warnings, and metadata without duplicating the full partition output payload.
|
||||
|
||||
Each partition result also includes:
|
||||
|
||||
```text
|
||||
partitionResults[*].moduleResults
|
||||
```
|
||||
|
||||
For `driver-working-time-v1`, the partition-level module results currently expose:
|
||||
|
||||
```text
|
||||
vehicle-evidence-attachment
|
||||
support-evidence-normalization
|
||||
driving-derived-projections
|
||||
```
|
||||
|
||||
The first implementation uses wrapper modules around the existing validated driver-working-time scope service. Logical phase modules such as `event-to-activity-intervals`, `event-to-vehicle-usage-intervals`, and `vehicle-usage-merge` are registered and executed as delegated modules so they can later be split into true standalone EPL/Java modules without changing the external processing-plan contract.
|
||||
|
||||
## First-class EPL modules
|
||||
|
||||
Runtime processing now supports modules implemented directly with Esper EPL through the common `RuntimeEplModuleExecutor`.
|
||||
|
||||
The first source-neutral EPL modules are:
|
||||
|
||||
| Module key | Engine | EPL resource | Output statement |
|
||||
|---|---|---|---|
|
||||
| `event-to-activity-intervals` | `EPL` | `esper/runtime-driver-activity-intervals.epl` | `driverActivityIntervals` |
|
||||
| `event-to-vehicle-usage-intervals` | `EPL` | `esper/runtime-driver-vehicle-usage-intervals.epl` | `driverVehicleUsageIntervals` |
|
||||
|
||||
These modules operate on canonical EventHub runtime events, not on tachograph-specific source rows. They are currently used as first-class phase modules in `driver-working-time-v1`; the final `driving-derived-projections` module remains a compatibility adapter over the validated working-time projection service until the remaining projection stages are split into direct EPL modules.
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# Runtime tachograph Esper scope processing
|
||||
|
||||
This document is kept for compatibility. The preferred architecture is now the common Runtime Processing execution model.
|
||||
|
||||
Preferred endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/executions
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```json
|
||||
{
|
||||
"processingPlanKey": "driver-working-time-v1"
|
||||
}
|
||||
```
|
||||
|
||||
Legacy compatibility endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/tachograph/esper-processing
|
||||
```
|
||||
|
||||
still exists and delegates through the common runtime processing infrastructure.
|
||||
|
||||
Legacy profile endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1"
|
||||
}
|
||||
```
|
||||
|
||||
also remains available, but new clients should use `processingPlanKey` and `/executions`.
|
||||
|
||||
The current `driver-working-time-v1` plan uses these modules:
|
||||
|
||||
```text
|
||||
event-to-activity-intervals
|
||||
event-to-vehicle-usage-intervals
|
||||
vehicle-evidence-attachment
|
||||
support-evidence-normalization
|
||||
driving-derived-projections
|
||||
```
|
||||
|
||||
It can load runtime events from multiple sources and sessions, partition them by driver, attach vehicle-only evidence by vehicle/time overlap, normalize support evidence, and run the existing tachograph Esper/Java processing chain per driver partition.
|
||||
|
|
@ -0,0 +1,552 @@
|
|||
{
|
||||
"info": {
|
||||
"name": "EventHub Runtime Event Processing",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": "Examples for source-neutral Runtime Event Processing and the tachograph-driver-esper-v1 profile. Includes plan execution, compatibility profile execution, validation endpoints, and legacy runtime diagnostic endpoints (driver-events, driver-timeline, driver-derived-projections)."
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://localhost:8085"
|
||||
},
|
||||
{
|
||||
"key": "sessionId",
|
||||
"value": "11111111-1111-1111-1111-111111111111"
|
||||
},
|
||||
{
|
||||
"key": "sessionId2",
|
||||
"value": "22222222-2222-2222-2222-222222222222"
|
||||
},
|
||||
{
|
||||
"key": "compositeSessionId",
|
||||
"value": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
},
|
||||
{
|
||||
"key": "driverKey",
|
||||
"value": "12:12345678901234"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "List runtime processing plans",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions/plans",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"executions",
|
||||
"plans"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute processing plan - driver working time single session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"executions"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": false,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute processing plan - driver working time multiple sessions all drivers",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"executions"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute processing plan - driver working time composite session all drivers",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"executions"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute processing plan - driver working time source DB mixed sources",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"executions"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{driverSourceEntityId}}\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": false,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "List runtime event-processing profiles",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/profiles",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing",
|
||||
"profiles"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute tachograph profile - single session and one driver",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"partitionKeys\": [\n \"{{driverKey}}\"\n ],\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute tachograph profile - multiple sessions all drivers",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute tachograph profile - composite session all drivers",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"attachVehicleOnlyEvents\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includePartitionDebug\": true\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Compatibility endpoint - tachograph esper processing",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"includeAllDrivers\": true,\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/tachograph/esper-processing",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"tachograph",
|
||||
"esper-processing"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Validate tachograph profile parity - single session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/validation/tachograph-parity",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing",
|
||||
"validation",
|
||||
"tachograph-parity"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includeDebug\": true\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Validate mixed-source vehicle evidence attachment and normalization",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"processingRequest\": {\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId1}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n },\n \"minimumAttachedVehicleEvidenceEvents\": 1,\n \"minimumNormalizedSupportEvidenceEvents\": 1,\n \"failWhenPartitionDebugMissing\": true\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/validation/mixed-source-evidence",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"event-processing",
|
||||
"validation",
|
||||
"mixed-source-evidence"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver events from single file session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-events"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver timeline from single file session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-timeline"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver derived projections from single file session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-derived-projections",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-derived-projections"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver events from multiple file sessions",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-events"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver timeline from composite session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-timeline"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver events from source DB",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-events"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{driverSourceEntityId}}\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Runtime diagnostics - driver timeline from EventHub DB",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"runtime-processing",
|
||||
"driver-timeline"
|
||||
]
|
||||
},
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -357,6 +357,7 @@ public class EventHubProperties {
|
|||
|
||||
public static class Processing {
|
||||
private TimelineInputMode timelineInputMode = TimelineInputMode.INTERVALS;
|
||||
private DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode = DrivingDerivedProjectionInputMode.INTERVALS;
|
||||
private int operatingSplitIdleHours = 7;
|
||||
private int significantDrivingMinutes = 3;
|
||||
private int minimumRestPeriodMinutes = 720;
|
||||
|
|
@ -377,6 +378,16 @@ public class EventHubProperties {
|
|||
}
|
||||
}
|
||||
|
||||
public DrivingDerivedProjectionInputMode getDrivingDerivedProjectionInputMode() {
|
||||
return drivingDerivedProjectionInputMode;
|
||||
}
|
||||
|
||||
public void setDrivingDerivedProjectionInputMode(DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode) {
|
||||
if (drivingDerivedProjectionInputMode != null) {
|
||||
this.drivingDerivedProjectionInputMode = drivingDerivedProjectionInputMode;
|
||||
}
|
||||
}
|
||||
|
||||
public int getOperatingSplitIdleHours() {
|
||||
return operatingSplitIdleHours;
|
||||
}
|
||||
|
|
@ -456,6 +467,14 @@ public class EventHubProperties {
|
|||
EVENTS
|
||||
}
|
||||
|
||||
public enum DrivingDerivedProjectionInputMode {
|
||||
/** Existing stable path: Java resolves intervals and EPL receives interval input streams. */
|
||||
INTERVALS,
|
||||
|
||||
/** New path: EPL receives EventHub point events and reconstructs interval streams internally. */
|
||||
EVENTS
|
||||
}
|
||||
|
||||
public static class LegalRequirements {
|
||||
private static final String DEFAULT_BASE_URL = "https://legalrequirements.services.bytebar.eu/ODataV4/LR";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package at.procon.eventhub.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcJsonConfig implements WebMvcConfigurer {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public WebMvcJsonConfig(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
converters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance);
|
||||
converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,36 @@
|
|||
package at.procon.eventhub.processing.api;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionService;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingPlanDescriptorDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationService;
|
||||
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
|
||||
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/eventhub/runtime-processing")
|
||||
|
|
@ -17,13 +38,64 @@ public class UnifiedRuntimeProcessingController {
|
|||
|
||||
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
|
||||
private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService;
|
||||
private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService;
|
||||
private final RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService;
|
||||
private final RuntimeEventProcessingService runtimeEventProcessingService;
|
||||
private final RuntimeProcessingExecutionService runtimeProcessingExecutionService;
|
||||
private final RuntimeTachographParityValidationService tachographParityValidationService;
|
||||
private final RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService;
|
||||
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService
|
||||
) {
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
|
||||
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService
|
||||
) {
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
|
||||
tachographEsperScopeProcessingService, runtimeEventProcessingService, null, null, null);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
|
||||
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService,
|
||||
RuntimeTachographParityValidationService tachographParityValidationService,
|
||||
RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService,
|
||||
RuntimeProcessingExecutionService runtimeProcessingExecutionService
|
||||
) {
|
||||
this.eventAssemblyService = eventAssemblyService;
|
||||
this.runtimeDriverTimelineService = runtimeDriverTimelineService;
|
||||
this.runtimeDerivedProjectionService = runtimeDerivedProjectionService;
|
||||
this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService;
|
||||
this.runtimeEventProcessingService = runtimeEventProcessingService;
|
||||
this.runtimeProcessingExecutionService = runtimeProcessingExecutionService;
|
||||
this.tachographParityValidationService = tachographParityValidationService;
|
||||
this.mixedSourceEvidenceValidationService = mixedSourceEvidenceValidationService;
|
||||
}
|
||||
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
|
||||
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService,
|
||||
RuntimeTachographParityValidationService tachographParityValidationService
|
||||
) {
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
|
||||
tachographEsperScopeProcessingService, runtimeEventProcessingService,
|
||||
tachographParityValidationService, null, null);
|
||||
}
|
||||
|
||||
@PostMapping("/driver-events")
|
||||
|
|
@ -39,4 +111,88 @@ public class UnifiedRuntimeProcessingController {
|
|||
) {
|
||||
return ResponseEntity.ok(runtimeDriverTimelineService.loadDriverTimeline(request.toRuntimeRequest()));
|
||||
}
|
||||
|
||||
@PostMapping("/driver-derived-projections")
|
||||
public ResponseEntity<UnifiedRuntimeDerivedProjectionResultDto> loadDriverDerivedProjections(
|
||||
@RequestBody UnifiedRuntimeProcessingApiRequest request
|
||||
) {
|
||||
return ResponseEntity.ok(runtimeDerivedProjectionService.loadDriverDerivedProjections(request));
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/executions/plans")
|
||||
public ResponseEntity<List<RuntimeProcessingPlanDescriptorDto>> listRuntimeProcessingPlans() {
|
||||
if (runtimeProcessingExecutionService == null) {
|
||||
throw new IllegalStateException("Runtime processing execution service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(runtimeProcessingExecutionService.listPlans());
|
||||
}
|
||||
|
||||
@PostMapping("/executions")
|
||||
public ResponseEntity<RuntimeProcessingExecutionResultDto> runRuntimeProcessingExecution(
|
||||
@RequestBody RuntimeProcessingExecutionApiRequest request
|
||||
) {
|
||||
if (runtimeProcessingExecutionService == null) {
|
||||
throw new IllegalStateException("Runtime processing execution service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(runtimeProcessingExecutionService.execute(request));
|
||||
}
|
||||
|
||||
@GetMapping("/event-processing/profiles")
|
||||
public ResponseEntity<List<RuntimeEventProcessingProfileDescriptorDto>> listEventProcessingProfiles() {
|
||||
if (runtimeEventProcessingService == null) {
|
||||
throw new IllegalStateException("Runtime event processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(runtimeEventProcessingService.listProfiles());
|
||||
}
|
||||
|
||||
@PostMapping("/event-processing")
|
||||
public ResponseEntity<RuntimeEventProcessingResultDto> runEventProcessing(
|
||||
@RequestBody RuntimeEventProcessingApiRequest request
|
||||
) {
|
||||
if (runtimeEventProcessingService == null) {
|
||||
throw new IllegalStateException("Runtime event processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(runtimeEventProcessingService.process(request));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/event-processing/validation/tachograph-parity")
|
||||
public ResponseEntity<RuntimeTachographParityValidationResultDto> validateTachographParity(
|
||||
@RequestBody RuntimeTachographParityValidationApiRequest request
|
||||
) {
|
||||
if (tachographParityValidationService == null) {
|
||||
throw new IllegalStateException("Runtime tachograph parity validation service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(tachographParityValidationService.validate(request));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/event-processing/validation/mixed-source-evidence")
|
||||
public ResponseEntity<RuntimeMixedSourceEvidenceValidationResultDto> validateMixedSourceEvidence(
|
||||
@RequestBody RuntimeMixedSourceEvidenceValidationApiRequest request
|
||||
) {
|
||||
if (mixedSourceEvidenceValidationService == null) {
|
||||
throw new IllegalStateException("Runtime mixed-source evidence validation service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(mixedSourceEvidenceValidationService.validate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/tachograph/esper-processing")
|
||||
public ResponseEntity<UnifiedRuntimeTachographEsperScopeResultDto> runTachographEsperProcessing(
|
||||
@RequestBody UnifiedRuntimeProcessingApiRequest request
|
||||
) {
|
||||
if (runtimeEventProcessingService != null) {
|
||||
RuntimeEventProcessingResultDto genericResult = runtimeEventProcessingService.process(
|
||||
RuntimeEventProcessingApiRequest.tachographDriverEsper(request)
|
||||
);
|
||||
return ResponseEntity.ok(UnifiedRuntimeTachographEsperScopeResultDto.fromGenericRuntimeEventProcessingResult(genericResult));
|
||||
}
|
||||
if (tachographEsperScopeProcessingService == null) {
|
||||
throw new IllegalStateException("Tachograph Esper scope processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(UnifiedRuntimeTachographEsperScopeResultDto.fromDriverWorkingTime(
|
||||
tachographEsperScopeProcessingService.processScope(request)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package at.procon.eventhub.processing.api;
|
||||
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInCompositeSessionException;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
|
|
@ -14,7 +16,9 @@ public class UnifiedRuntimeProcessingExceptionHandler {
|
|||
|
||||
@ExceptionHandler({
|
||||
TachographFileSessionNotFoundException.class,
|
||||
DriverNotFoundInSessionException.class
|
||||
TachographCompositeSessionNotFoundException.class,
|
||||
DriverNotFoundInSessionException.class,
|
||||
DriverNotFoundInCompositeSessionException.class
|
||||
})
|
||||
public ResponseEntity<Map<String, Object>> notFound(RuntimeException exception) {
|
||||
return error(HttpStatus.NOT_FOUND, exception);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,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
|
||||
) {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RuntimeDriverPartitionDebugDto(
|
||||
String driverKey,
|
||||
int directDriverEventCount,
|
||||
int vehicleUsageIntervalCount,
|
||||
int candidateVehicleEvidenceEventCount,
|
||||
int attachedVehicleEvidenceEventCount,
|
||||
int ignoredVehicleEvidenceEventCount,
|
||||
int mergedEventCount,
|
||||
List<RuntimeVehicleUsageIntervalDebugDto> vehicleUsageIntervals,
|
||||
List<RuntimeVehicleEvidenceAttachmentDecisionDto> vehicleEvidenceDecisions,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeDriverPartitionDebugDto {
|
||||
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
|
||||
vehicleEvidenceDecisions = vehicleEvidenceDecisions == null ? List.of() : List.copyOf(vehicleEvidenceDecisions);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public record RuntimeVehicleEvidenceAttachmentDecisionDto(
|
||||
String decision,
|
||||
String reason,
|
||||
String eventKey,
|
||||
String externalSourceEventId,
|
||||
OffsetDateTime occurredAt,
|
||||
String eventDomain,
|
||||
String eventType,
|
||||
String lifecycle,
|
||||
RuntimeEventScopeType scopeType,
|
||||
Set<String> vehicleKeys,
|
||||
List<String> matchingVehicleUsageIntervalIds
|
||||
) {
|
||||
public RuntimeVehicleEvidenceAttachmentDecisionDto {
|
||||
vehicleKeys = vehicleKeys == null ? Set.of() : Set.copyOf(vehicleKeys);
|
||||
matchingVehicleUsageIntervalIds = matchingVehicleUsageIntervalIds == null ? List.of() : List.copyOf(matchingVehicleUsageIntervalIds);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record RuntimeVehicleUsageIntervalDebugDto(
|
||||
String intervalId,
|
||||
OffsetDateTime from,
|
||||
OffsetDateTime to,
|
||||
String registrationKey,
|
||||
String vehicleKey,
|
||||
String sourceKind
|
||||
) {
|
||||
public static RuntimeVehicleUsageIntervalDebugDto from(ResolvedVehicleUsageInterval interval) {
|
||||
if (interval == null) {
|
||||
return null;
|
||||
}
|
||||
return new RuntimeVehicleUsageIntervalDebugDto(
|
||||
interval.intervalId(),
|
||||
interval.from(),
|
||||
interval.to(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
||||
import java.util.List;
|
||||
|
||||
public record UnifiedRuntimeDerivedProjectionResultDto(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int driverSeedEventCount,
|
||||
int discoveredVehicleCount,
|
||||
int expandedVehicleEventCount,
|
||||
int mergedEventCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
DriverWorkingTimeProcessingResultDto projection,
|
||||
List<String> notes,
|
||||
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization,
|
||||
RuntimeDriverPartitionDebugDto partitionDebug
|
||||
) {
|
||||
public UnifiedRuntimeDerivedProjectionResultDto {
|
||||
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
}
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int driverSeedEventCount,
|
||||
int discoveredVehicleCount,
|
||||
int expandedVehicleEventCount,
|
||||
int mergedEventCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
DriverWorkingTimeProcessingResultDto projection,
|
||||
List<String> notes
|
||||
) {
|
||||
this(
|
||||
request,
|
||||
driverSeedEventCount,
|
||||
discoveredVehicleCount,
|
||||
expandedVehicleEventCount,
|
||||
mergedEventCount,
|
||||
discoveredVehicles,
|
||||
projection,
|
||||
notes,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int driverSeedEventCount,
|
||||
int discoveredVehicleCount,
|
||||
int expandedVehicleEventCount,
|
||||
int mergedEventCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
DriverWorkingTimeProcessingResultDto projection,
|
||||
List<String> notes,
|
||||
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization
|
||||
) {
|
||||
this(
|
||||
request,
|
||||
driverSeedEventCount,
|
||||
discoveredVehicleCount,
|
||||
expandedVehicleEventCount,
|
||||
mergedEventCount,
|
||||
discoveredVehicles,
|
||||
projection,
|
||||
notes,
|
||||
supportEvidenceNormalization,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto withPartitionDebug(RuntimeDriverPartitionDebugDto debug) {
|
||||
return new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
request,
|
||||
driverSeedEventCount,
|
||||
discoveredVehicleCount,
|
||||
expandedVehicleEventCount,
|
||||
mergedEventCount,
|
||||
discoveredVehicles,
|
||||
projection,
|
||||
notes,
|
||||
supportEvidenceNormalization,
|
||||
debug
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,30 +4,45 @@ import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
|||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record UnifiedRuntimeProcessingApiRequest(
|
||||
UUID sessionId,
|
||||
List<UUID> sessionIds,
|
||||
UUID compositeSessionId,
|
||||
String tenantKey,
|
||||
Set<UnifiedEventSourceFamily> sourceFamilies,
|
||||
UnifiedRuntimeEventBackend eventBackend,
|
||||
String driverKey,
|
||||
Set<String> driverKeys,
|
||||
Boolean includeAllDrivers,
|
||||
Set<String> vehicleKeys,
|
||||
Boolean includeAllVehicles,
|
||||
String driverSourceEntityId,
|
||||
String driverCardNation,
|
||||
String driverCardNumber,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
Boolean expandVehicleEvents,
|
||||
Integer vehicleExpansionPaddingMinutes
|
||||
Integer vehicleExpansionPaddingMinutes,
|
||||
Integer significantDrivingMinutes,
|
||||
Integer minimumRestPeriodMinutes
|
||||
) {
|
||||
public UnifiedRuntimeProcessingRequest toRuntimeRequest() {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
sessionIds,
|
||||
compositeSessionId,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
eventBackend,
|
||||
driverKey,
|
||||
driverKeys,
|
||||
includeAllDrivers != null && includeAllDrivers,
|
||||
vehicleKeys,
|
||||
includeAllVehicles != null && includeAllVehicles,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
package at.procon.eventhub.processing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int inputEventCount,
|
||||
int selectedDriverCount,
|
||||
int discoveredVehicleCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
Map<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults,
|
||||
Map<String, RuntimeDriverPartitionDebugDto> partitionDebugByDriver,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto {
|
||||
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
|
||||
driverResults = driverResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(driverResults));
|
||||
partitionDebugByDriver = partitionDebugByDriver == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionDebugByDriver));
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeTachographEsperScopeResultDto fromDriverWorkingTime(
|
||||
UnifiedRuntimeDriverWorkingTimeScopeResultDto result
|
||||
) {
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
return new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
result.request(),
|
||||
result.inputEventCount(),
|
||||
result.selectedDriverCount(),
|
||||
result.discoveredVehicleCount(),
|
||||
result.discoveredVehicles(),
|
||||
result.driverResults(),
|
||||
result.partitionDebugByDriver(),
|
||||
result.notes(),
|
||||
result.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeTachographEsperScopeResultDto fromGenericRuntimeEventProcessingResult(
|
||||
RuntimeEventProcessingResultDto genericResult
|
||||
) {
|
||||
if (genericResult == null) {
|
||||
throw new IllegalArgumentException("genericResult must not be null");
|
||||
}
|
||||
LinkedHashMap<String, UnifiedRuntimeDerivedProjectionResultDto> driverResults = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : genericResult.partitionResults().entrySet()) {
|
||||
Object value = entry.getValue().result();
|
||||
if (value instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) {
|
||||
driverResults.put(entry.getKey(), projectionResult);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Cannot convert generic partition result for key "
|
||||
+ entry.getKey() + " to UnifiedRuntimeDerivedProjectionResultDto: "
|
||||
+ (value == null ? "null" : value.getClass().getName()));
|
||||
}
|
||||
}
|
||||
return new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
genericResult.request(),
|
||||
genericResult.inputEventCount(),
|
||||
genericResult.selectedPartitionCount(),
|
||||
genericResult.discoveredVehicleCount(),
|
||||
genericResult.discoveredVehicles(),
|
||||
driverResults,
|
||||
extractPartitionDebug(genericResult),
|
||||
genericResult.notes(),
|
||||
genericResult.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
private static Map<String, RuntimeDriverPartitionDebugDto> extractPartitionDebug(
|
||||
RuntimeEventProcessingResultDto genericResult
|
||||
) {
|
||||
LinkedHashMap<String, RuntimeDriverPartitionDebugDto> debugByDriver = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, RuntimeEventProcessingPartitionResultDto> entry : genericResult.partitionResults().entrySet()) {
|
||||
Object debug = entry.getValue().metadata().get("partitionDebug");
|
||||
if (debug instanceof RuntimeDriverPartitionDebugDto partitionDebug) {
|
||||
debugByDriver.put(entry.getKey(), partitionDebug);
|
||||
}
|
||||
}
|
||||
return debugByDriver;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package at.procon.eventhub.processing.eventprocessing;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import java.util.List;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.RuntimeEventProcessingProfileRegistry;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RuntimeEventProcessingService {
|
||||
|
||||
private final RuntimeEventProcessingProfileRegistry profileRegistry;
|
||||
|
||||
public RuntimeEventProcessingService(RuntimeEventProcessingProfileRegistry profileRegistry) {
|
||||
this.profileRegistry = profileRegistry;
|
||||
}
|
||||
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
return profileRegistry.require(request.profileKey()).process(request);
|
||||
}
|
||||
|
||||
public List<RuntimeEventProcessingProfileDescriptorDto> listProfiles() {
|
||||
return profileRegistry.profileDescriptors();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import java.util.Set;
|
||||
|
||||
public record RuntimeEventPartitioningApiRequest(
|
||||
RuntimeEventPartitioningStrategy strategy,
|
||||
Set<String> partitionKeys,
|
||||
Boolean includeAllPartitions,
|
||||
Set<String> driverKeys,
|
||||
Boolean includeAllDrivers,
|
||||
Set<String> vehicleKeys,
|
||||
Boolean includeAllVehicles,
|
||||
Boolean attachVehicleEvidence,
|
||||
Integer vehicleEvidencePaddingMinutes,
|
||||
Boolean includeDebug
|
||||
) {
|
||||
public RuntimeEventPartitioningApiRequest {
|
||||
strategy = strategy == null ? RuntimeEventPartitioningStrategy.CUSTOM_PROFILE : strategy;
|
||||
partitionKeys = partitionKeys == null ? Set.of() : Set.copyOf(partitionKeys);
|
||||
driverKeys = driverKeys == null ? Set.of() : Set.copyOf(driverKeys);
|
||||
vehicleKeys = vehicleKeys == null ? Set.of() : Set.copyOf(vehicleKeys);
|
||||
if (vehicleEvidencePaddingMinutes != null && vehicleEvidencePaddingMinutes < 0) {
|
||||
throw new IllegalArgumentException("vehicleEvidencePaddingMinutes must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean allPartitions() {
|
||||
return includeAllPartitions != null && includeAllPartitions;
|
||||
}
|
||||
|
||||
public boolean allDrivers() {
|
||||
return includeAllDrivers != null && includeAllDrivers;
|
||||
}
|
||||
|
||||
public boolean allVehicles() {
|
||||
return includeAllVehicles != null && includeAllVehicles;
|
||||
}
|
||||
|
||||
public boolean attachVehicleEvidenceOrDefault() {
|
||||
return attachVehicleEvidence == null || attachVehicleEvidence;
|
||||
}
|
||||
|
||||
public int vehicleEvidencePaddingMinutesOrDefault(int fallback) {
|
||||
return vehicleEvidencePaddingMinutes == null ? Math.max(0, fallback) : Math.max(0, vehicleEvidencePaddingMinutes);
|
||||
}
|
||||
|
||||
public boolean includeDebugOrDefault() {
|
||||
return includeDebug != null && includeDebug;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record RuntimeEventProcessingApiRequest(
|
||||
String profileKey,
|
||||
UnifiedRuntimeProcessingApiRequest scope,
|
||||
RuntimeEventPartitioningApiRequest partitioning,
|
||||
Map<String, Object> parameters
|
||||
) {
|
||||
public RuntimeEventProcessingApiRequest {
|
||||
if (profileKey == null || profileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("profileKey must not be blank");
|
||||
}
|
||||
profileKey = profileKey.trim();
|
||||
if (scope == null) {
|
||||
throw new IllegalArgumentException("scope must not be null");
|
||||
}
|
||||
partitioning = partitioning == null
|
||||
? new RuntimeEventPartitioningApiRequest(null, null, null, null, null, null, null, null, null, null)
|
||||
: partitioning;
|
||||
parameters = parameters == null
|
||||
? Map.of()
|
||||
: Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
|
||||
}
|
||||
|
||||
public static RuntimeEventProcessingApiRequest tachographDriverEsper(UnifiedRuntimeProcessingApiRequest scope) {
|
||||
return new RuntimeEventProcessingApiRequest(
|
||||
"tachograph-driver-esper-v1",
|
||||
scope,
|
||||
new RuntimeEventPartitioningApiRequest(
|
||||
at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy.DRIVER,
|
||||
null,
|
||||
scope != null ? scope.includeAllDrivers() : null,
|
||||
scope != null ? scope.driverKeys() : null,
|
||||
scope != null ? scope.includeAllDrivers() : null,
|
||||
scope != null ? scope.vehicleKeys() : null,
|
||||
scope != null ? scope.includeAllVehicles() : null,
|
||||
null,
|
||||
scope != null ? scope.vehicleExpansionPaddingMinutes() : null,
|
||||
null
|
||||
),
|
||||
Map.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public record RuntimeEventProcessingPartitionResultDto(
|
||||
String partitionType,
|
||||
String partitionKey,
|
||||
String resultType,
|
||||
Object result,
|
||||
Map<String, Object> metadata,
|
||||
Map<String, RuntimeProcessingModuleResult> moduleResults
|
||||
) {
|
||||
public RuntimeEventProcessingPartitionResultDto {
|
||||
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
|
||||
moduleResults = moduleResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(moduleResults));
|
||||
}
|
||||
|
||||
public RuntimeEventProcessingPartitionResultDto(
|
||||
String partitionType,
|
||||
String partitionKey,
|
||||
String resultType,
|
||||
Object result,
|
||||
Map<String, Object> metadata
|
||||
) {
|
||||
this(partitionType, partitionKey, resultType, result, metadata, Map.of());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.RuntimeEventProcessingProfile;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public record RuntimeEventProcessingProfileDescriptorDto(
|
||||
String profileKey,
|
||||
String displayName,
|
||||
String description,
|
||||
RuntimeEventPartitioningStrategy defaultPartitioningStrategy,
|
||||
List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies,
|
||||
Set<String> requiredParameters,
|
||||
Set<String> optionalParameters
|
||||
) {
|
||||
public RuntimeEventProcessingProfileDescriptorDto {
|
||||
supportedPartitioningStrategies = supportedPartitioningStrategies == null
|
||||
? List.of()
|
||||
: List.copyOf(supportedPartitioningStrategies);
|
||||
requiredParameters = requiredParameters == null ? Set.of() : Set.copyOf(requiredParameters);
|
||||
optionalParameters = optionalParameters == null ? Set.of() : Set.copyOf(optionalParameters);
|
||||
}
|
||||
|
||||
public static RuntimeEventProcessingProfileDescriptorDto from(RuntimeEventProcessingProfile profile) {
|
||||
if (profile == null) {
|
||||
throw new IllegalArgumentException("profile must not be null");
|
||||
}
|
||||
return new RuntimeEventProcessingProfileDescriptorDto(
|
||||
profile.profileKey(),
|
||||
profile.displayName(),
|
||||
profile.description(),
|
||||
profile.defaultPartitioningStrategy(),
|
||||
profile.supportedPartitioningStrategies(),
|
||||
profile.requiredParameters(),
|
||||
profile.optionalParameters()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.dto;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public record RuntimeEventProcessingResultDto(
|
||||
String profileKey,
|
||||
RuntimeEventPartitioningStrategy partitioningStrategy,
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
int inputEventCount,
|
||||
int selectedPartitionCount,
|
||||
int discoveredVehicleCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeEventProcessingResultDto {
|
||||
discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles);
|
||||
partitionResults = partitionResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionResults));
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
|
||||
public static RuntimeEventProcessingResultDto fromExecution(
|
||||
RuntimeProcessingExecutionResultDto result,
|
||||
String legacyProfileKey
|
||||
) {
|
||||
if (result == null) {
|
||||
throw new IllegalArgumentException("result must not be null");
|
||||
}
|
||||
return new RuntimeEventProcessingResultDto(
|
||||
legacyProfileKey == null || legacyProfileKey.isBlank() ? result.processingPlanKey() : legacyProfileKey,
|
||||
result.partitioningStrategy(),
|
||||
result.request(),
|
||||
result.inputEventCount(),
|
||||
result.selectedPartitionCount(),
|
||||
result.discoveredVehicleCount(),
|
||||
result.discoveredVehicles(),
|
||||
result.partitionResults(),
|
||||
result.notes(),
|
||||
result.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.module;
|
||||
|
||||
public enum RuntimeProcessingModuleStatus {
|
||||
SUCCESS,
|
||||
SKIPPED,
|
||||
FAILED
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.partition;
|
||||
|
||||
public enum RuntimeEventPartitioningStrategy {
|
||||
NONE,
|
||||
DRIVER,
|
||||
VEHICLE,
|
||||
DRIVER_VEHICLE,
|
||||
SOURCE_FAMILY,
|
||||
CUSTOM_PROFILE
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.partition;
|
||||
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RuntimeEventScopeClassifier {
|
||||
|
||||
public RuntimeEventScopeType classify(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return RuntimeEventScopeType.UNKNOWN;
|
||||
}
|
||||
boolean hasDriver = hasDriver(event);
|
||||
boolean hasVehicle = hasVehicle(event);
|
||||
if (hasDriver && hasVehicle) {
|
||||
return RuntimeEventScopeType.DRIVER_VEHICLE_SCOPED;
|
||||
}
|
||||
if (hasDriver) {
|
||||
return RuntimeEventScopeType.DRIVER_SCOPED;
|
||||
}
|
||||
if (hasVehicle) {
|
||||
return RuntimeEventScopeType.VEHICLE_SCOPED;
|
||||
}
|
||||
return RuntimeEventScopeType.GLOBAL_SUPPORT;
|
||||
}
|
||||
|
||||
public boolean hasDriver(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
if (text(rawPayload(event), "driverKey") != null) {
|
||||
return true;
|
||||
}
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
return driverRef != null && driverRef.hasAnyReference();
|
||||
}
|
||||
|
||||
public boolean hasVehicle(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return false;
|
||||
}
|
||||
JsonNode raw = rawPayload(event);
|
||||
if (text(raw, "vehicleKey") != null || text(raw, "registrationKey") != null) {
|
||||
return true;
|
||||
}
|
||||
VehicleRefDto vehicleRef = event.vehicleRef();
|
||||
return vehicleRef != null && vehicleRef.hasAnyReference();
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.partition;
|
||||
|
||||
public enum RuntimeEventScopeType {
|
||||
DRIVER_SCOPED,
|
||||
VEHICLE_SCOPED,
|
||||
DRIVER_VEHICLE_SCOPED,
|
||||
GLOBAL_SUPPORT,
|
||||
UNKNOWN
|
||||
}
|
||||
|
|
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface RuntimeEventProcessingProfile {
|
||||
|
||||
String profileKey();
|
||||
|
||||
RuntimeEventPartitioningStrategy defaultPartitioningStrategy();
|
||||
|
||||
default String displayName() {
|
||||
return profileKey();
|
||||
}
|
||||
|
||||
default String description() {
|
||||
return "";
|
||||
}
|
||||
|
||||
default List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
|
||||
RuntimeEventPartitioningStrategy defaultStrategy = defaultPartitioningStrategy();
|
||||
return defaultStrategy == null ? List.of() : List.of(defaultStrategy);
|
||||
}
|
||||
|
||||
default Set<String> requiredParameters() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
default Set<String> optionalParameters() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RuntimeEventProcessingProfileRegistry {
|
||||
|
||||
private final Map<String, RuntimeEventProcessingProfile> profilesByKey;
|
||||
|
||||
public RuntimeEventProcessingProfileRegistry(List<RuntimeEventProcessingProfile> profiles) {
|
||||
LinkedHashMap<String, RuntimeEventProcessingProfile> byKey = new LinkedHashMap<>();
|
||||
for (RuntimeEventProcessingProfile profile : profiles == null ? List.<RuntimeEventProcessingProfile>of() : profiles) {
|
||||
String key = profile.profileKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
throw new IllegalStateException("Runtime event processing profile returned a blank profileKey: " + profile.getClass().getName());
|
||||
}
|
||||
RuntimeEventProcessingProfile previous = byKey.putIfAbsent(key, profile);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate runtime event processing profileKey: " + key);
|
||||
}
|
||||
}
|
||||
this.profilesByKey = Collections.unmodifiableMap(byKey);
|
||||
}
|
||||
|
||||
public RuntimeEventProcessingProfile require(String profileKey) {
|
||||
String normalized = profileKey == null ? null : profileKey.trim();
|
||||
RuntimeEventProcessingProfile profile = normalized == null ? null : profilesByKey.get(normalized);
|
||||
if (profile == null) {
|
||||
throw new IllegalArgumentException("Unknown runtime event processing profileKey: " + profileKey
|
||||
+ ". Available profiles: " + profilesByKey.keySet());
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
public Map<String, RuntimeEventProcessingProfile> profilesByKey() {
|
||||
return profilesByKey;
|
||||
}
|
||||
|
||||
public List<RuntimeEventProcessingProfileDescriptorDto> profileDescriptors() {
|
||||
return profilesByKey.values().stream()
|
||||
.map(RuntimeEventProcessingProfileDescriptorDto::from)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.plan.DriverWorkingTimeRuntimeProcessingPlan;
|
||||
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile {
|
||||
|
||||
/**
|
||||
* Legacy compatibility key. New clients should use processingPlanKey=driver-working-time-v1
|
||||
* via /api/eventhub/runtime-processing/executions.
|
||||
*/
|
||||
public static final String PROFILE_KEY = DriverWorkingTimeRuntimeProcessingPlan.LEGACY_PROFILE_ALIAS;
|
||||
|
||||
private final DriverWorkingTimeRuntimeProcessingPlan plan;
|
||||
|
||||
@Autowired
|
||||
public TachographDriverEsperRuntimeEventProcessingProfile(DriverWorkingTimeRuntimeProcessingPlan plan) {
|
||||
this.plan = plan;
|
||||
}
|
||||
|
||||
public TachographDriverEsperRuntimeEventProcessingProfile(RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService) {
|
||||
this(new DriverWorkingTimeRuntimeProcessingPlan(scopeProcessingService));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String profileKey() {
|
||||
return PROFILE_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
|
||||
return plan.defaultPartitioningStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String displayName() {
|
||||
return "Tachograph Driver Esper Processing";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Compatibility adapter for legacy profileKey=" + PROFILE_KEY
|
||||
+ ". New clients should use processingPlanKey=" + plan.processingPlanKey() + ". "
|
||||
+ plan.description();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
|
||||
return plan.supportedPartitioningStrategies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> optionalParameters() {
|
||||
return plan.optionalParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> requiredParameters() {
|
||||
return plan.requiredParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
RuntimeProcessingExecutionResultDto executionResult = plan.execute(new RuntimeProcessingExecutionApiRequest(
|
||||
plan.processingPlanKey(),
|
||||
request.scope(),
|
||||
request.partitioning(),
|
||||
List.of(),
|
||||
request.parameters()
|
||||
));
|
||||
return RuntimeEventProcessingResultDto.fromExecution(executionResult, profileKey());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.support;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Source-neutral support-evidence view used before adapting mixed provider
|
||||
* events into a concrete processing profile. The current tachograph Esper
|
||||
* profile consumes this as geo support evidence when latitude/longitude are
|
||||
* available, while the original provider semantics remain in rawAttributes.
|
||||
*/
|
||||
public record RuntimeSupportEvidenceEvent(
|
||||
String eventId,
|
||||
String sourceFamily,
|
||||
String sourceKind,
|
||||
String eventDomain,
|
||||
String eventType,
|
||||
String lifecycle,
|
||||
String driverKey,
|
||||
String vehicleKey,
|
||||
String registrationKey,
|
||||
OffsetDateTime occurredAt,
|
||||
Long occurredAtEpochSecond,
|
||||
BigDecimal latitude,
|
||||
BigDecimal longitude,
|
||||
String countryCode,
|
||||
String regionCode,
|
||||
String countryFrom,
|
||||
String countryTo,
|
||||
String operation,
|
||||
Long odometerKm,
|
||||
BigDecimal speedKmh,
|
||||
BigDecimal maxSpeedKmh,
|
||||
Map<String, Object> rawAttributes
|
||||
) {
|
||||
public RuntimeSupportEvidenceEvent {
|
||||
rawAttributes = rawAttributes == null ? Map.of() : Map.copyOf(rawAttributes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.support;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import java.util.List;
|
||||
|
||||
public record RuntimeSupportEvidenceNormalizationResult(
|
||||
List<EventHubEventDto> normalizedEvents,
|
||||
int inputEventCount,
|
||||
int normalizedSupportEvidenceEventCount,
|
||||
int unchangedEventCount,
|
||||
List<String> notes
|
||||
) {
|
||||
public RuntimeSupportEvidenceNormalizationResult {
|
||||
normalizedEvents = normalizedEvents == null ? List.of() : List.copyOf(normalizedEvents);
|
||||
normalizedSupportEvidenceEventCount = Math.max(0, normalizedSupportEvidenceEventCount);
|
||||
unchangedEventCount = Math.max(0, unchangedEventCount);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.support;
|
||||
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventDetailsDto;
|
||||
import at.procon.eventhub.dto.EventDomain;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.EventLifecycle;
|
||||
import at.procon.eventhub.dto.EventType;
|
||||
import at.procon.eventhub.dto.GeoPointDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RuntimeSupportEvidenceNormalizer {
|
||||
|
||||
private static final LinkedHashSet<EventDomain> DIRECT_TACHOGRAPH_SUPPORT_DOMAINS = new LinkedHashSet<>(List.of(
|
||||
EventDomain.POSITION,
|
||||
EventDomain.PLACE,
|
||||
EventDomain.BORDER_CROSSING,
|
||||
EventDomain.LOAD_UNLOAD
|
||||
));
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public RuntimeSupportEvidenceNormalizer(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public RuntimeSupportEvidenceNormalizationResult normalizeForTachographDriver(
|
||||
String driverKey,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
List<EventHubEventDto> safeEvents = events == null ? List.of() : List.copyOf(events);
|
||||
List<EventHubEventDto> normalizedEvents = new ArrayList<>(safeEvents.size());
|
||||
int normalizedSupportEvidence = 0;
|
||||
int unchanged = 0;
|
||||
for (EventHubEventDto event : safeEvents) {
|
||||
EventHubEventDto normalized = normalizeOneForTachographDriver(driverKey, event);
|
||||
normalizedEvents.add(normalized);
|
||||
if (normalized != event) {
|
||||
normalizedSupportEvidence++;
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
List<String> notes = new ArrayList<>();
|
||||
notes.add("Runtime support evidence normalization inspected " + safeEvents.size() + " event(s).");
|
||||
notes.add("Runtime support evidence normalization adapted " + normalizedSupportEvidence
|
||||
+ " support/vehicle event(s) for the tachograph Esper profile.");
|
||||
return new RuntimeSupportEvidenceNormalizationResult(
|
||||
normalizedEvents,
|
||||
safeEvents.size(),
|
||||
normalizedSupportEvidence,
|
||||
unchanged,
|
||||
notes
|
||||
);
|
||||
}
|
||||
|
||||
public RuntimeSupportEvidenceEvent toSupportEvidenceEvent(String fallbackDriverKey, EventHubEventDto event) {
|
||||
if (event == null || isDriverActivityOrCardUsage(event)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = rawPayload(event);
|
||||
GeoPointDto position = event.position();
|
||||
BigDecimal latitude = position == null ? decimal(raw, "latitude") : position.latitude();
|
||||
BigDecimal longitude = position == null ? decimal(raw, "longitude") : position.longitude();
|
||||
Long odometerKm = firstNonNull(longValue(raw, "odometerKm"), toKilometers(event.odometerM()));
|
||||
return new RuntimeSupportEvidenceEvent(
|
||||
firstNonBlank(text(raw, "supportEventId"), text(raw, "sourceRowId"), event.externalSourceEventId()),
|
||||
sourceFamily(event),
|
||||
firstNonBlank(text(raw, "sourceKind"), sourceKind(event)),
|
||||
event.eventDomain() == null ? null : event.eventDomain().name(),
|
||||
event.eventType() == null ? null : event.eventType().name(),
|
||||
event.lifecycle() == null ? null : event.lifecycle().name(),
|
||||
firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(event)),
|
||||
firstNonBlank(text(raw, "vehicleKey"), vehicleKey(event)),
|
||||
firstNonBlank(text(raw, "registrationKey"), registrationKey(event)),
|
||||
event.occurredAt(),
|
||||
event.occurredAt() == null ? null : event.occurredAt().toEpochSecond(),
|
||||
latitude,
|
||||
longitude,
|
||||
firstNonBlank(text(raw, "country"), detailText(event, "country")),
|
||||
firstNonBlank(text(raw, "region"), detailText(event, "region")),
|
||||
firstNonBlank(text(raw, "countryFrom"), detailText(event, "countryFrom")),
|
||||
firstNonBlank(text(raw, "countryTo"), detailText(event, "countryTo")),
|
||||
firstNonBlank(text(raw, "operation"), detailText(event, "operation")),
|
||||
odometerKm,
|
||||
decimal(raw, "avgSpeedKmh"),
|
||||
decimal(raw, "maxSpeedKmh"),
|
||||
rawAttributes(event, raw)
|
||||
);
|
||||
}
|
||||
|
||||
private EventHubEventDto normalizeOneForTachographDriver(String fallbackDriverKey, EventHubEventDto event) {
|
||||
if (event == null || isDriverActivityOrCardUsage(event)) {
|
||||
return event;
|
||||
}
|
||||
RuntimeSupportEvidenceEvent support = toSupportEvidenceEvent(fallbackDriverKey, event);
|
||||
if (support == null || !hasGeoOrOdometerEvidence(support)) {
|
||||
return event;
|
||||
}
|
||||
EventDomain normalizedDomain = normalizedDomain(event);
|
||||
EventType normalizedType = normalizedType(normalizedDomain, event.eventType());
|
||||
EventLifecycle normalizedLifecycle = normalizedLifecycle(normalizedDomain, event.lifecycle());
|
||||
JsonNode payload = normalizedPayload(fallbackDriverKey, event, support, normalizedDomain);
|
||||
EventDetailsDto details = normalizedDetails(event, support, normalizedDomain);
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
if ((driverRef == null || !driverRef.hasAnyReference()) && support.driverKey() != null) {
|
||||
driverRef = new DriverRefDto(support.driverKey(), null);
|
||||
}
|
||||
return new EventHubEventDto(
|
||||
event.eventId(),
|
||||
event.externalSourceEventId(),
|
||||
driverRef,
|
||||
event.vehicleRef(),
|
||||
event.occurredAt(),
|
||||
event.receivedPartnerAt(),
|
||||
event.receivedHubAt(),
|
||||
normalizedDomain,
|
||||
normalizedType,
|
||||
normalizedLifecycle,
|
||||
event.odometerM(),
|
||||
normalizedPosition(event, support),
|
||||
details,
|
||||
event.sourcePackageRef(),
|
||||
payload,
|
||||
event.manualEntry(),
|
||||
event.packageInfo()
|
||||
);
|
||||
}
|
||||
|
||||
private GeoPointDto normalizedPosition(EventHubEventDto original, RuntimeSupportEvidenceEvent support) {
|
||||
if (original != null && original.position() != null) {
|
||||
return original.position();
|
||||
}
|
||||
if (support == null || support.latitude() == null || support.longitude() == null) {
|
||||
return null;
|
||||
}
|
||||
return new GeoPointDto(support.latitude(), support.longitude());
|
||||
}
|
||||
|
||||
private boolean isDriverActivityOrCardUsage(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return true;
|
||||
}
|
||||
if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
|
||||
return true;
|
||||
}
|
||||
return event.eventDomain() == EventDomain.DRIVER_CARD
|
||||
&& (event.eventType() == EventType.CARD_INSERTED || event.eventType() == EventType.CARD_WITHDRAWN);
|
||||
}
|
||||
|
||||
private boolean hasGeoOrOdometerEvidence(RuntimeSupportEvidenceEvent support) {
|
||||
return support != null
|
||||
&& ((support.latitude() != null && support.longitude() != null) || support.odometerKm() != null);
|
||||
}
|
||||
|
||||
private EventDomain normalizedDomain(EventHubEventDto event) {
|
||||
if (event.eventDomain() != null && DIRECT_TACHOGRAPH_SUPPORT_DOMAINS.contains(event.eventDomain())) {
|
||||
return event.eventDomain();
|
||||
}
|
||||
return EventDomain.POSITION;
|
||||
}
|
||||
|
||||
private EventType normalizedType(EventDomain normalizedDomain, EventType originalType) {
|
||||
if (normalizedDomain == EventDomain.POSITION) {
|
||||
return EventType.POSITION_RECORDED;
|
||||
}
|
||||
if (normalizedDomain == EventDomain.PLACE) {
|
||||
return EventType.WORKING_DAY_PLACE_RECORDED;
|
||||
}
|
||||
if (normalizedDomain == EventDomain.BORDER_CROSSING) {
|
||||
return originalType == null ? EventType.BORDER_INBOUND : originalType;
|
||||
}
|
||||
if (normalizedDomain == EventDomain.LOAD_UNLOAD) {
|
||||
return originalType == null ? EventType.LOAD_UNLOAD : originalType;
|
||||
}
|
||||
return originalType == null ? EventType.UNKNOWN_EVENT : originalType;
|
||||
}
|
||||
|
||||
private EventLifecycle normalizedLifecycle(EventDomain normalizedDomain, EventLifecycle originalLifecycle) {
|
||||
if (normalizedDomain == EventDomain.POSITION || normalizedDomain == EventDomain.PLACE) {
|
||||
return EventLifecycle.SNAPSHOT;
|
||||
}
|
||||
return originalLifecycle == null ? EventLifecycle.SNAPSHOT : originalLifecycle;
|
||||
}
|
||||
|
||||
private EventDetailsDto normalizedDetails(
|
||||
EventHubEventDto original,
|
||||
RuntimeSupportEvidenceEvent support,
|
||||
EventDomain normalizedDomain
|
||||
) {
|
||||
Map<String, Object> attributes = new LinkedHashMap<>();
|
||||
put(attributes, "normalizedSupportEvidence", true);
|
||||
put(attributes, "normalizedForProfile", "tachograph-driver-esper-v1");
|
||||
put(attributes, "originalEventDomain", support.eventDomain());
|
||||
put(attributes, "originalEventType", support.eventType());
|
||||
put(attributes, "originalLifecycle", support.lifecycle());
|
||||
put(attributes, "sourceFamily", support.sourceFamily());
|
||||
put(attributes, "sourceKind", support.sourceKind());
|
||||
put(attributes, "country", support.countryCode());
|
||||
put(attributes, "region", support.regionCode());
|
||||
put(attributes, "countryFrom", support.countryFrom());
|
||||
put(attributes, "countryTo", support.countryTo());
|
||||
put(attributes, "operation", support.operation());
|
||||
if (original.eventDetails() != null && original.eventDetails().attributes() != null) {
|
||||
attributes.put("originalAttributes", original.eventDetails().attributes());
|
||||
}
|
||||
String type = normalizedDomain == EventDomain.POSITION ? "POSITION" : normalizedDomain.name();
|
||||
return new EventDetailsDto(type, objectMapper.valueToTree(attributes));
|
||||
}
|
||||
|
||||
private JsonNode normalizedPayload(
|
||||
String fallbackDriverKey,
|
||||
EventHubEventDto original,
|
||||
RuntimeSupportEvidenceEvent support,
|
||||
EventDomain normalizedDomain
|
||||
) {
|
||||
ObjectNode root = objectMapper.createObjectNode();
|
||||
ObjectNode raw = root.putObject("raw");
|
||||
JsonNode originalRaw = rawPayload(original);
|
||||
if (originalRaw != null && originalRaw.isObject()) {
|
||||
originalRaw.fields().forEachRemaining(entry -> raw.set(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
put(raw, "normalizedSupportEvidence", true);
|
||||
put(raw, "normalizedForProfile", "tachograph-driver-esper-v1");
|
||||
put(raw, "supportEventId", support.eventId());
|
||||
put(raw, "supportEventDomain", support.eventDomain());
|
||||
put(raw, "supportEventType", support.eventType());
|
||||
put(raw, "supportEventLifecycle", support.lifecycle());
|
||||
put(raw, "originalEventDomain", support.eventDomain());
|
||||
put(raw, "originalEventType", support.eventType());
|
||||
put(raw, "originalLifecycle", support.lifecycle());
|
||||
put(raw, "normalizedEventDomain", normalizedDomain.name());
|
||||
put(raw, "driverKey", firstNonBlank(support.driverKey(), fallbackDriverKey));
|
||||
put(raw, "vehicleKey", support.vehicleKey());
|
||||
put(raw, "registrationKey", support.registrationKey());
|
||||
put(raw, "sourceFamily", support.sourceFamily());
|
||||
put(raw, "sourceKind", support.sourceKind());
|
||||
put(raw, "country", support.countryCode());
|
||||
put(raw, "region", support.regionCode());
|
||||
put(raw, "countryFrom", support.countryFrom());
|
||||
put(raw, "countryTo", support.countryTo());
|
||||
put(raw, "operation", support.operation());
|
||||
put(raw, "odometerKm", support.odometerKm());
|
||||
put(raw, "avgSpeedKmh", support.speedKmh());
|
||||
put(raw, "maxSpeedKmh", support.maxSpeedKmh());
|
||||
put(raw, "latitude", support.latitude());
|
||||
put(raw, "longitude", support.longitude());
|
||||
put(raw, "rawRecordPath", firstNonBlank(text(originalRaw, "rawRecordPath"), original.externalSourceEventId()));
|
||||
return root;
|
||||
}
|
||||
|
||||
private Map<String, Object> rawAttributes(EventHubEventDto event, JsonNode raw) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
put(result, "externalSourceEventId", event == null ? null : event.externalSourceEventId());
|
||||
put(result, "eventDomain", event == null || event.eventDomain() == null ? null : event.eventDomain().name());
|
||||
put(result, "eventType", event == null || event.eventType() == null ? null : event.eventType().name());
|
||||
put(result, "lifecycle", event == null || event.lifecycle() == null ? null : event.lifecycle().name());
|
||||
if (raw != null) {
|
||||
result.put("raw", raw);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String sourceFamily(EventHubEventDto event) {
|
||||
if (event == null || event.packageInfo() == null) {
|
||||
return null;
|
||||
}
|
||||
return event.packageInfo().eventFamily();
|
||||
}
|
||||
|
||||
private String sourceKind(EventHubEventDto event) {
|
||||
if (event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null) {
|
||||
return null;
|
||||
}
|
||||
return event.packageInfo().eventSource().sourceKind();
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
if (event == null || event.payload() == null || event.payload().isNull() || event.payload().isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = event.payload().get("raw");
|
||||
return raw == null || raw.isNull() ? event.payload() : raw;
|
||||
}
|
||||
|
||||
private String driverKey(EventHubEventDto event) {
|
||||
if (event == null || event.driverRef() == null || !event.driverRef().hasAnyReference()) {
|
||||
return null;
|
||||
}
|
||||
return event.driverRef().stableKey();
|
||||
}
|
||||
|
||||
private String vehicleKey(EventHubEventDto event) {
|
||||
JsonNode raw = rawPayload(event);
|
||||
String rawVehicleKey = text(raw, "vehicleKey");
|
||||
if (rawVehicleKey != null) {
|
||||
return rawVehicleKey;
|
||||
}
|
||||
VehicleRefDto vehicleRef = event == null ? null : event.vehicleRef();
|
||||
if (vehicleRef == null) {
|
||||
return null;
|
||||
}
|
||||
if (vehicleRef.vin() != null) {
|
||||
return vehicleRef.vin();
|
||||
}
|
||||
if (vehicleRef.sourceVehicleEntityId() != null) {
|
||||
return vehicleRef.sourceVehicleEntityId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String registrationKey(EventHubEventDto event) {
|
||||
JsonNode raw = rawPayload(event);
|
||||
String rawRegistrationKey = text(raw, "registrationKey");
|
||||
if (rawRegistrationKey != null) {
|
||||
return rawRegistrationKey;
|
||||
}
|
||||
VehicleRefDto vehicleRef = event == null ? null : event.vehicleRef();
|
||||
if (vehicleRef == null) {
|
||||
return null;
|
||||
}
|
||||
if (vehicleRef.vehicleRegistration() != null && vehicleRef.vehicleRegistration().hasValue()) {
|
||||
return vehicleRef.vehicleRegistration().stableKey();
|
||||
}
|
||||
return vehicleRef.sourceRegistrationEntityId();
|
||||
}
|
||||
|
||||
private String detailText(EventHubEventDto event, String field) {
|
||||
if (event == null || event.eventDetails() == null || event.eventDetails().attributes() == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = event.eventDetails().attributes().get(field);
|
||||
return value == null || value.isNull() ? null : value.asText(null);
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
|
||||
private Long longValue(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return value.isNumber() ? value.asLong() : Long.parseLong(value.asText());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal decimal(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (value.isNumber()) {
|
||||
return value.decimalValue();
|
||||
}
|
||||
String text = value.asText(null);
|
||||
if (text == null || text.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(text.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long toKilometers(Long meters) {
|
||||
return meters == null ? null : meters / 1_000L;
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private final <T> T firstNonNull(T... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (T value : values) {
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void put(ObjectNode node, String field, Object value) {
|
||||
if (node != null && field != null && value != null) {
|
||||
node.set(field, objectMapper.valueToTree(value));
|
||||
}
|
||||
}
|
||||
|
||||
private void put(Map<String, Object> target, String field, Object value) {
|
||||
if (target != null && field != null && value != null) {
|
||||
target.put(field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.validation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RuntimeTachographDriverParityResultDto(
|
||||
String driverKey,
|
||||
String status,
|
||||
String referenceMode,
|
||||
int referenceSessionCount,
|
||||
boolean runtimePartitionPresent,
|
||||
List<RuntimeTachographParityCategoryComparisonDto> comparisons,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeTachographDriverParityResultDto {
|
||||
comparisons = comparisons == null ? List.of() : List.copyOf(comparisons);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.validation;
|
||||
|
||||
public record RuntimeTachographParityCategoryComparisonDto(
|
||||
String category,
|
||||
int fileSessionCount,
|
||||
int runtimeCount,
|
||||
boolean equal
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.validation;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record RuntimeTachographParityValidationApiRequest(
|
||||
UUID sessionId,
|
||||
List<UUID> sessionIds,
|
||||
UUID compositeSessionId,
|
||||
String driverKey,
|
||||
Set<String> driverKeys,
|
||||
Boolean includeAllDrivers,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
Boolean expandVehicleEvents,
|
||||
Integer vehicleExpansionPaddingMinutes,
|
||||
Integer significantDrivingMinutes,
|
||||
Integer minimumRestPeriodMinutes,
|
||||
Boolean includeDebug
|
||||
) {
|
||||
public RuntimeTachographParityValidationApiRequest {
|
||||
sessionIds = sessionIds == null ? List.of() : List.copyOf(sessionIds);
|
||||
driverKeys = driverKeys == null ? Set.of() : Set.copyOf(driverKeys);
|
||||
vehicleExpansionPaddingMinutes = vehicleExpansionPaddingMinutes == null
|
||||
? null
|
||||
: Math.max(0, vehicleExpansionPaddingMinutes);
|
||||
significantDrivingMinutes = significantDrivingMinutes == null ? null : Math.max(1, significantDrivingMinutes);
|
||||
minimumRestPeriodMinutes = minimumRestPeriodMinutes == null ? null : Math.max(1, minimumRestPeriodMinutes);
|
||||
}
|
||||
|
||||
public Set<String> requestedDriverKeys() {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
if (driverKey != null && !driverKey.isBlank()) {
|
||||
result.add(driverKey.trim());
|
||||
}
|
||||
driverKeys.stream()
|
||||
.filter(value -> value != null && !value.isBlank())
|
||||
.map(String::trim)
|
||||
.forEach(result::add);
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
public boolean includeAllDriversOrDefault() {
|
||||
return includeAllDrivers == null ? requestedDriverKeys().isEmpty() : includeAllDrivers;
|
||||
}
|
||||
|
||||
public boolean expandVehicleEventsOrDefault() {
|
||||
return expandVehicleEvents == null || expandVehicleEvents;
|
||||
}
|
||||
|
||||
public boolean includeDebugOrDefault() {
|
||||
return includeDebug != null && includeDebug;
|
||||
}
|
||||
|
||||
public int vehicleExpansionPaddingMinutesOrDefault() {
|
||||
return vehicleExpansionPaddingMinutes == null ? 0 : Math.max(0, vehicleExpansionPaddingMinutes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.validation;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public record RuntimeTachographParityValidationResultDto(
|
||||
String profileKey,
|
||||
String status,
|
||||
List<UUID> sessionIds,
|
||||
int selectedDriverCount,
|
||||
Map<String, RuntimeTachographDriverParityResultDto> driverResults,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeTachographParityValidationResultDto {
|
||||
sessionIds = sessionIds == null ? List.of() : List.copyOf(sessionIds);
|
||||
driverResults = driverResults == null ? Map.of() : Map.copyOf(new LinkedHashMap<>(driverResults));
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.validation;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperEventsProcessingRequest;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographCompositeSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionRepository;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RuntimeTachographParityValidationService {
|
||||
|
||||
private final TachographFileSessionRepository fileSessionRepository;
|
||||
private final TachographCompositeSessionRepository compositeSessionRepository;
|
||||
private final TachographFileSessionProcessingService fileSessionProcessingService;
|
||||
private final RuntimeEventProcessingService runtimeEventProcessingService;
|
||||
|
||||
public RuntimeTachographParityValidationService(
|
||||
TachographFileSessionRepository fileSessionRepository,
|
||||
TachographCompositeSessionRepository compositeSessionRepository,
|
||||
TachographFileSessionProcessingService fileSessionProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService
|
||||
) {
|
||||
this.fileSessionRepository = fileSessionRepository;
|
||||
this.compositeSessionRepository = compositeSessionRepository;
|
||||
this.fileSessionProcessingService = fileSessionProcessingService;
|
||||
this.runtimeEventProcessingService = runtimeEventProcessingService;
|
||||
}
|
||||
|
||||
public RuntimeTachographParityValidationResultDto validate(RuntimeTachographParityValidationApiRequest request) {
|
||||
RuntimeTachographParityValidationApiRequest effectiveRequest = request == null
|
||||
? new RuntimeTachographParityValidationApiRequest(null, List.of(), null, null, Set.of(), true,
|
||||
null, null, true, 0, null, null, false)
|
||||
: request;
|
||||
List<UUID> sessionIds = resolveSessionIds(effectiveRequest);
|
||||
Set<String> driverKeys = resolveDriverKeys(effectiveRequest, sessionIds);
|
||||
if (driverKeys.isEmpty()) {
|
||||
throw new IllegalArgumentException("No driver keys could be resolved for tachograph parity validation.");
|
||||
}
|
||||
|
||||
RuntimeEventProcessingResultDto runtimeResult = runtimeEventProcessingService.process(genericRuntimeRequest(
|
||||
effectiveRequest,
|
||||
sessionIds,
|
||||
driverKeys
|
||||
));
|
||||
|
||||
LinkedHashMap<String, RuntimeTachographDriverParityResultDto> driverResults = new LinkedHashMap<>();
|
||||
List<String> warnings = new ArrayList<>(runtimeResult.warnings());
|
||||
for (String driverKey : driverKeys) {
|
||||
RuntimeTachographDriverParityResultDto driverResult = compareDriver(
|
||||
effectiveRequest,
|
||||
sessionIds,
|
||||
driverKey,
|
||||
runtimeResult
|
||||
);
|
||||
driverResults.put(driverKey, driverResult);
|
||||
warnings.addAll(driverResult.warnings());
|
||||
}
|
||||
|
||||
String status = driverResults.values().stream().allMatch(result -> "EQUAL".equals(result.status()))
|
||||
? "EQUAL"
|
||||
: "DIFFERENT";
|
||||
List<String> notes = new ArrayList<>(runtimeResult.notes());
|
||||
notes.add("Validation compares the legacy tachograph file-session esper-events path with the generic runtime event-processing profile '"
|
||||
+ TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY + "'.");
|
||||
if (sessionIds.size() > 1) {
|
||||
notes.add("For multiple sessions, file-session reference counts are summed per driver. Runtime processing may intentionally merge or deduplicate intervals across session boundaries, so category differences need domain review.");
|
||||
}
|
||||
|
||||
return new RuntimeTachographParityValidationResultDto(
|
||||
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
|
||||
status,
|
||||
sessionIds,
|
||||
driverResults.size(),
|
||||
driverResults,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
private RuntimeTachographDriverParityResultDto compareDriver(
|
||||
RuntimeTachographParityValidationApiRequest request,
|
||||
List<UUID> sessionIds,
|
||||
String driverKey,
|
||||
RuntimeEventProcessingResultDto runtimeResult
|
||||
) {
|
||||
RuntimeProjectionCounts runtimeCounts = runtimeCounts(runtimeResult, driverKey);
|
||||
ReferenceProjectionCounts referenceCounts = referenceCounts(request, sessionIds, driverKey);
|
||||
List<RuntimeTachographParityCategoryComparisonDto> comparisons = comparisons(referenceCounts.counts(), runtimeCounts.counts());
|
||||
boolean runtimePresent = runtimeCounts.present();
|
||||
boolean referencePresent = referenceCounts.referenceSessionCount() > 0;
|
||||
boolean allEqual = comparisons.stream().allMatch(RuntimeTachographParityCategoryComparisonDto::equal);
|
||||
|
||||
String status;
|
||||
if (!referencePresent) {
|
||||
status = "REFERENCE_MISSING";
|
||||
} else if (!runtimePresent) {
|
||||
status = "RUNTIME_MISSING";
|
||||
} else if (allEqual) {
|
||||
status = "EQUAL";
|
||||
} else {
|
||||
status = "DIFFERENT";
|
||||
}
|
||||
|
||||
List<String> notes = new ArrayList<>();
|
||||
notes.add("Reference mode: " + referenceCounts.referenceMode() + ".");
|
||||
notes.add("Reference session count: " + referenceCounts.referenceSessionCount() + ".");
|
||||
if (sessionIds.size() > 1) {
|
||||
notes.add("Reference counts are summed from individual file-session endpoint results for this driver.");
|
||||
}
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (!referencePresent) {
|
||||
warnings.add("Driver " + driverKey + " was not found in any selected tachograph file session.");
|
||||
}
|
||||
if (!runtimePresent) {
|
||||
warnings.add("Generic runtime event-processing profile did not return a partition for driver " + driverKey + ".");
|
||||
}
|
||||
if (runtimePresent && referencePresent && !allEqual) {
|
||||
warnings.add("Runtime result differs from file-session reference result for driver " + driverKey + ".");
|
||||
}
|
||||
return new RuntimeTachographDriverParityResultDto(
|
||||
driverKey,
|
||||
status,
|
||||
referenceCounts.referenceMode(),
|
||||
referenceCounts.referenceSessionCount(),
|
||||
runtimePresent,
|
||||
comparisons,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
private RuntimeEventProcessingApiRequest genericRuntimeRequest(
|
||||
RuntimeTachographParityValidationApiRequest request,
|
||||
List<UUID> sessionIds,
|
||||
Set<String> driverKeys
|
||||
) {
|
||||
UnifiedRuntimeProcessingApiRequest scope = new UnifiedRuntimeProcessingApiRequest(
|
||||
null,
|
||||
sessionIds,
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
null,
|
||||
driverKeys,
|
||||
request.includeAllDriversOrDefault(),
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
request.occurredFrom(),
|
||||
request.occurredTo(),
|
||||
request.expandVehicleEventsOrDefault(),
|
||||
request.vehicleExpansionPaddingMinutesOrDefault(),
|
||||
request.significantDrivingMinutes(),
|
||||
request.minimumRestPeriodMinutes()
|
||||
);
|
||||
RuntimeEventPartitioningApiRequest partitioning = new RuntimeEventPartitioningApiRequest(
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
null,
|
||||
request.includeAllDriversOrDefault(),
|
||||
driverKeys,
|
||||
request.includeAllDriversOrDefault(),
|
||||
null,
|
||||
false,
|
||||
request.expandVehicleEventsOrDefault(),
|
||||
request.vehicleExpansionPaddingMinutesOrDefault(),
|
||||
request.includeDebugOrDefault()
|
||||
);
|
||||
Map<String, Object> parameters = new LinkedHashMap<>();
|
||||
if (request.significantDrivingMinutes() != null) {
|
||||
parameters.put("significantDrivingMinutes", request.significantDrivingMinutes());
|
||||
}
|
||||
if (request.minimumRestPeriodMinutes() != null) {
|
||||
parameters.put("minimumRestPeriodMinutes", request.minimumRestPeriodMinutes());
|
||||
}
|
||||
parameters.put("attachVehicleOnlyEvents", request.expandVehicleEventsOrDefault());
|
||||
parameters.put("vehicleEvidencePaddingMinutes", request.vehicleExpansionPaddingMinutesOrDefault());
|
||||
parameters.put("includePartitionDebug", request.includeDebugOrDefault());
|
||||
return new RuntimeEventProcessingApiRequest(
|
||||
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
|
||||
scope,
|
||||
partitioning,
|
||||
parameters
|
||||
);
|
||||
}
|
||||
|
||||
private List<UUID> resolveSessionIds(RuntimeTachographParityValidationApiRequest request) {
|
||||
LinkedHashSet<UUID> result = new LinkedHashSet<>();
|
||||
if (request.sessionId() != null) {
|
||||
result.add(request.sessionId());
|
||||
}
|
||||
result.addAll(request.sessionIds());
|
||||
if (request.compositeSessionId() != null) {
|
||||
TachographCompositeSession compositeSession = compositeSessionRepository.find(request.compositeSessionId())
|
||||
.orElseThrow(() -> new TachographCompositeSessionNotFoundException(request.compositeSessionId()));
|
||||
result.addAll(compositeSession.memberSessionIds());
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
throw new IllegalArgumentException("sessionId, sessionIds, or compositeSessionId is required for tachograph parity validation.");
|
||||
}
|
||||
result.forEach(this::requireFileSession);
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private Set<String> resolveDriverKeys(RuntimeTachographParityValidationApiRequest request, List<UUID> sessionIds) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>(request.requestedDriverKeys());
|
||||
if (request.includeAllDriversOrDefault() || result.isEmpty()) {
|
||||
for (UUID sessionId : sessionIds) {
|
||||
result.addAll(requireFileSession(sessionId).driversByKey().keySet());
|
||||
}
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private TachographFileSession requireFileSession(UUID sessionId) {
|
||||
return fileSessionRepository.find(sessionId)
|
||||
.orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
|
||||
}
|
||||
|
||||
private ReferenceProjectionCounts referenceCounts(
|
||||
RuntimeTachographParityValidationApiRequest request,
|
||||
List<UUID> sessionIds,
|
||||
String driverKey
|
||||
) {
|
||||
ProjectionCounts aggregate = ProjectionCounts.empty();
|
||||
int referenceSessionCount = 0;
|
||||
TachographEsperEventsProcessingRequest fileSessionRequest = new TachographEsperEventsProcessingRequest(
|
||||
request.occurredFrom(),
|
||||
request.occurredTo(),
|
||||
request.significantDrivingMinutes(),
|
||||
request.minimumRestPeriodMinutes()
|
||||
);
|
||||
for (UUID sessionId : sessionIds) {
|
||||
TachographFileSession session = requireFileSession(sessionId);
|
||||
if (!session.driversByKey().containsKey(driverKey)) {
|
||||
continue;
|
||||
}
|
||||
TachographEsperDriverProcessingResultDto projection = fileSessionProcessingService.getEsperDriverProcessingResults(
|
||||
sessionId,
|
||||
driverKey,
|
||||
fileSessionRequest
|
||||
);
|
||||
aggregate = aggregate.plus(ProjectionCounts.from(projection));
|
||||
referenceSessionCount++;
|
||||
}
|
||||
String referenceMode = sessionIds.size() == 1 ? "SINGLE_FILE_SESSION" : "SUM_OF_FILE_SESSION_RESULTS";
|
||||
return new ReferenceProjectionCounts(aggregate, referenceMode, referenceSessionCount);
|
||||
}
|
||||
|
||||
private RuntimeProjectionCounts runtimeCounts(RuntimeEventProcessingResultDto runtimeResult, String driverKey) {
|
||||
RuntimeEventProcessingPartitionResultDto partitionResult = runtimeResult.partitionResults().get(driverKey);
|
||||
if (partitionResult == null) {
|
||||
return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
|
||||
}
|
||||
Object result = partitionResult.result();
|
||||
if (result instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) {
|
||||
return new RuntimeProjectionCounts(ProjectionCounts.from(projectionResult.projection()), projectionResult.projection() != null);
|
||||
}
|
||||
return new RuntimeProjectionCounts(ProjectionCounts.empty(), false);
|
||||
}
|
||||
|
||||
private List<RuntimeTachographParityCategoryComparisonDto> comparisons(
|
||||
ProjectionCounts reference,
|
||||
ProjectionCounts runtime
|
||||
) {
|
||||
return List.of(
|
||||
comparison("activityIntervals", reference.activityIntervalCount(), runtime.activityIntervalCount()),
|
||||
comparison("drivingIntervals", reference.drivingIntervalCount(), runtime.drivingIntervalCount()),
|
||||
comparison("drivingInterruptionIntervals", reference.drivingInterruptionIntervalCount(), runtime.drivingInterruptionIntervalCount()),
|
||||
comparison("drivingInterruptionVehicleChangeIntervals", reference.drivingInterruptionVehicleChangeIntervalCount(), runtime.drivingInterruptionVehicleChangeIntervalCount()),
|
||||
comparison("dailyWeeklyRestCandidateIntervals", reference.dailyWeeklyRestCandidateIntervalCount(), runtime.dailyWeeklyRestCandidateIntervalCount()),
|
||||
comparison("dailyWeeklyRestCandidateCoverageIntervals", reference.dailyWeeklyRestCandidateCoverageIntervalCount(), runtime.dailyWeeklyRestCandidateCoverageIntervalCount()),
|
||||
comparison("unclassifiedDailyWeeklyRestCandidateCoverageIntervals", reference.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(), runtime.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount()),
|
||||
comparison("potentialHomeOvernightStayIntervals", reference.potentialHomeOvernightStayIntervalCount(), runtime.potentialHomeOvernightStayIntervalCount()),
|
||||
comparison("potentialInVehicleOvernightStayIntervals", reference.potentialInVehicleOvernightStayIntervalCount(), runtime.potentialInVehicleOvernightStayIntervalCount()),
|
||||
comparison("potentialInVehicleTripIntervals", reference.potentialInVehicleTripIntervalCount(), runtime.potentialInVehicleTripIntervalCount()),
|
||||
comparison("vehicleUsageIntervals", reference.vehicleUsageIntervalCount(), runtime.vehicleUsageIntervalCount()),
|
||||
comparison("vuCardAbsentIntervals", reference.vuCardAbsentIntervalCount(), runtime.vuCardAbsentIntervalCount()),
|
||||
comparison("supportGeoEvents", reference.supportGeoEventCount(), runtime.supportGeoEventCount())
|
||||
);
|
||||
}
|
||||
|
||||
private RuntimeTachographParityCategoryComparisonDto comparison(String category, int fileSessionCount, int runtimeCount) {
|
||||
return new RuntimeTachographParityCategoryComparisonDto(
|
||||
category,
|
||||
fileSessionCount,
|
||||
runtimeCount,
|
||||
fileSessionCount == runtimeCount
|
||||
);
|
||||
}
|
||||
|
||||
private record ReferenceProjectionCounts(
|
||||
ProjectionCounts counts,
|
||||
String referenceMode,
|
||||
int referenceSessionCount
|
||||
) {
|
||||
}
|
||||
|
||||
private record RuntimeProjectionCounts(
|
||||
ProjectionCounts counts,
|
||||
boolean present
|
||||
) {
|
||||
}
|
||||
|
||||
private record ProjectionCounts(
|
||||
int activityIntervalCount,
|
||||
int drivingIntervalCount,
|
||||
int drivingInterruptionIntervalCount,
|
||||
int drivingInterruptionVehicleChangeIntervalCount,
|
||||
int dailyWeeklyRestCandidateIntervalCount,
|
||||
int dailyWeeklyRestCandidateCoverageIntervalCount,
|
||||
int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
|
||||
int potentialHomeOvernightStayIntervalCount,
|
||||
int potentialInVehicleOvernightStayIntervalCount,
|
||||
int potentialInVehicleTripIntervalCount,
|
||||
int vehicleUsageIntervalCount,
|
||||
int vuCardAbsentIntervalCount,
|
||||
int supportGeoEventCount
|
||||
) {
|
||||
static ProjectionCounts empty() {
|
||||
return new ProjectionCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) {
|
||||
return projection == null ? empty() : from(projection.toDriverWorkingTime());
|
||||
}
|
||||
|
||||
static ProjectionCounts from(DriverWorkingTimeProcessingResultDto projection) {
|
||||
if (projection == null) {
|
||||
return empty();
|
||||
}
|
||||
return new ProjectionCounts(
|
||||
projection.activityIntervalCount(),
|
||||
projection.drivingIntervalCount(),
|
||||
projection.drivingInterruptionIntervalCount(),
|
||||
projection.drivingInterruptionVehicleChangeIntervalCount(),
|
||||
projection.dailyWeeklyRestCandidateIntervalCount(),
|
||||
projection.dailyWeeklyRestCandidateCoverageIntervalCount(),
|
||||
projection.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(),
|
||||
projection.potentialHomeOvernightStayIntervalCount(),
|
||||
projection.potentialInVehicleOvernightStayIntervalCount(),
|
||||
projection.potentialInVehicleTripIntervalCount(),
|
||||
projection.vehicleUsageIntervalCount(),
|
||||
projection.vuCardAbsentIntervalCount(),
|
||||
projection.supportGeoEventCount()
|
||||
);
|
||||
}
|
||||
|
||||
ProjectionCounts plus(ProjectionCounts other) {
|
||||
return new ProjectionCounts(
|
||||
activityIntervalCount + other.activityIntervalCount,
|
||||
drivingIntervalCount + other.drivingIntervalCount,
|
||||
drivingInterruptionIntervalCount + other.drivingInterruptionIntervalCount,
|
||||
drivingInterruptionVehicleChangeIntervalCount + other.drivingInterruptionVehicleChangeIntervalCount,
|
||||
dailyWeeklyRestCandidateIntervalCount + other.dailyWeeklyRestCandidateIntervalCount,
|
||||
dailyWeeklyRestCandidateCoverageIntervalCount + other.dailyWeeklyRestCandidateCoverageIntervalCount,
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount + other.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
|
||||
potentialHomeOvernightStayIntervalCount + other.potentialHomeOvernightStayIntervalCount,
|
||||
potentialInVehicleOvernightStayIntervalCount + other.potentialInVehicleOvernightStayIntervalCount,
|
||||
potentialInVehicleTripIntervalCount + other.potentialInVehicleTripIntervalCount,
|
||||
vehicleUsageIntervalCount + other.vehicleUsageIntervalCount,
|
||||
vuCardAbsentIntervalCount + other.vuCardAbsentIntervalCount,
|
||||
supportGeoEventCount + other.supportGeoEventCount
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package at.procon.eventhub.processing.model;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
|
||||
import java.util.List;
|
||||
|
||||
public record RuntimeDriverVehicleEvidenceAttachmentResult(
|
||||
String driverKey,
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> attachedVehicleEvidenceEvents,
|
||||
List<EventHubEventDto> mergedEvents,
|
||||
int vehicleUsageIntervalCount,
|
||||
int candidateVehicleEvidenceEventCount,
|
||||
int ignoredVehicleEvidenceEventCount,
|
||||
List<RuntimeVehicleUsageIntervalDebugDto> vehicleUsageIntervals,
|
||||
List<RuntimeVehicleEvidenceAttachmentDecisionDto> vehicleEvidenceDecisions,
|
||||
List<String> notes,
|
||||
List<String> warnings
|
||||
) {
|
||||
public RuntimeDriverVehicleEvidenceAttachmentResult {
|
||||
directDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
|
||||
attachedVehicleEvidenceEvents = attachedVehicleEvidenceEvents == null ? List.of() : List.copyOf(attachedVehicleEvidenceEvents);
|
||||
mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents);
|
||||
vehicleUsageIntervals = vehicleUsageIntervals == null ? List.of() : List.copyOf(vehicleUsageIntervals);
|
||||
vehicleEvidenceDecisions = vehicleEvidenceDecisions == null ? List.of() : List.copyOf(vehicleEvidenceDecisions);
|
||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||
}
|
||||
|
||||
public RuntimeDriverPartitionDebugDto toPartitionDebug() {
|
||||
return new RuntimeDriverPartitionDebugDto(
|
||||
driverKey,
|
||||
directDriverEvents.size(),
|
||||
vehicleUsageIntervalCount,
|
||||
candidateVehicleEvidenceEventCount,
|
||||
attachedVehicleEvidenceEvents.size(),
|
||||
ignoredVehicleEvidenceEventCount,
|
||||
mergedEvents.size(),
|
||||
vehicleUsageIntervals,
|
||||
vehicleEvidenceDecisions,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,9 +36,6 @@ public record UnifiedDriverEventsRequest(
|
|||
}
|
||||
if (sourceFamily == UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION) {
|
||||
Objects.requireNonNull(sessionId, "sessionId must not be null");
|
||||
if (driverKey == null) {
|
||||
throw new IllegalArgumentException("driverKey must not be blank");
|
||||
}
|
||||
} else {
|
||||
if (tenantKey == null) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
|
|
@ -50,6 +47,10 @@ public record UnifiedDriverEventsRequest(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* File-session requests may omit driverKey when the caller intentionally wants
|
||||
* to load all drivers from the session and partition them later in runtime scope processing.
|
||||
*/
|
||||
public static UnifiedDriverEventsRequest forTachographFileSession(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
|
|
|
|||
|
|
@ -2,15 +2,24 @@ package at.procon.eventhub.processing.model;
|
|||
|
||||
import at.procon.eventhub.reference.DriverCardNumberNormalizer;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record UnifiedRuntimeProcessingRequest(
|
||||
UUID sessionId,
|
||||
List<UUID> sessionIds,
|
||||
UUID compositeSessionId,
|
||||
String tenantKey,
|
||||
Set<UnifiedEventSourceFamily> sourceFamilies,
|
||||
UnifiedRuntimeEventBackend eventBackend,
|
||||
String driverKey,
|
||||
Set<String> driverKeys,
|
||||
boolean includeAllDrivers,
|
||||
Set<String> vehicleKeys,
|
||||
boolean includeAllVehicles,
|
||||
String driverSourceEntityId,
|
||||
String driverCardNation,
|
||||
String driverCardNumber,
|
||||
|
|
@ -20,32 +29,64 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
public UnifiedRuntimeProcessingRequest {
|
||||
driverKey = normalize(driverKey);
|
||||
tenantKey = normalize(tenantKey);
|
||||
driverSourceEntityId = normalize(driverSourceEntityId);
|
||||
driverCardNation = normalizeUpper(driverCardNation);
|
||||
driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
|
||||
boolean includesFileSession = sourceFamilies != null && sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
|
||||
boolean includesExternalDb = sourceFamilies != null && sourceFamilies.stream()
|
||||
.anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB);
|
||||
if (tenantKey == null) {
|
||||
if (!includesFileSession || includesExternalDb) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
}
|
||||
}
|
||||
if (sourceFamilies == null || sourceFamilies.isEmpty()) {
|
||||
throw new IllegalArgumentException("sourceFamilies must not be empty");
|
||||
}
|
||||
sourceFamilies = Set.copyOf(sourceFamilies);
|
||||
eventBackend = eventBackend == null ? UnifiedRuntimeEventBackend.SOURCE_DB : eventBackend;
|
||||
if (includesFileSession && sessionId == null) {
|
||||
throw new IllegalArgumentException("sessionId must not be null when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
sessionIds = normalizeSessionIds(sessionId, sessionIds);
|
||||
if (sessionId == null && !sessionIds.isEmpty()) {
|
||||
sessionId = sessionIds.get(0);
|
||||
}
|
||||
if (includesFileSession && driverKey == null) {
|
||||
throw new IllegalArgumentException("driverKey must not be blank when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
if (compositeSessionId != null && !sessionIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("Use either compositeSessionId or sessionId/sessionIds, not both.");
|
||||
}
|
||||
if (includesExternalDb && driverSourceEntityId == null && driverCardNumber == null) {
|
||||
throw new IllegalArgumentException("At least one driver selector must be provided.");
|
||||
driverKey = normalize(driverKey);
|
||||
driverKeys = normalizeStrings(driverKeys);
|
||||
if (driverKey != null) {
|
||||
LinkedHashSet<String> mergedDriverKeys = new LinkedHashSet<>(driverKeys);
|
||||
mergedDriverKeys.add(driverKey);
|
||||
driverKeys = Set.copyOf(mergedDriverKeys);
|
||||
}
|
||||
if (driverKey == null && driverKeys.size() == 1) {
|
||||
driverKey = driverKeys.iterator().next();
|
||||
}
|
||||
vehicleKeys = normalizeStrings(vehicleKeys);
|
||||
tenantKey = normalize(tenantKey);
|
||||
driverSourceEntityId = normalize(driverSourceEntityId);
|
||||
driverCardNation = normalizeUpper(driverCardNation);
|
||||
driverCardNumber = normalizeDriverCardNumber(driverCardNumber);
|
||||
boolean includesFileSession = sourceFamilies.contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION);
|
||||
boolean includesExternalDb = sourceFamilies.stream()
|
||||
.anyMatch(family -> family == UnifiedEventSourceFamily.TACHOGRAPH_DB || family == UnifiedEventSourceFamily.YELLOWFOX_DB);
|
||||
if (includesFileSession && eventBackend == UnifiedRuntimeEventBackend.EVENTHUB_DB) {
|
||||
throw new IllegalArgumentException("TACHOGRAPH_FILE_SESSION runtime processing currently supports SOURCE_DB backend only.");
|
||||
}
|
||||
if (tenantKey == null) {
|
||||
if (!includesFileSession || includesExternalDb) {
|
||||
throw new IllegalArgumentException("tenantKey must not be blank");
|
||||
}
|
||||
}
|
||||
if (includesFileSession && compositeSessionId == null && sessionIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("sessionId, sessionIds or compositeSessionId must be provided when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
}
|
||||
if (includesFileSession && driverKey == null && driverKeys.isEmpty() && !includeAllDrivers) {
|
||||
throw new IllegalArgumentException("driverKey, driverKeys or includeAllDrivers must be provided when TACHOGRAPH_FILE_SESSION is selected.");
|
||||
}
|
||||
if (includesExternalDb && driverSourceEntityId == null && driverCardNumber == null
|
||||
&& driverKeys.isEmpty() && !includeAllDrivers) {
|
||||
throw new IllegalArgumentException("At least one driver selector, driverKeys or includeAllDrivers must be provided.");
|
||||
}
|
||||
if (includesExternalDb && (includeAllDrivers || !driverKeys.isEmpty())
|
||||
&& (occurredFrom == null || occurredTo == null)) {
|
||||
throw new IllegalArgumentException("occurredFrom and occurredTo are required when loading broad external DB runtime scopes.");
|
||||
}
|
||||
if (eventBackend == UnifiedRuntimeEventBackend.EVENTHUB_DB
|
||||
&& includesExternalDb
|
||||
&& driverSourceEntityId == null
|
||||
&& driverCardNumber == null
|
||||
&& (includeAllDrivers || !driverKeys.isEmpty())) {
|
||||
throw new IllegalArgumentException("Broad multi-driver EVENTHUB_DB runtime scopes are not supported yet; provide a concrete EventHub driver selector or use SOURCE_DB.");
|
||||
}
|
||||
if (occurredFrom != null && occurredTo != null && occurredTo.isBefore(occurredFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom");
|
||||
|
|
@ -116,11 +157,17 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
eventBackend,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
|
|
@ -143,11 +190,17 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
UnifiedRuntimeEventBackend.EVENTHUB_DB,
|
||||
null,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
|
|
@ -168,10 +221,76 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
driverKey,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
occurredFrom,
|
||||
occurredTo,
|
||||
expandVehicleEvents,
|
||||
vehicleExpansionPaddingMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeProcessingRequest forTachographFileSessions(
|
||||
List<UUID> sessionIds,
|
||||
String driverKey,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
boolean expandVehicleEvents,
|
||||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
sessionIds,
|
||||
null,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
driverKey,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
occurredFrom,
|
||||
occurredTo,
|
||||
expandVehicleEvents,
|
||||
vehicleExpansionPaddingMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public static UnifiedRuntimeProcessingRequest forTachographCompositeSession(
|
||||
UUID compositeSessionId,
|
||||
String driverKey,
|
||||
OffsetDateTime occurredFrom,
|
||||
OffsetDateTime occurredTo,
|
||||
boolean expandVehicleEvents,
|
||||
int vehicleExpansionPaddingMinutes
|
||||
) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
null,
|
||||
List.of(),
|
||||
compositeSessionId,
|
||||
null,
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
driverKey,
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
|
@ -190,6 +309,58 @@ public record UnifiedRuntimeProcessingRequest(
|
|||
return occurredTo == null ? null : occurredTo.plusMinutes(vehicleExpansionPaddingMinutes);
|
||||
}
|
||||
|
||||
private static List<UUID> normalizeSessionIds(UUID sessionId, List<UUID> sessionIds) {
|
||||
LinkedHashSet<UUID> result = new LinkedHashSet<>();
|
||||
if (sessionId != null) {
|
||||
result.add(sessionId);
|
||||
}
|
||||
if (sessionIds != null) {
|
||||
result.addAll(sessionIds.stream().filter(value -> value != null).toList());
|
||||
}
|
||||
return List.copyOf(new ArrayList<>(result));
|
||||
}
|
||||
|
||||
public UnifiedRuntimeProcessingRequest withDriverKey(String value) {
|
||||
return new UnifiedRuntimeProcessingRequest(
|
||||
sessionId,
|
||||
sessionIds,
|
||||
compositeSessionId,
|
||||
tenantKey,
|
||||
sourceFamilies,
|
||||
eventBackend,
|
||||
value,
|
||||
Set.of(),
|
||||
false,
|
||||
vehicleKeys,
|
||||
includeAllVehicles,
|
||||
driverSourceEntityId,
|
||||
driverCardNation,
|
||||
driverCardNumber,
|
||||
occurredFrom,
|
||||
occurredTo,
|
||||
expandVehicleEvents,
|
||||
vehicleExpansionPaddingMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public boolean scopeDriverSelectionRequested() {
|
||||
return includeAllDrivers || driverKeys.size() > 1 || (driverKey == null && !driverKeys.isEmpty());
|
||||
}
|
||||
|
||||
private static Set<String> normalizeStrings(Set<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||
for (String value : values) {
|
||||
String normalizedValue = normalize(value);
|
||||
if (normalizedValue != null) {
|
||||
normalized.add(normalizedValue);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(normalized);
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,399 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeVehicleEvidenceAttachmentDecisionDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeVehicleUsageIntervalDebugDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeClassifier;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventScopeType;
|
||||
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class RuntimeDriverVehicleEvidenceAttachmentService {
|
||||
|
||||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final RuntimeEventScopeClassifier scopeClassifier;
|
||||
|
||||
public RuntimeDriverVehicleEvidenceAttachmentService(
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
RuntimeEventScopeClassifier scopeClassifier
|
||||
) {
|
||||
this.timelineReconstructor = timelineReconstructor;
|
||||
this.scopeClassifier = scopeClassifier;
|
||||
}
|
||||
|
||||
public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
|
||||
String driverKey,
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> runtimeScopeEvents,
|
||||
boolean attachVehicleOnlyEvents,
|
||||
int vehicleEvidencePaddingMinutes
|
||||
) {
|
||||
return attachVehicleEvidence(
|
||||
driverKey,
|
||||
directDriverEvents,
|
||||
runtimeScopeEvents,
|
||||
attachVehicleOnlyEvents,
|
||||
vehicleEvidencePaddingMinutes,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public RuntimeDriverVehicleEvidenceAttachmentResult attachVehicleEvidence(
|
||||
String driverKey,
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> runtimeScopeEvents,
|
||||
boolean attachVehicleOnlyEvents,
|
||||
int vehicleEvidencePaddingMinutes,
|
||||
boolean includeDebug
|
||||
) {
|
||||
List<EventHubEventDto> safeDriverEvents = directDriverEvents == null ? List.of() : List.copyOf(directDriverEvents);
|
||||
List<EventHubEventDto> safeScopeEvents = runtimeScopeEvents == null ? List.of() : List.copyOf(runtimeScopeEvents);
|
||||
int paddingMinutes = Math.max(0, vehicleEvidencePaddingMinutes);
|
||||
|
||||
List<String> notes = new ArrayList<>();
|
||||
List<String> warnings = new ArrayList<>();
|
||||
if (!attachVehicleOnlyEvents) {
|
||||
notes.add("Vehicle-only evidence attachment is disabled for driver partition " + driverKey + ".");
|
||||
List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions = includeDebug
|
||||
? disabledDecisions(safeScopeEvents)
|
||||
: List.of();
|
||||
return new RuntimeDriverVehicleEvidenceAttachmentResult(
|
||||
driverKey,
|
||||
safeDriverEvents,
|
||||
List.of(),
|
||||
deduplicateAndSort(safeDriverEvents, List.of()),
|
||||
0,
|
||||
disabledDecisions.size(),
|
||||
disabledDecisions.size(),
|
||||
List.of(),
|
||||
disabledDecisions,
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(null, driverKey, safeDriverEvents);
|
||||
List<ResolvedVehicleUsageInterval> usageIntervals = mergeVehicleUsageIntervals(timeline.vehicleUsageIntervals());
|
||||
List<RuntimeVehicleUsageIntervalDebugDto> usageIntervalDebug = includeDebug
|
||||
? usageIntervals.stream().map(RuntimeVehicleUsageIntervalDebugDto::from).toList()
|
||||
: List.of();
|
||||
List<RuntimeVehicleEvidenceAttachmentDecisionDto> decisions = includeDebug
|
||||
? new ArrayList<>(directDriverDecisions(safeDriverEvents))
|
||||
: new ArrayList<>();
|
||||
List<EventHubEventDto> candidateVehicleEvidence = safeScopeEvents.stream()
|
||||
.filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
|
||||
.toList();
|
||||
List<EventHubEventDto> attached = new ArrayList<>();
|
||||
int ignored = 0;
|
||||
for (EventHubEventDto vehicleEvent : candidateVehicleEvidence) {
|
||||
List<ResolvedVehicleUsageInterval> matchingIntervals = matchingUsageIntervals(vehicleEvent, usageIntervals, paddingMinutes);
|
||||
if (matchingIntervals.isEmpty()) {
|
||||
ignored++;
|
||||
if (includeDebug) {
|
||||
decisions.add(decision(
|
||||
"IGNORED_NO_OVERLAPPING_VEHICLE_USAGE",
|
||||
"Vehicle-scoped event did not overlap any reconstructed vehicle-usage interval for driver " + driverKey + ".",
|
||||
vehicleEvent,
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
attached.add(vehicleEvent);
|
||||
if (includeDebug) {
|
||||
decisions.add(decision(
|
||||
"ATTACHED_VEHICLE_EVIDENCE",
|
||||
"Vehicle-scoped event overlapped driver vehicle usage interval(s).",
|
||||
vehicleEvent,
|
||||
matchingIntervals
|
||||
));
|
||||
}
|
||||
if (matchingIntervals.size() > 1) {
|
||||
warnings.add("Vehicle-only event " + vehicleEvent.externalSourceEventId()
|
||||
+ " matched multiple vehicle-usage intervals for driver " + driverKey
|
||||
+ "; it was attached once after deduplication.");
|
||||
}
|
||||
}
|
||||
|
||||
notes.add("Vehicle-only evidence attachment used " + usageIntervals.size()
|
||||
+ " reconstructed vehicle-usage interval(s) for driver " + driverKey + ".");
|
||||
notes.add("Vehicle-only evidence padding minutes: " + paddingMinutes + ".");
|
||||
notes.add("Candidate vehicle-only evidence events: " + candidateVehicleEvidence.size() + ".");
|
||||
notes.add("Attached vehicle-only evidence events: " + attached.size() + ".");
|
||||
notes.add("Ignored vehicle-only evidence events: " + ignored + ".");
|
||||
if (usageIntervals.isEmpty() && !candidateVehicleEvidence.isEmpty()) {
|
||||
warnings.add("Vehicle-only evidence was available for driver " + driverKey
|
||||
+ ", but no driver vehicle-usage intervals were reconstructed; no vehicle-only evidence was attached.");
|
||||
}
|
||||
|
||||
return new RuntimeDriverVehicleEvidenceAttachmentResult(
|
||||
driverKey,
|
||||
safeDriverEvents,
|
||||
attached,
|
||||
deduplicateAndSort(safeDriverEvents, attached),
|
||||
usageIntervals.size(),
|
||||
candidateVehicleEvidence.size(),
|
||||
ignored,
|
||||
usageIntervalDebug,
|
||||
includeDebug ? decisions : List.of(),
|
||||
notes,
|
||||
warnings
|
||||
);
|
||||
}
|
||||
|
||||
private List<RuntimeVehicleEvidenceAttachmentDecisionDto> disabledDecisions(List<EventHubEventDto> runtimeScopeEvents) {
|
||||
return (runtimeScopeEvents == null ? List.<EventHubEventDto>of() : runtimeScopeEvents).stream()
|
||||
.filter(event -> scopeClassifier.classify(event) == RuntimeEventScopeType.VEHICLE_SCOPED)
|
||||
.map(event -> decision(
|
||||
"IGNORED_ATTACHMENT_DISABLED",
|
||||
"Vehicle evidence attachment was disabled for this partition.",
|
||||
event,
|
||||
List.of()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<RuntimeVehicleEvidenceAttachmentDecisionDto> directDriverDecisions(List<EventHubEventDto> directDriverEvents) {
|
||||
return (directDriverEvents == null ? List.<EventHubEventDto>of() : directDriverEvents).stream()
|
||||
.map(event -> decision(
|
||||
"DIRECT_DRIVER_EVENT",
|
||||
"Event already carries the selected driver reference and belongs directly to the driver partition.",
|
||||
event,
|
||||
List.of()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private RuntimeVehicleEvidenceAttachmentDecisionDto decision(
|
||||
String decision,
|
||||
String reason,
|
||||
EventHubEventDto event,
|
||||
List<ResolvedVehicleUsageInterval> matchingIntervals
|
||||
) {
|
||||
List<String> intervalIds = (matchingIntervals == null ? List.<ResolvedVehicleUsageInterval>of() : matchingIntervals).stream()
|
||||
.map(ResolvedVehicleUsageInterval::intervalId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
return new RuntimeVehicleEvidenceAttachmentDecisionDto(
|
||||
decision,
|
||||
reason,
|
||||
dedupKey(event),
|
||||
event == null ? null : event.externalSourceEventId(),
|
||||
event == null ? null : event.occurredAt(),
|
||||
event == null || event.eventDomain() == null ? null : event.eventDomain().name(),
|
||||
event == null || event.eventType() == null ? null : event.eventType().name(),
|
||||
event == null || event.lifecycle() == null ? null : event.lifecycle().name(),
|
||||
scopeClassifier.classify(event),
|
||||
vehicleKeys(event),
|
||||
intervalIds
|
||||
);
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> matchingUsageIntervals(
|
||||
EventHubEventDto vehicleEvent,
|
||||
List<ResolvedVehicleUsageInterval> usageIntervals,
|
||||
int paddingMinutes
|
||||
) {
|
||||
if (vehicleEvent == null || vehicleEvent.occurredAt() == null || usageIntervals == null || usageIntervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
|
||||
for (ResolvedVehicleUsageInterval interval : usageIntervals) {
|
||||
if (matchesVehicle(vehicleEvent, interval) && timeInside(vehicleEvent.occurredAt(), interval, paddingMinutes)) {
|
||||
result.add(interval);
|
||||
}
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private boolean timeInside(OffsetDateTime occurredAt, ResolvedVehicleUsageInterval interval, int paddingMinutes) {
|
||||
if (occurredAt == null || interval == null || interval.from() == null) {
|
||||
return false;
|
||||
}
|
||||
OffsetDateTime from = interval.from().minusMinutes(paddingMinutes);
|
||||
OffsetDateTime to = interval.to() == null ? OffsetDateTime.MAX : interval.to().plusMinutes(paddingMinutes);
|
||||
return !occurredAt.isBefore(from) && !occurredAt.isAfter(to);
|
||||
}
|
||||
|
||||
private boolean matchesVehicle(EventHubEventDto event, ResolvedVehicleUsageInterval interval) {
|
||||
if (event == null || interval == null) {
|
||||
return false;
|
||||
}
|
||||
Set<String> eventKeys = vehicleKeys(event);
|
||||
Set<String> intervalKeys = vehicleKeys(interval);
|
||||
if (eventKeys.isEmpty() || intervalKeys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return eventKeys.stream().anyMatch(intervalKeys::contains);
|
||||
}
|
||||
|
||||
private Set<String> vehicleKeys(EventHubEventDto event) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
JsonNode raw = rawPayload(event);
|
||||
add(result, text(raw, "vehicleKey"));
|
||||
add(result, text(raw, "registrationKey"));
|
||||
VehicleRefDto vehicleRef = event.vehicleRef();
|
||||
if (vehicleRef != null) {
|
||||
add(result, vehicleRef.sourceVehicleEntityId());
|
||||
add(result, vehicleRef.sourceRegistrationEntityId());
|
||||
add(result, vehicleRef.vin());
|
||||
if (vehicleRef.vin() != null) {
|
||||
add(result, "VIN:" + vehicleRef.vin());
|
||||
}
|
||||
if (vehicleRef.vehicleRegistration() != null) {
|
||||
String registrationKey = vehicleRef.vehicleRegistration().stableKey();
|
||||
add(result, registrationKey);
|
||||
add(result, "VR:" + registrationKey);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private Set<String> vehicleKeys(ResolvedVehicleUsageInterval interval) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
add(result, interval.vehicleKey());
|
||||
add(result, interval.registrationKey());
|
||||
if (interval.vehicleKey() != null) {
|
||||
add(result, "VIN:" + interval.vehicleKey());
|
||||
}
|
||||
if (interval.registrationKey() != null) {
|
||||
add(result, "VR:" + interval.registrationKey());
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private void add(Set<String> keys, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
keys.add(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(List<ResolvedVehicleUsageInterval> intervals) {
|
||||
if (intervals == null || intervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ResolvedVehicleUsageInterval> sorted = intervals.stream()
|
||||
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(ResolvedVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
List<ResolvedVehicleUsageInterval> merged = new ArrayList<>();
|
||||
for (ResolvedVehicleUsageInterval next : sorted) {
|
||||
if (merged.isEmpty()) {
|
||||
merged.add(next);
|
||||
continue;
|
||||
}
|
||||
ResolvedVehicleUsageInterval current = merged.get(merged.size() - 1);
|
||||
if (canMerge(current, next)) {
|
||||
merged.set(merged.size() - 1, merge(current, next));
|
||||
} else {
|
||||
merged.add(next);
|
||||
}
|
||||
}
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
private boolean canMerge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
|
||||
if (left == null || right == null || left.to() == null || right.from() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(left.driverKey(), right.driverKey())
|
||||
&& Objects.equals(left.registrationKey(), right.registrationKey())
|
||||
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
|
||||
&& !right.from().isAfter(left.to().plusSeconds(1));
|
||||
}
|
||||
|
||||
private ResolvedVehicleUsageInterval merge(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
|
||||
LinkedHashSet<String> sourceIntervalIds = new LinkedHashSet<>();
|
||||
if (left.sourceIntervalIds() != null) {
|
||||
sourceIntervalIds.addAll(left.sourceIntervalIds());
|
||||
}
|
||||
if (right.sourceIntervalIds() != null) {
|
||||
sourceIntervalIds.addAll(right.sourceIntervalIds());
|
||||
}
|
||||
OffsetDateTime end = left.to();
|
||||
if (right.to() != null && (end == null || right.to().isAfter(end))) {
|
||||
end = right.to();
|
||||
}
|
||||
return ResolvedVehicleUsageInterval.resolved(
|
||||
left.sessionId(),
|
||||
left.driverKey(),
|
||||
left.intervalId(),
|
||||
left.from(),
|
||||
end,
|
||||
left.odometerBeginKm(),
|
||||
right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
|
||||
left.registrationKey(),
|
||||
left.vehicleKey(),
|
||||
left.sourceKind(),
|
||||
List.copyOf(sourceIntervalIds)
|
||||
);
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> deduplicateAndSort(
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> vehicleEvidenceEvents
|
||||
) {
|
||||
LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
|
||||
appendDeduplicated(byKey, directDriverEvents);
|
||||
appendDeduplicated(byKey, vehicleEvidenceEvents);
|
||||
return sort(new ArrayList<>(byKey.values()));
|
||||
}
|
||||
|
||||
private void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
|
||||
for (EventHubEventDto event : events == null ? List.<EventHubEventDto>of() : events) {
|
||||
byKey.putIfAbsent(dedupKey(event), event);
|
||||
}
|
||||
}
|
||||
|
||||
private String dedupKey(EventHubEventDto event) {
|
||||
String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null
|
||||
? event.packageInfo().eventSource().stableKey()
|
||||
: "NO_SOURCE";
|
||||
return sourceKey + "|" + event.externalSourceEventId();
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> sort(List<EventHubEventDto> events) {
|
||||
return (events == null ? List.<EventHubEventDto>of() : events).stream()
|
||||
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
|
||||
.thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
|
||||
.thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
|
||||
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,14 @@ import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
|||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.model.UnifiedVehicleEventsRequest;
|
||||
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
||||
import at.procon.eventhub.service.EventHubEventSorter;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
|
|
@ -15,13 +22,22 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
|
||||
private final UnifiedDriverEventSourceService driverEventSourceService;
|
||||
private final UnifiedVehicleEventSourceService vehicleEventSourceService;
|
||||
private final TachographCompositeSessionRepository compositeSessionRepository;
|
||||
private final EventAcquisitionRecordKeyService eventKeyService;
|
||||
private final EventHubEventSorter eventSorter;
|
||||
|
||||
public TachographFileSessionRuntimeEventLoader(
|
||||
UnifiedDriverEventSourceService driverEventSourceService,
|
||||
UnifiedVehicleEventSourceService vehicleEventSourceService
|
||||
UnifiedVehicleEventSourceService vehicleEventSourceService,
|
||||
TachographCompositeSessionRepository compositeSessionRepository,
|
||||
EventAcquisitionRecordKeyService eventKeyService,
|
||||
EventHubEventSorter eventSorter
|
||||
) {
|
||||
this.driverEventSourceService = driverEventSourceService;
|
||||
this.vehicleEventSourceService = vehicleEventSourceService;
|
||||
this.compositeSessionRepository = compositeSessionRepository;
|
||||
this.eventKeyService = eventKeyService;
|
||||
this.eventSorter = eventSorter;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -32,14 +48,18 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
|
||||
@Override
|
||||
public List<EventHubEventDto> loadDriverEvents(UnifiedRuntimeProcessingRequest request) {
|
||||
return driverEventSourceService.loadDriverEvents(
|
||||
List<EventHubEventDto> result = new ArrayList<>();
|
||||
for (UUID sessionId : resolveSessionIds(request)) {
|
||||
result.addAll(driverEventSourceService.loadDriverEvents(
|
||||
UnifiedDriverEventsRequest.forTachographFileSession(
|
||||
request.sessionId(),
|
||||
sessionId,
|
||||
request.driverKey(),
|
||||
request.occurredFrom(),
|
||||
request.occurredTo()
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
return deduplicateBySignatureAndSort(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -47,9 +67,11 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
UnifiedRuntimeProcessingRequest request,
|
||||
UnifiedDiscoveredVehicleRef vehicleRef
|
||||
) {
|
||||
return vehicleEventSourceService.loadVehicleEvents(
|
||||
List<EventHubEventDto> result = new ArrayList<>();
|
||||
for (UUID sessionId : resolveSessionIds(request)) {
|
||||
result.addAll(vehicleEventSourceService.loadVehicleEvents(
|
||||
UnifiedVehicleEventsRequest.forTachographFileSession(
|
||||
request.sessionId(),
|
||||
sessionId,
|
||||
vehicleRef.sourceVehicleEntityId(),
|
||||
vehicleRef.vin(),
|
||||
vehicleRef.registrationNation(),
|
||||
|
|
@ -57,6 +79,25 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
|||
request.vehicleOccurredFrom(),
|
||||
request.vehicleOccurredTo()
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
return deduplicateBySignatureAndSort(result);
|
||||
}
|
||||
|
||||
private List<UUID> resolveSessionIds(UnifiedRuntimeProcessingRequest request) {
|
||||
if (request.compositeSessionId() != null) {
|
||||
return compositeSessionRepository.find(request.compositeSessionId())
|
||||
.orElseThrow(() -> new TachographCompositeSessionNotFoundException(request.compositeSessionId()))
|
||||
.memberSessionIds();
|
||||
}
|
||||
return request.sessionIds();
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> deduplicateBySignatureAndSort(List<EventHubEventDto> events) {
|
||||
LinkedHashMap<String, EventHubEventDto> bySignature = new LinkedHashMap<>();
|
||||
for (EventHubEventDto event : events) {
|
||||
bySignature.putIfAbsent(eventKeyService.buildEventSignatureHash(event), event);
|
||||
}
|
||||
return eventSorter.sort(new ArrayList<>(bySignature.values()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ public class TachographFileSessionUnifiedDriverEventSource implements UnifiedDri
|
|||
public List<EventHubEventDto> loadDriverEvents(UnifiedDriverEventsRequest request) {
|
||||
TachographFileSession session = repository.find(request.sessionId())
|
||||
.orElseThrow(() -> new TachographFileSessionNotFoundException(request.sessionId()));
|
||||
if (request.driverKey() == null) {
|
||||
return session.driversByKey().values().stream()
|
||||
.flatMap(driver -> eventBuilder.buildEvents(session, driver).stream())
|
||||
.filter(event -> withinWindow(event.occurredAt(), request.occurredFrom(), request.occurredTo()))
|
||||
.toList();
|
||||
}
|
||||
DriverExtractionSession driver = session.driversByKey().get(request.driverKey());
|
||||
if (driver == null) {
|
||||
throw new DriverNotFoundInSessionException(request.sessionId(), request.driverKey());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,494 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult;
|
||||
import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingDerivedProjectionBundle;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
|
||||
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UnifiedRuntimeDerivedProjectionService {
|
||||
|
||||
private final UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService;
|
||||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||
private final DriverWorkingTimeProcessingCore workingTimeProcessingCore;
|
||||
private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionService(
|
||||
UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventHubProperties properties,
|
||||
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
|
||||
) {
|
||||
this(
|
||||
runtimeEventAssemblyService,
|
||||
timelineReconstructor,
|
||||
driverTimelineBuilder,
|
||||
reusableProjectionBuilder,
|
||||
properties,
|
||||
new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)),
|
||||
supportEvidenceNormalizer
|
||||
);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public UnifiedRuntimeDerivedProjectionService(
|
||||
UnifiedRuntimeEventAssemblyService runtimeEventAssemblyService,
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventHubProperties properties,
|
||||
DriverWorkingTimeProcessingCore workingTimeProcessingCore,
|
||||
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
|
||||
) {
|
||||
this.runtimeEventAssemblyService = runtimeEventAssemblyService;
|
||||
this.timelineReconstructor = timelineReconstructor;
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||
this.properties = properties;
|
||||
this.workingTimeProcessingCore = workingTimeProcessingCore;
|
||||
this.supportEvidenceNormalizer = supportEvidenceNormalizer;
|
||||
}
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto loadDriverDerivedProjections(
|
||||
UnifiedRuntimeProcessingApiRequest apiRequest
|
||||
) {
|
||||
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
|
||||
UnifiedRuntimeEventBundle eventBundle = runtimeEventAssemblyService.assembleDriverScopedEvents(request);
|
||||
return buildDriverDerivedProjection(apiRequest, request, eventBundle, null);
|
||||
}
|
||||
|
||||
public UnifiedRuntimeDerivedProjectionResultDto buildDriverDerivedProjection(
|
||||
UnifiedRuntimeProcessingApiRequest apiRequest,
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
UnifiedRuntimeEventBundle eventBundle,
|
||||
String explicitDriverKey
|
||||
) {
|
||||
String driverKey = explicitDriverKey == null
|
||||
? resolveDriverKey(request, eventBundle.mergedEvents())
|
||||
: explicitDriverKey;
|
||||
RuntimeSupportEvidenceNormalizationResult normalizationResult = supportEvidenceNormalizer.normalizeForTachographDriver(
|
||||
driverKey,
|
||||
eventBundle.mergedEvents()
|
||||
);
|
||||
List<EventHubEventDto> normalizedEvents = normalizationResult.normalizedEvents();
|
||||
ResolvedDriverTimeline timeline = timelineReconstructor.reconstruct(
|
||||
runtimeSessionId(request),
|
||||
driverKey,
|
||||
normalizedEvents
|
||||
);
|
||||
|
||||
OffsetDateTime requestedFrom = apiRequest.occurredFrom() == null
|
||||
? timeline.loadedFrom()
|
||||
: utc(apiRequest.occurredFrom());
|
||||
OffsetDateTime requestedTo = apiRequest.occurredTo() == null
|
||||
? timeline.loadedTo()
|
||||
: utc(apiRequest.occurredTo());
|
||||
if (requestedFrom != null && requestedTo != null && requestedTo.isBefore(requestedFrom)) {
|
||||
throw new IllegalArgumentException("occurredTo must not be before occurredFrom.");
|
||||
}
|
||||
|
||||
int significantDrivingMinutes = apiRequest.significantDrivingMinutes() == null
|
||||
? processingProperties().getSignificantDrivingMinutes()
|
||||
: Math.max(1, apiRequest.significantDrivingMinutes());
|
||||
int minimumRestPeriodMinutes = apiRequest.minimumRestPeriodMinutes() == null
|
||||
? processingProperties().getMinimumRestPeriodMinutes()
|
||||
: Math.max(1, apiRequest.minimumRestPeriodMinutes());
|
||||
|
||||
List<String> notes = new ArrayList<>(eventBundle.notes());
|
||||
notes.addAll(normalizationResult.notes());
|
||||
notes.add("Runtime derived projections were evaluated from the unified merged event stream using normalized support evidence and the shared driver working-time processing core.");
|
||||
notes.add("Significant driving threshold minutes: " + significantDrivingMinutes + ".");
|
||||
notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + ".");
|
||||
if (request.occurredFrom() != null || request.occurredTo() != null) {
|
||||
notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request.");
|
||||
}
|
||||
|
||||
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(TachographEsperProcessingInput.fromEvents(
|
||||
runtimeSessionId(request),
|
||||
driverKey,
|
||||
timeline,
|
||||
normalizedEvents,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes,
|
||||
notes
|
||||
));
|
||||
notes = projection.notes();
|
||||
|
||||
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
|
||||
normalizationResult.inputEventCount(),
|
||||
normalizationResult.normalizedSupportEvidenceEventCount(),
|
||||
normalizationResult.unchangedEventCount(),
|
||||
normalizationResult.notes()
|
||||
);
|
||||
|
||||
return new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
request,
|
||||
eventBundle.driverSeedEvents().size(),
|
||||
eventBundle.discoveredVehicles().size(),
|
||||
eventBundle.expandedVehicleEvents().size(),
|
||||
eventBundle.mergedEvents().size(),
|
||||
eventBundle.discoveredVehicles(),
|
||||
projection,
|
||||
notes,
|
||||
normalizationDebug
|
||||
);
|
||||
}
|
||||
|
||||
private EventHubProperties.Processing processingProperties() {
|
||||
return properties.getTachographFileSession().getProcessing();
|
||||
}
|
||||
|
||||
private UUID runtimeSessionId(UnifiedRuntimeProcessingRequest request) {
|
||||
if (request.compositeSessionId() != null || request.sessionIds().size() > 1) {
|
||||
return null;
|
||||
}
|
||||
return request.sessionIds().size() == 1 ? request.sessionIds().get(0) : request.sessionId();
|
||||
}
|
||||
|
||||
private String resolveDriverKey(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
if (request.driverKey() != null) {
|
||||
return request.driverKey();
|
||||
}
|
||||
if (request.driverSourceEntityId() != null) {
|
||||
return request.driverSourceEntityId();
|
||||
}
|
||||
for (EventHubEventDto event : events) {
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
if (driverRef != null && driverRef.hasAnyReference()) {
|
||||
return driverRef.stableKey();
|
||||
}
|
||||
}
|
||||
if (request.driverCardNation() != null && request.driverCardNumber() != null) {
|
||||
return request.driverCardNation() + ":" + request.driverCardNumber();
|
||||
}
|
||||
return request.driverCardNumber();
|
||||
}
|
||||
|
||||
|
||||
private List<TachographEsperVehicleUsageIntervalEvent> mergeVehicleUsageIntervals(
|
||||
List<TachographEsperVehicleUsageIntervalEvent> intervals
|
||||
) {
|
||||
if (intervals == null || intervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<TachographEsperVehicleUsageIntervalEvent> sorted = intervals.stream()
|
||||
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
List<TachographEsperVehicleUsageIntervalEvent> merged = new ArrayList<>();
|
||||
for (TachographEsperVehicleUsageIntervalEvent next : sorted) {
|
||||
if (merged.isEmpty()) {
|
||||
merged.add(next);
|
||||
continue;
|
||||
}
|
||||
TachographEsperVehicleUsageIntervalEvent current = merged.get(merged.size() - 1);
|
||||
if (canMergeVehicleUsage(current, next)) {
|
||||
merged.set(merged.size() - 1, mergeVehicleUsage(current, next));
|
||||
} else {
|
||||
merged.add(next);
|
||||
}
|
||||
}
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
private boolean canMergeVehicleUsage(
|
||||
TachographEsperVehicleUsageIntervalEvent left,
|
||||
TachographEsperVehicleUsageIntervalEvent right
|
||||
) {
|
||||
if (left == null || right == null || left.endedAt() == null || right.startedAt() == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(left.driverKey(), right.driverKey())
|
||||
&& Objects.equals(left.registrationKey(), right.registrationKey())
|
||||
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
|
||||
&& !right.startedAt().isAfter(left.endedAt().plusSeconds(1));
|
||||
}
|
||||
|
||||
private TachographEsperVehicleUsageIntervalEvent mergeVehicleUsage(
|
||||
TachographEsperVehicleUsageIntervalEvent left,
|
||||
TachographEsperVehicleUsageIntervalEvent right
|
||||
) {
|
||||
List<String> sourceIntervalIds = new ArrayList<>();
|
||||
if (left.sourceIntervalIds() != null) {
|
||||
sourceIntervalIds.addAll(left.sourceIntervalIds());
|
||||
}
|
||||
if (right.sourceIntervalIds() != null) {
|
||||
for (String sourceIntervalId : right.sourceIntervalIds()) {
|
||||
if (!sourceIntervalIds.contains(sourceIntervalId)) {
|
||||
sourceIntervalIds.add(sourceIntervalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
OffsetDateTime end = right.endedAt() == null || right.endedAt().isBefore(left.endedAt())
|
||||
? left.endedAt()
|
||||
: right.endedAt();
|
||||
return new TachographEsperVehicleUsageIntervalEvent(
|
||||
left.sessionId(),
|
||||
left.driverKey(),
|
||||
left.intervalId(),
|
||||
left.startedAt(),
|
||||
end,
|
||||
Duration.between(left.startedAt(), end).getSeconds(),
|
||||
left.odometerBeginKm(),
|
||||
right.odometerEndKm() == null ? left.odometerEndKm() : right.odometerEndKm(),
|
||||
left.registrationKey(),
|
||||
left.vehicleKey(),
|
||||
left.sourceKind(),
|
||||
sourceIntervalIds
|
||||
);
|
||||
}
|
||||
|
||||
private List<TachographEsperActivityIntervalEvent> clipActivityIntervals(
|
||||
List<TachographEsperActivityIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return intervals == null ? List.of() : List.copyOf(intervals);
|
||||
}
|
||||
return (intervals == null ? List.<TachographEsperActivityIntervalEvent>of() : intervals).stream()
|
||||
.map(interval -> {
|
||||
if (!intersects(interval.startedAt(), interval.endedAt(), requestedFrom, requestedTo)) {
|
||||
return null;
|
||||
}
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (start == null || end == null || !end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
boolean clipped = interval.clippedToRequestedPeriod()
|
||||
|| !Objects.equals(start, interval.startedAt())
|
||||
|| !Objects.equals(end, interval.endedAt());
|
||||
return new TachographEsperActivityIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
interval.intervalId(),
|
||||
interval.activityType(),
|
||||
interval.cardSlot(),
|
||||
interval.cardStatus(),
|
||||
interval.drivingStatus(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
interval.sourceIntervalIds(),
|
||||
interval.synthetic(),
|
||||
clipped,
|
||||
interval.level()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperActivityIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperActivityIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperVehicleUsageIntervalEvent> clipVehicleUsageIntervals(
|
||||
List<TachographEsperVehicleUsageIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return intervals == null ? List.of() : List.copyOf(intervals);
|
||||
}
|
||||
return (intervals == null ? List.<TachographEsperVehicleUsageIntervalEvent>of() : intervals).stream()
|
||||
.map(interval -> {
|
||||
if (!intersects(interval.startedAt(), interval.endedAt(), requestedFrom, requestedTo)) {
|
||||
return null;
|
||||
}
|
||||
OffsetDateTime start = max(interval.startedAt(), requestedFrom);
|
||||
OffsetDateTime end = min(interval.endedAt(), requestedTo);
|
||||
if (start == null || end == null || !end.isAfter(start)) {
|
||||
return null;
|
||||
}
|
||||
boolean startClipped = !Objects.equals(start, interval.startedAt());
|
||||
boolean endClipped = !Objects.equals(end, interval.endedAt());
|
||||
return new TachographEsperVehicleUsageIntervalEvent(
|
||||
interval.sessionId(),
|
||||
interval.driverKey(),
|
||||
interval.intervalId(),
|
||||
start,
|
||||
end,
|
||||
Duration.between(start, end).getSeconds(),
|
||||
startClipped ? null : interval.odometerBeginKm(),
|
||||
endClipped ? null : interval.odometerEndKm(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
interval.sourceKind(),
|
||||
interval.sourceIntervalIds()
|
||||
);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparing(TachographEsperVehicleUsageIntervalEvent::startedAt)
|
||||
.thenComparing(TachographEsperVehicleUsageIntervalEvent::endedAt))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<TachographEsperDrivingInterruptionIntervalEvent> clipDrivingIntervals(
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
return filterIntersecting(
|
||||
intervals,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
TachographEsperDrivingInterruptionIntervalEvent::startedAt,
|
||||
TachographEsperDrivingInterruptionIntervalEvent::endedAt
|
||||
);
|
||||
}
|
||||
|
||||
private List<TachographEsperVuCardAbsentIntervalEvent> clipVuCardAbsentIntervals(
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
return filterIntersecting(
|
||||
intervals,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
TachographEsperVuCardAbsentIntervalEvent::startedAt,
|
||||
TachographEsperVuCardAbsentIntervalEvent::endedAt
|
||||
);
|
||||
}
|
||||
|
||||
private List<TachographEsperSupportGeoEvent> clipSupportGeoEvents(
|
||||
List<ExtractedSupportEvent> supportEvents,
|
||||
String driverKey,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
return (supportEvents == null ? List.<ExtractedSupportEvent>of() : supportEvents).stream()
|
||||
.filter(event -> event.occurredAt() != null)
|
||||
.filter(event -> driverKey == null || event.driverKey() == null || Objects.equals(driverKey, event.driverKey()))
|
||||
.filter(event -> requestedFrom == null || !event.occurredAt().isBefore(requestedFrom))
|
||||
.filter(event -> requestedTo == null || !event.occurredAt().isAfter(requestedTo))
|
||||
.map(event -> new TachographEsperSupportGeoEvent(
|
||||
event.eventId(),
|
||||
event.driverKey(),
|
||||
event.occurredAt(),
|
||||
event.eventDomain(),
|
||||
event.eventType(),
|
||||
event.eventLifecycle(),
|
||||
event.registrationKey(),
|
||||
event.vehicleKey(),
|
||||
event.country(),
|
||||
event.region(),
|
||||
event.countryFrom(),
|
||||
event.countryTo(),
|
||||
event.operation(),
|
||||
event.latitude(),
|
||||
event.longitude(),
|
||||
event.odometerKm(),
|
||||
event.rawRecordPath()
|
||||
))
|
||||
.sorted(Comparator.comparing(TachographEsperSupportGeoEvent::occurredAt)
|
||||
.thenComparing(TachographEsperSupportGeoEvent::eventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private <T> List<T> filterIntersecting(
|
||||
List<T> intervals,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo,
|
||||
TimeAccessor<T> startAccessor,
|
||||
TimeAccessor<T> endAccessor
|
||||
) {
|
||||
if (intervals == null || intervals.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (requestedFrom == null || requestedTo == null) {
|
||||
return List.copyOf(intervals);
|
||||
}
|
||||
return intervals.stream()
|
||||
.filter(interval -> intersects(startAccessor.get(interval), endAccessor.get(interval), requestedFrom, requestedTo))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private boolean intersects(
|
||||
OffsetDateTime intervalStart,
|
||||
OffsetDateTime intervalEnd,
|
||||
OffsetDateTime requestedFrom,
|
||||
OffsetDateTime requestedTo
|
||||
) {
|
||||
if (intervalStart == null || intervalEnd == null || requestedFrom == null || requestedTo == null) {
|
||||
return false;
|
||||
}
|
||||
return intervalEnd.isAfter(requestedFrom) && intervalStart.isBefore(requestedTo);
|
||||
}
|
||||
|
||||
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isBefore(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isAfter(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime utc(OffsetDateTime value) {
|
||||
return value == null ? null : value.withOffsetSameInstant(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TimeAccessor<T> {
|
||||
OffsetDateTime get(T value);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,15 @@ public class UnifiedRuntimeEventAssemblyService {
|
|||
notes.add(request.eventBackend() == UnifiedRuntimeEventBackend.EVENTHUB_DB
|
||||
? "Driver seed events were loaded from the local EventHub event store."
|
||||
: "Driver seed events were loaded directly from the selected runtime sources.");
|
||||
if (request.sourceFamilies().contains(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION)) {
|
||||
if (request.compositeSessionId() != null) {
|
||||
notes.add("Tachograph file-session events were loaded from composite session " + request.compositeSessionId() + ".");
|
||||
} else if (request.sessionIds().size() > 1) {
|
||||
notes.add("Tachograph file-session events were loaded from " + request.sessionIds().size() + " selected sessions.");
|
||||
} else if (request.sessionId() != null) {
|
||||
notes.add("Tachograph file-session events were loaded from session " + request.sessionId() + ".");
|
||||
}
|
||||
}
|
||||
if (request.expandVehicleEvents()) {
|
||||
notes.add("Vehicle expansion loaded additional events for vehicles discovered in the driver seed set.");
|
||||
notes.add("Vehicle expansion padding minutes: " + request.vehicleExpansionPaddingMinutes() + ".");
|
||||
|
|
@ -133,10 +142,10 @@ public class UnifiedRuntimeEventAssemblyService {
|
|||
appendDeduplicated(byKey, right);
|
||||
return byKey.values().stream()
|
||||
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(event -> event.eventDomain().name())
|
||||
.thenComparing(event -> event.eventType().name())
|
||||
.thenComparing(event -> event.lifecycle().name())
|
||||
.thenComparing(EventHubEventDto::externalSourceEventId))
|
||||
.thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
|
||||
.thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
|
||||
.thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
|
||||
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package at.procon.eventhub.processing.service;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
|
||||
/**
|
||||
* @deprecated Compatibility adapter. Use {@link RuntimeDriverWorkingTimeScopeProcessingService};
|
||||
* tachograph files/databases are only one possible source for the common driver working-time runtime plan.
|
||||
*/
|
||||
@Deprecated(forRemoval = false)
|
||||
public class UnifiedRuntimeTachographEsperScopeProcessingService {
|
||||
|
||||
private final RuntimeDriverWorkingTimeScopeProcessingService delegate;
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeProcessingService(
|
||||
RuntimeDriverWorkingTimeScopeProcessingService delegate
|
||||
) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
|
||||
return toLegacy(delegate.processScope(apiRequest));
|
||||
}
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto processScope(
|
||||
UnifiedRuntimeProcessingApiRequest apiRequest,
|
||||
boolean includePartitionDebug
|
||||
) {
|
||||
return toLegacy(delegate.processScope(apiRequest, includePartitionDebug));
|
||||
}
|
||||
|
||||
private UnifiedRuntimeTachographEsperScopeResultDto toLegacy(
|
||||
UnifiedRuntimeDriverWorkingTimeScopeResultDto result
|
||||
) {
|
||||
return new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
result.request(),
|
||||
result.inputEventCount(),
|
||||
result.selectedDriverCount(),
|
||||
result.discoveredVehicleCount(),
|
||||
result.discoveredVehicles(),
|
||||
result.driverResults(),
|
||||
result.partitionDebugByDriver(),
|
||||
result.notes(),
|
||||
result.warnings()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ public final class TachographNationRegistry {
|
|||
continue;
|
||||
}
|
||||
String[] parts = line.split(";", -1);
|
||||
if (parts.length < 5) {
|
||||
if (parts.length < 4) {
|
||||
continue;
|
||||
}
|
||||
Integer numericCode = parseInteger(parts[3]);
|
||||
|
|
@ -43,7 +43,7 @@ public final class TachographNationRegistry {
|
|||
}
|
||||
String name = normalizeNullable(parts[1]);
|
||||
String alphaCode = normalizeAlpha(parts[2]);
|
||||
String defaultLanguageCode = normalizeNullable(parts[4]);
|
||||
String defaultLanguageCode = parts.length > 4 ? normalizeNullable(parts[4]) : null;
|
||||
NationRecord record = new NationRecord(numericCode, alphaCode, name, defaultLanguageCode, true);
|
||||
byNumeric.put(numericCode, record);
|
||||
if (alphaCode != null) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -44,6 +43,8 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
|||
CardSlot cardSlot = cardSlot(rs);
|
||||
CardStatus cardStatus = cardStatus(rs);
|
||||
DrivingStatus drivingStatus = drivingStatus(rs);
|
||||
EventType eventType = eventType(rs);
|
||||
EventLifecycle lifecycle = lifecycle(rs);
|
||||
|
||||
String externalSourceEventId = string(rs, "external_source_event_id");
|
||||
if (externalSourceEventId == null) {
|
||||
|
|
@ -59,13 +60,13 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
|||
offsetDateTime(rs, "received_partner_at"),
|
||||
OffsetDateTime.now(),
|
||||
EventDomain.DRIVER_ACTIVITY,
|
||||
eventType(rs),
|
||||
lifecycle(rs),
|
||||
eventType,
|
||||
lifecycle,
|
||||
longValue(rs, "odometer_m"),
|
||||
null,
|
||||
detailsFactory.driverActivity(cardSlot, cardStatus, drivingStatus),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs, context)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle, cardSlot, cardStatus, drivingStatus)),
|
||||
isManualEntry(cardStatus, drivingStatus),
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -115,9 +116,24 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
|||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs, ExtractionContext<TachographImportRequest> context) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventType eventType,
|
||||
EventLifecycle lifecycle,
|
||||
CardSlot cardSlot,
|
||||
CardStatus cardStatus,
|
||||
DrivingStatus drivingStatus
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, eventType, lifecycle
|
||||
);
|
||||
put(raw, "activityType", eventType == null ? null : eventType.name());
|
||||
put(raw, "slot", cardSlot == null ? null : cardSlot.name());
|
||||
put(raw, "cardStatus", cardStatus == null ? null : cardStatus.name());
|
||||
put(raw, "drivingStatus", drivingStatus == null ? null : drivingStatus.name());
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -60,7 +59,7 @@ abstract class AbstractTachographBorderCrossingRowMapper implements ExtractionRo
|
|||
position(rs),
|
||||
detailsFactory.borderCrossing(string(rs, "country_from"), string(rs, "country_to")),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef)),
|
||||
false,
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -112,9 +111,18 @@ abstract class AbstractTachographBorderCrossingRowMapper implements ExtractionRo
|
|||
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, EventType.BORDER_OUTBOUND, EventLifecycle.OUTBOUND
|
||||
);
|
||||
put(raw, "supportEventType", EventType.BORDER_OUTBOUND.name());
|
||||
put(raw, "countryFrom", string(rs, "country_from"));
|
||||
put(raw, "countryTo", string(rs, "country_to"));
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -44,6 +43,7 @@ abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapp
|
|||
EventType eventType = eventType(rs);
|
||||
EventLifecycle lifecycle = lifecycle(rs);
|
||||
CardStatus cardStatus = cardStatus(lifecycle);
|
||||
CardSlot cardSlot = cardSlot(rs);
|
||||
|
||||
String externalSourceEventId = string(rs, "external_source_event_id");
|
||||
if (externalSourceEventId == null) {
|
||||
|
|
@ -63,9 +63,9 @@ abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapp
|
|||
lifecycle,
|
||||
longValue(rs, "odometer_m"),
|
||||
null,
|
||||
detailsFactory.driverCard(cardSlot(rs), cardStatus, driverCard),
|
||||
detailsFactory.driverCard(cardSlot, cardStatus, driverCard),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle, cardStatus, cardSlot)),
|
||||
false,
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -111,9 +111,22 @@ abstract class AbstractTachographCardEventRowMapper implements ExtractionRowMapp
|
|||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventType eventType,
|
||||
EventLifecycle lifecycle,
|
||||
CardStatus cardStatus,
|
||||
CardSlot cardSlot
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, eventType, lifecycle
|
||||
);
|
||||
put(raw, "supportEventType", eventType == null ? null : eventType.name());
|
||||
put(raw, "slot", cardSlot == null ? null : cardSlot.name());
|
||||
put(raw, "cardStatus", cardStatus == null ? null : cardStatus.name());
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -62,7 +61,7 @@ abstract class AbstractTachographLoadUnloadRowMapper implements ExtractionRowMap
|
|||
position(rs),
|
||||
detailsFactory.loadUnload(string(rs, "operation")),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType)),
|
||||
false,
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -114,9 +113,18 @@ abstract class AbstractTachographLoadUnloadRowMapper implements ExtractionRowMap
|
|||
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventType eventType
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, eventType, EventLifecycle.SNAPSHOT
|
||||
);
|
||||
put(raw, "supportEventType", eventType == null ? null : eventType.name());
|
||||
put(raw, "operation", string(rs, "operation"));
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -63,7 +62,7 @@ abstract class AbstractTachographPlaceRowMapper implements ExtractionRowMapper<T
|
|||
position(rs),
|
||||
detailsFactory.place(string(rs, "country"), string(rs, "region")),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle)),
|
||||
booleanValue(rs, "manual_entry"),
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -115,9 +114,20 @@ abstract class AbstractTachographPlaceRowMapper implements ExtractionRowMapper<T
|
|||
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventType eventType,
|
||||
EventLifecycle lifecycle
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, eventType, lifecycle
|
||||
);
|
||||
put(raw, "supportEventType", eventType == null ? null : eventType.name());
|
||||
put(raw, "country", string(rs, "country"));
|
||||
put(raw, "region", string(rs, "region"));
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -60,7 +59,7 @@ abstract class AbstractTachographPositionRowMapper implements ExtractionRowMappe
|
|||
position(rs),
|
||||
detailsFactory.position("GNSS_ACCUMULATED_DRIVING"),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef)),
|
||||
false,
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -112,9 +111,16 @@ abstract class AbstractTachographPositionRowMapper implements ExtractionRowMappe
|
|||
return latitude == null || longitude == null ? null : new GeoPointDto(latitude, longitude);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, EventType.POSITION_RECORDED, EventLifecycle.SNAPSHOT
|
||||
);
|
||||
put(raw, "supportEventType", EventType.POSITION_RECORDED.name());
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -62,7 +61,7 @@ abstract class AbstractTachographSpecificConditionRowMapper implements Extractio
|
|||
null,
|
||||
detailsFactory.specificCondition(),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, eventType, lifecycle)),
|
||||
false,
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -108,9 +107,18 @@ abstract class AbstractTachographSpecificConditionRowMapper implements Extractio
|
|||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventType eventType,
|
||||
EventLifecycle lifecycle
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, eventType, lifecycle
|
||||
);
|
||||
put(raw, "supportEventType", eventType == null ? null : eventType.name());
|
||||
raw.putAll(sourceSpecificPayload(rs));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import java.sql.Timestamp;
|
|||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
|
@ -63,7 +62,7 @@ public class SpeedingEventRowMapper implements ExtractionRowMapper<TachographImp
|
|||
null,
|
||||
detailsFactory.speeding(decimal(rs, "avg_speed_kmh"), decimal(rs, "max_speed_kmh"), null),
|
||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||
detailsFactory.payloadFromMap(payload(rs)),
|
||||
detailsFactory.payloadFromMap(payload(rs, context, driverRef, vehicleRef, lifecycle)),
|
||||
false,
|
||||
context.packageInfo()
|
||||
);
|
||||
|
|
@ -105,9 +104,19 @@ public class SpeedingEventRowMapper implements ExtractionRowMapper<TachographImp
|
|||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> payload(ResultSet rs) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||
private Map<String, Object> payload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventLifecycle lifecycle
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = TachographRawPayloadSupport.baseRawPayload(
|
||||
rs, context, driverRef, vehicleRef, EventType.SPEEDING, lifecycle
|
||||
);
|
||||
put(raw, "supportEventType", EventType.SPEEDING.name());
|
||||
put(raw, "avgSpeedKmh", decimal(rs, "avg_speed_kmh"));
|
||||
put(raw, "maxSpeedKmh", decimal(rs, "max_speed_kmh"));
|
||||
return Map.of("raw", raw);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
package at.procon.eventhub.tachograph.service;
|
||||
|
||||
import at.procon.eventhub.dto.DriverCardRefDto;
|
||||
import at.procon.eventhub.dto.DriverRefDto;
|
||||
import at.procon.eventhub.dto.EventLifecycle;
|
||||
import at.procon.eventhub.dto.EventType;
|
||||
import at.procon.eventhub.dto.VehicleRefDto;
|
||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||
import at.procon.eventhub.importing.extraction.ExtractionContext;
|
||||
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class TachographRawPayloadSupport {
|
||||
|
||||
private TachographRawPayloadSupport() {
|
||||
}
|
||||
|
||||
static Map<String, Object> baseRawPayload(
|
||||
ResultSet rs,
|
||||
ExtractionContext<TachographImportRequest> context,
|
||||
DriverRefDto driverRef,
|
||||
VehicleRefDto vehicleRef,
|
||||
EventType eventType,
|
||||
EventLifecycle lifecycle
|
||||
) throws SQLException {
|
||||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
String sourceRowId = string(rs, "source_row_id");
|
||||
put(raw, "sourceRowId", sourceRowId);
|
||||
if (sourceRowId != null) {
|
||||
put(raw, "sourceRowIds", List.of(sourceRowId));
|
||||
put(raw, "intervalId", intervalId(context, sourceRowId));
|
||||
}
|
||||
put(raw, "sourceKind", context == null || context.planItem() == null ? null : context.planItem().sourceKind());
|
||||
put(raw, "extractionCode", context == null || context.planItem() == null ? null : context.planItem().extractionCode());
|
||||
put(raw, "level", "RAW_INTERVAL");
|
||||
put(raw, "eventType", eventType == null ? null : eventType.name());
|
||||
put(raw, "lifecycle", lifecycle == null ? null : lifecycle.name());
|
||||
|
||||
enrichDriver(raw, driverRef);
|
||||
enrichVehicle(raw, vehicleRef);
|
||||
enrichSourcePackage(raw, rs);
|
||||
enrichOdometer(raw, rs);
|
||||
return raw;
|
||||
}
|
||||
|
||||
static void put(Map<String, Object> target, String key, Object value) {
|
||||
if (value != null) {
|
||||
target.put(key, value instanceof Enum<?> enumValue ? enumValue.name() : value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String intervalId(ExtractionContext<TachographImportRequest> context, String sourceRowId) {
|
||||
String extractionCode = context == null || context.planItem() == null ? null : context.planItem().extractionCode();
|
||||
return "TACHOGRAPH:" + (extractionCode == null || extractionCode.isBlank() ? "UNKNOWN" : extractionCode) + ":" + sourceRowId;
|
||||
}
|
||||
|
||||
private static void enrichDriver(Map<String, Object> raw, DriverRefDto driverRef) {
|
||||
if (driverRef == null || !driverRef.hasAnyReference()) {
|
||||
return;
|
||||
}
|
||||
put(raw, "driverKey", driverRef.stableKey());
|
||||
put(raw, "driverSourceEntityId", driverRef.sourceEntityId());
|
||||
DriverCardRefDto driverCard = driverRef.driverCard();
|
||||
if (driverCard != null && driverCard.hasValue()) {
|
||||
put(raw, "driverCardKey", driverCard.stableKey());
|
||||
put(raw, "driverCardNation", driverCard.nation());
|
||||
put(raw, "driverCardNationNumericCode", driverCard.nationNumericCode());
|
||||
put(raw, "driverCardNumber", driverCard.number());
|
||||
}
|
||||
}
|
||||
|
||||
private static void enrichVehicle(Map<String, Object> raw, VehicleRefDto vehicleRef) {
|
||||
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
|
||||
return;
|
||||
}
|
||||
put(raw, "vehicleKey", vehicleRef.stableKey());
|
||||
put(raw, "vehicleSourceEntityId", vehicleRef.sourceVehicleEntityId());
|
||||
put(raw, "vehicleVin", vehicleRef.vin());
|
||||
put(raw, "vehicleRegistrationSourceEntityId", vehicleRef.sourceRegistrationEntityId());
|
||||
VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration();
|
||||
if (registration != null && registration.hasValue()) {
|
||||
put(raw, "registrationKey", registration.stableKey());
|
||||
put(raw, "vehicleRegistrationNation", registration.nation());
|
||||
put(raw, "vehicleRegistrationNationNumericCode", registration.nationNumericCode());
|
||||
put(raw, "vehicleRegistrationNumber", registration.number());
|
||||
}
|
||||
}
|
||||
|
||||
private static void enrichSourcePackage(Map<String, Object> raw, ResultSet rs) throws SQLException {
|
||||
put(raw, "sourcePackageKind", string(rs, "source_package_kind"));
|
||||
put(raw, "sourcePackageId", string(rs, "source_package_id"));
|
||||
put(raw, "sourcePackageEntityId", string(rs, "source_package_entity_id"));
|
||||
}
|
||||
|
||||
private static void enrichOdometer(Map<String, Object> raw, ResultSet rs) throws SQLException {
|
||||
Long odometerM = longValue(rs, "odometer_m");
|
||||
put(raw, "odometerM", odometerM);
|
||||
if (odometerM != null) {
|
||||
put(raw, "odometerKm", odometerM / 1_000L);
|
||||
}
|
||||
}
|
||||
|
||||
private static String string(ResultSet rs, String column) throws SQLException {
|
||||
try {
|
||||
String value = rs.getString(column);
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
} catch (SQLException ex) {
|
||||
if (missingColumn(ex)) {
|
||||
return null;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private static Long longValue(ResultSet rs, String column) throws SQLException {
|
||||
Object value;
|
||||
try {
|
||||
value = rs.getObject(column);
|
||||
} catch (SQLException ex) {
|
||||
if (missingColumn(ex)) {
|
||||
return null;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
return Long.parseLong(value.toString());
|
||||
}
|
||||
|
||||
private static boolean missingColumn(SQLException ex) {
|
||||
String state = ex.getSQLState();
|
||||
String message = ex.getMessage();
|
||||
return "S0022".equals(state)
|
||||
|| "42703".equals(state)
|
||||
|| (message != null && message.toLowerCase(java.util.Locale.ROOT).contains("column")
|
||||
&& (message.toLowerCase(java.util.Locale.ROOT).contains("not found")
|
||||
|| message.toLowerCase(java.util.Locale.ROOT).contains("not valid")
|
||||
|| message.toLowerCase(java.util.Locale.ROOT).contains("invalid")));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package at.procon.eventhub.tachographfilesession.dto;
|
||||
|
||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent;
|
||||
|
|
@ -13,6 +14,7 @@ import java.time.OffsetDateTime;
|
|||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Deprecated(forRemoval = false)
|
||||
public record TachographEsperDriverProcessingResultDto(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
|
|
@ -49,4 +51,86 @@ public record TachographEsperDriverProcessingResultDto(
|
|||
List<TachographEsperSupportGeoEvent> supportGeoEvents,
|
||||
List<String> notes
|
||||
) {
|
||||
public static TachographEsperDriverProcessingResultDto fromDriverWorkingTime(
|
||||
DriverWorkingTimeProcessingResultDto result
|
||||
) {
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
return new TachographEsperDriverProcessingResultDto(
|
||||
result.sessionId(),
|
||||
result.driverKey(),
|
||||
result.sourceKind(),
|
||||
result.loadedFrom(),
|
||||
result.loadedTo(),
|
||||
result.requestedFrom(),
|
||||
result.requestedTo(),
|
||||
result.activityIntervalCount(),
|
||||
result.drivingIntervalCount(),
|
||||
result.drivingInterruptionIntervalCount(),
|
||||
result.drivingInterruptionVehicleChangeIntervalCount(),
|
||||
result.dailyWeeklyRestCandidateIntervalCount(),
|
||||
result.dailyWeeklyRestCandidateCoverageIntervalCount(),
|
||||
result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(),
|
||||
result.potentialHomeOvernightStayIntervalCount(),
|
||||
result.potentialInVehicleOvernightStayIntervalCount(),
|
||||
result.potentialInVehicleTripIntervalCount(),
|
||||
result.vehicleUsageIntervalCount(),
|
||||
result.vuCardAbsentIntervalCount(),
|
||||
result.supportGeoEventCount(),
|
||||
result.activityIntervals(),
|
||||
result.drivingIntervals(),
|
||||
result.drivingInterruptionIntervals(),
|
||||
result.drivingInterruptionVehicleChangeIntervals(),
|
||||
result.dailyWeeklyRestCandidateIntervals(),
|
||||
result.dailyWeeklyRestCandidateCoverageIntervals(),
|
||||
result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(),
|
||||
result.potentialHomeOvernightStayIntervals(),
|
||||
result.potentialInVehicleOvernightStayIntervals(),
|
||||
result.potentialInVehicleTripIntervals(),
|
||||
result.vehicleUsageIntervals(),
|
||||
result.vuCardAbsentIntervals(),
|
||||
result.supportGeoEvents(),
|
||||
result.notes()
|
||||
);
|
||||
}
|
||||
|
||||
public DriverWorkingTimeProcessingResultDto toDriverWorkingTime() {
|
||||
return new DriverWorkingTimeProcessingResultDto(
|
||||
sessionId,
|
||||
driverKey,
|
||||
sourceKind,
|
||||
loadedFrom,
|
||||
loadedTo,
|
||||
requestedFrom,
|
||||
requestedTo,
|
||||
activityIntervalCount,
|
||||
drivingIntervalCount,
|
||||
drivingInterruptionIntervalCount,
|
||||
drivingInterruptionVehicleChangeIntervalCount,
|
||||
dailyWeeklyRestCandidateIntervalCount,
|
||||
dailyWeeklyRestCandidateCoverageIntervalCount,
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount,
|
||||
potentialHomeOvernightStayIntervalCount,
|
||||
potentialInVehicleOvernightStayIntervalCount,
|
||||
potentialInVehicleTripIntervalCount,
|
||||
vehicleUsageIntervalCount,
|
||||
vuCardAbsentIntervalCount,
|
||||
supportGeoEventCount,
|
||||
activityIntervals,
|
||||
drivingIntervals,
|
||||
drivingInterruptionIntervals,
|
||||
drivingInterruptionVehicleChangeIntervals,
|
||||
dailyWeeklyRestCandidateIntervals,
|
||||
dailyWeeklyRestCandidateCoverageIntervals,
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervals,
|
||||
potentialHomeOvernightStayIntervals,
|
||||
potentialInVehicleOvernightStayIntervals,
|
||||
potentialInVehicleTripIntervals,
|
||||
vehicleUsageIntervals,
|
||||
vuCardAbsentIntervals,
|
||||
supportGeoEvents,
|
||||
notes
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,19 @@ public interface DriverTimelineEventBuilder {
|
|||
ResolvedDriverTimeline timeline
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds the rawest point-event representation supported by the implementation.
|
||||
*
|
||||
* <p>The default preserves the existing interval-backed behavior. Implementations that can
|
||||
* emit point events directly from source records should override this method.</p>
|
||||
*/
|
||||
default TachographTimelineEventBundle buildRawEventBundle(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession
|
||||
) {
|
||||
return buildEventBundle(session, driverSession);
|
||||
}
|
||||
|
||||
default List<EventHubEventDto> buildEvents(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ import com.espertech.esper.runtime.client.EPDeployException;
|
|||
import com.espertech.esper.runtime.client.EPDeployment;
|
||||
import com.espertech.esper.runtime.client.EPRuntime;
|
||||
import com.espertech.esper.runtime.client.EPRuntimeProvider;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.dto.EventDomain;
|
||||
import at.procon.eventhub.dto.EventHubEventDto;
|
||||
import at.procon.eventhub.dto.EventLifecycle;
|
||||
import at.procon.eventhub.dto.EventType;
|
||||
import at.procon.eventhub.processing.service.UnifiedEventTimelineReconstructor;
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
|
||||
|
|
@ -39,6 +45,7 @@ import java.util.Objects;
|
|||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
|
@ -48,16 +55,42 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
|
||||
private static final AtomicLong RUNTIME_COUNTER = new AtomicLong();
|
||||
private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE =
|
||||
loadResource("esper/tachograph-driving-derived-projection-bundle.epl");
|
||||
loadResource("esper/driver-working-time-derived-projections.epl");
|
||||
private static final String DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL =
|
||||
loadResource("esper/runtime-driver-event-interval-preprocessor.epl");
|
||||
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder;
|
||||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
public DriverTimelineReusableProjectionBuilder(
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this(driverTimelineBuilder, null, new UnifiedEventTimelineReconstructor(), properties);
|
||||
}
|
||||
|
||||
public DriverTimelineReusableProjectionBuilder(
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
RawSourceDriverTimelineEventBuilder rawSourceEventBuilder,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this(driverTimelineBuilder, rawSourceEventBuilder, new UnifiedEventTimelineReconstructor(), properties);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public DriverTimelineReusableProjectionBuilder(
|
||||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
RawSourceDriverTimelineEventBuilder rawSourceEventBuilder,
|
||||
UnifiedEventTimelineReconstructor timelineReconstructor,
|
||||
EventHubProperties properties
|
||||
) {
|
||||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.rawSourceEventBuilder = rawSourceEventBuilder;
|
||||
this.timelineReconstructor = timelineReconstructor == null
|
||||
? new UnifiedEventTimelineReconstructor()
|
||||
: timelineReconstructor;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +100,18 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
if (session == null || driverSession == null) {
|
||||
return emptyBundle();
|
||||
}
|
||||
if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) {
|
||||
return buildEsperDrivingDerivedProjectionBundleFromEvents(
|
||||
session.sessionId(),
|
||||
driverSession.driverKey(),
|
||||
rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents(),
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
ResolvedDriverTimeline timeline = driverTimelineBuilder.build(session, driverSession);
|
||||
return buildEsperDrivingDerivedProjectionBundle(
|
||||
session.sessionId(),
|
||||
|
|
@ -86,6 +131,41 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
return emptyBundle();
|
||||
}
|
||||
|
||||
if (drivingDerivedProjectionInputMode() == EventHubProperties.DrivingDerivedProjectionInputMode.EVENTS) {
|
||||
List<Map<String, Object>> activityInputEvents = new ArrayList<>();
|
||||
List<Map<String, Object>> vehicleUsageInputEvents = new ArrayList<>();
|
||||
List<Map<String, Object>> supportGeoInputEvents = new ArrayList<>();
|
||||
|
||||
for (DriverExtractionSession driverSession : session.driversByKey().values()) {
|
||||
if (driverSession == null || driverSession.driverKey() == null) {
|
||||
continue;
|
||||
}
|
||||
ResolvedDriverTimeline timeline = reconstructMergedTimelineFromEvents(
|
||||
session.sessionId(),
|
||||
driverSession.driverKey(),
|
||||
rawSourceEventBuilder().buildRawEventBundle(session, driverSession).allEvents()
|
||||
);
|
||||
if (timeline == null) {
|
||||
continue;
|
||||
}
|
||||
activityInputEvents.addAll(buildActivityIntervalInputEvents(
|
||||
session.sessionId(),
|
||||
driverSession.driverKey(),
|
||||
timeline.activityIntervals()
|
||||
));
|
||||
vehicleUsageInputEvents.addAll(buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals()));
|
||||
supportGeoInputEvents.addAll(buildSupportGeoInputEvents(session.sessionId(), timeline.supportEvents()));
|
||||
}
|
||||
|
||||
return buildEsperDrivingDerivedProjectionBundle(
|
||||
activityInputEvents,
|
||||
vehicleUsageInputEvents,
|
||||
supportGeoInputEvents,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> activityInputEvents = new ArrayList<>();
|
||||
List<Map<String, Object>> vehicleUsageInputEvents = new ArrayList<>();
|
||||
List<Map<String, Object>> supportGeoInputEvents = new ArrayList<>();
|
||||
|
|
@ -234,6 +314,353 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
);
|
||||
}
|
||||
|
||||
public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents(
|
||||
List<EventHubEventDto> events,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
return buildEsperDrivingDerivedProjectionBundleFromEvents(
|
||||
null,
|
||||
null,
|
||||
events,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
public TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromEvents(
|
||||
UUID fallbackSessionId,
|
||||
String fallbackDriverKey,
|
||||
List<EventHubEventDto> events,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
if (fallbackDriverKey == null) {
|
||||
Map<String, List<EventHubEventDto>> eventsByDriver = groupEventsByDriverKey(events);
|
||||
if (eventsByDriver.size() > 1) {
|
||||
return buildEsperDrivingDerivedProjectionBundleFromGroupedEvents(
|
||||
fallbackSessionId,
|
||||
eventsByDriver,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
if (eventsByDriver.size() == 1) {
|
||||
Map.Entry<String, List<EventHubEventDto>> onlyDriver = eventsByDriver.entrySet().iterator().next();
|
||||
fallbackDriverKey = onlyDriver.getKey();
|
||||
events = onlyDriver.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
ResolvedDriverTimeline reconstructedTimeline = reconstructMergedTimelineFromEvents(
|
||||
fallbackSessionId,
|
||||
fallbackDriverKey,
|
||||
events
|
||||
);
|
||||
return buildEsperDrivingDerivedProjectionBundle(
|
||||
fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId,
|
||||
fallbackDriverKey,
|
||||
reconstructedTimeline,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromGroupedEvents(
|
||||
UUID fallbackSessionId,
|
||||
Map<String, List<EventHubEventDto>> eventsByDriver,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
List<Map<String, Object>> activityInputEvents = new ArrayList<>();
|
||||
List<Map<String, Object>> vehicleUsageInputEvents = new ArrayList<>();
|
||||
List<Map<String, Object>> supportGeoInputEvents = new ArrayList<>();
|
||||
UUID sessionId = fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId;
|
||||
|
||||
for (Map.Entry<String, List<EventHubEventDto>> entry : eventsByDriver.entrySet()) {
|
||||
String driverKey = entry.getKey();
|
||||
if (driverKey == null || driverKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
ResolvedDriverTimeline timeline = reconstructMergedTimelineFromEvents(
|
||||
sessionId,
|
||||
driverKey,
|
||||
entry.getValue()
|
||||
);
|
||||
if (timeline == null) {
|
||||
continue;
|
||||
}
|
||||
activityInputEvents.addAll(buildActivityIntervalInputEvents(
|
||||
sessionId,
|
||||
driverKey,
|
||||
timeline.activityIntervals()
|
||||
));
|
||||
vehicleUsageInputEvents.addAll(buildVehicleUsageIntervalInputEvents(timeline.vehicleUsageIntervals()));
|
||||
supportGeoInputEvents.addAll(buildSupportGeoInputEvents(sessionId, timeline.supportEvents()));
|
||||
}
|
||||
|
||||
return buildEsperDrivingDerivedProjectionBundle(
|
||||
activityInputEvents,
|
||||
vehicleUsageInputEvents,
|
||||
supportGeoInputEvents,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, List<EventHubEventDto>> groupEventsByDriverKey(List<EventHubEventDto> events) {
|
||||
Map<String, List<EventHubEventDto>> grouped = new LinkedHashMap<>();
|
||||
for (EventHubEventDto event : safeList(events)) {
|
||||
String driverKey = eventDriverKey(event);
|
||||
if (driverKey == null || driverKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
grouped.computeIfAbsent(driverKey, ignored -> new ArrayList<>()).add(event);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private String eventDriverKey(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = rawPayload(event);
|
||||
return firstNonBlank(text(raw, "driverKey"), driverKey(event));
|
||||
}
|
||||
|
||||
private ResolvedDriverTimeline reconstructMergedTimelineFromEvents(
|
||||
UUID fallbackSessionId,
|
||||
String fallbackDriverKey,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
ResolvedDriverTimeline reconstructed = timelineReconstructor.reconstruct(
|
||||
fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId,
|
||||
fallbackDriverKey,
|
||||
safeList(events)
|
||||
);
|
||||
return new ResolvedDriverTimeline(
|
||||
reconstructed.sourceKind(),
|
||||
reconstructed.loadedFrom(),
|
||||
reconstructed.loadedTo(),
|
||||
mergeVehicleUsageIntervals(reconstructed.vehicleUsageIntervals(), reconstructed.sourceKind()),
|
||||
reconstructed.activityIntervals(),
|
||||
reconstructed.supportEvents(),
|
||||
reconstructed.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> mergeVehicleUsageIntervals(
|
||||
List<ResolvedVehicleUsageInterval> intervals,
|
||||
String sourceKind
|
||||
) {
|
||||
List<ResolvedVehicleUsageInterval> sorted = safeList(intervals).stream()
|
||||
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from)
|
||||
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.toList();
|
||||
if (sorted.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<ResolvedVehicleUsageInterval> result = new ArrayList<>();
|
||||
ResolvedVehicleUsageInterval current = sorted.getFirst();
|
||||
List<String> currentSources = new ArrayList<>(current.sourceIntervalIds());
|
||||
for (int i = 1; i < sorted.size(); i++) {
|
||||
ResolvedVehicleUsageInterval next = sorted.get(i);
|
||||
if (canMergeVehicleUsage(current, next)) {
|
||||
currentSources.addAll(next.sourceIntervalIds());
|
||||
current = ResolvedVehicleUsageInterval.resolved(
|
||||
current.sessionId(),
|
||||
current.driverKey(),
|
||||
current.intervalId() + "+" + next.intervalId(),
|
||||
current.from(),
|
||||
mergedTo(current.to(), next.to()),
|
||||
current.odometerBeginKm(),
|
||||
mergedOdometerEnd(current, next),
|
||||
current.registrationKey(),
|
||||
current.vehicleKey(),
|
||||
sourceKind,
|
||||
currentSources
|
||||
);
|
||||
} else {
|
||||
result.add(current);
|
||||
current = next;
|
||||
currentSources = new ArrayList<>(current.sourceIntervalIds());
|
||||
}
|
||||
}
|
||||
result.add(current);
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean canMergeVehicleUsage(ResolvedVehicleUsageInterval left, ResolvedVehicleUsageInterval right) {
|
||||
return Objects.equals(left.registrationKey(), right.registrationKey())
|
||||
&& Objects.equals(left.vehicleKey(), right.vehicleKey())
|
||||
&& !right.from().isAfter(mergeBoundary(left.to()));
|
||||
}
|
||||
|
||||
private Long mergedOdometerEnd(
|
||||
ResolvedVehicleUsageInterval current,
|
||||
ResolvedVehicleUsageInterval next
|
||||
) {
|
||||
if (current == null) {
|
||||
return next == null ? null : next.odometerEndKm();
|
||||
}
|
||||
if (next == null) {
|
||||
return current.odometerEndKm();
|
||||
}
|
||||
if (current.to() == null || next.to() == null) {
|
||||
return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm();
|
||||
}
|
||||
if (next.to().isAfter(current.to()) || next.to().isEqual(current.to())) {
|
||||
return next.odometerEndKm() != null ? next.odometerEndKm() : current.odometerEndKm();
|
||||
}
|
||||
return current.odometerEndKm() != null ? current.odometerEndKm() : next.odometerEndKm();
|
||||
}
|
||||
|
||||
private OffsetDateTime mergedTo(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null || right == null) {
|
||||
return null;
|
||||
}
|
||||
return left.isAfter(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime mergeBoundary(OffsetDateTime endInclusive) {
|
||||
return endInclusive == null ? OffsetDateTime.MAX : endInclusive.plusSeconds(1);
|
||||
}
|
||||
|
||||
private TachographEsperDrivingDerivedProjectionBundle buildEsperDrivingDerivedProjectionBundleFromPointInput(
|
||||
List<Map<String, Object>> activityPointInputEvents,
|
||||
List<Map<String, Object>> vehicleUsagePointInputEvents,
|
||||
List<Map<String, Object>> supportGeoInputEvents,
|
||||
int significantDrivingMinutes,
|
||||
int minimumRestPeriodMinutes
|
||||
) {
|
||||
if ((activityPointInputEvents == null || activityPointInputEvents.isEmpty())
|
||||
&& (vehicleUsagePointInputEvents == null || vehicleUsagePointInputEvents.isEmpty())) {
|
||||
return emptyBundle();
|
||||
}
|
||||
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionIntervals = new ArrayList<>();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> dailyWeeklyRestCandidateIntervals = new ArrayList<>();
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> dailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>();
|
||||
List<TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent> unclassifiedDailyWeeklyRestCandidateCoverageIntervals = new ArrayList<>();
|
||||
List<TachographEsperDrivingInterruptionIntervalEvent> drivingInterruptionVehicleChangeIntervals = new ArrayList<>();
|
||||
List<TachographEsperVuCardAbsentIntervalEvent> vuCardAbsentIntervals = new ArrayList<>();
|
||||
List<TachographEsperPotentialHomeOvernightStayIntervalEvent> potentialHomeOvernightStayIntervals = new ArrayList<>();
|
||||
List<TachographEsperPotentialInVehicleOvernightStayIntervalEvent> potentialInVehicleOvernightStayIntervals = new ArrayList<>();
|
||||
List<TachographEsperPotentialInVehicleTripIntervalEvent> potentialInVehicleTripIntervals = new ArrayList<>();
|
||||
|
||||
executeWithRuntime(
|
||||
configuration -> {
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographActivityPointInputEvent",
|
||||
activityPointInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographVehicleUsagePointInputEvent",
|
||||
vehicleUsagePointInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographProjectionFinalizeEvent",
|
||||
projectionFinalizeInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographActivityIntervalInputEvent",
|
||||
activityIntervalInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographVehicleUsageIntervalInputEvent",
|
||||
vehicleUsageIntervalInputDefinition()
|
||||
);
|
||||
configuration.getCommon().addEventType(
|
||||
"TachographSupportGeoEvidenceInputEvent",
|
||||
supportGeoEvidenceInputDefinition()
|
||||
);
|
||||
},
|
||||
renderDrivingDerivedProjectionEventsEpl(significantDrivingMinutes, minimumRestPeriodMinutes),
|
||||
Map.of(
|
||||
"drivingInterruptionIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionIntervals),
|
||||
"dailyWeeklyRestCandidateIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, dailyWeeklyRestCandidateIntervals),
|
||||
"dailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, dailyWeeklyRestCandidateCoverageIntervals),
|
||||
"unclassifiedDailyWeeklyRestCandidateCoverageIntervals", newData -> collectDailyWeeklyRestCandidateCoverageIntervalEvents(newData, unclassifiedDailyWeeklyRestCandidateCoverageIntervals),
|
||||
"drivingInterruptionVehicleChangeIntervals", newData -> collectDrivingInterruptionIntervalEvents(newData, drivingInterruptionVehicleChangeIntervals),
|
||||
"vuCardAbsentIntervals", newData -> collectVuCardAbsentIntervalEvents(newData, vuCardAbsentIntervals),
|
||||
"potentialHomeOvernightStayIntervals", newData -> collectPotentialHomeOvernightStayIntervalEvents(newData, potentialHomeOvernightStayIntervals),
|
||||
"potentialInVehicleOvernightStayIntervals", newData -> collectPotentialInVehicleOvernightStayIntervalEvents(newData, potentialInVehicleOvernightStayIntervals),
|
||||
"potentialInVehicleTripIntervals", newData -> collectPotentialInVehicleTripIntervalEvents(newData, potentialInVehicleTripIntervals)
|
||||
),
|
||||
runtime -> {
|
||||
if (supportGeoInputEvents != null) {
|
||||
for (Map<String, Object> supportGeoEvidence : supportGeoInputEvents) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
supportGeoEvidence,
|
||||
"TachographSupportGeoEvidenceInputEvent"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (vehicleUsagePointInputEvents != null) {
|
||||
for (Map<String, Object> point : vehicleUsagePointInputEvents) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
point,
|
||||
"TachographVehicleUsagePointInputEvent"
|
||||
);
|
||||
}
|
||||
for (Map<String, Object> finalizeEvent : buildProjectionFinalizeEvents(vehicleUsagePointInputEvents)) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
finalizeEvent,
|
||||
"TachographProjectionFinalizeEvent"
|
||||
);
|
||||
}
|
||||
}
|
||||
if (activityPointInputEvents != null) {
|
||||
for (Map<String, Object> point : activityPointInputEvents) {
|
||||
runtime.getEventService().sendEventMap(
|
||||
point,
|
||||
"TachographActivityPointInputEvent"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return new TachographEsperDrivingDerivedProjectionBundle(
|
||||
sortDrivingInterruptionIntervals(drivingInterruptionIntervals),
|
||||
sortDrivingInterruptionIntervals(dailyWeeklyRestCandidateIntervals),
|
||||
sortDailyWeeklyRestCandidateCoverageIntervals(dailyWeeklyRestCandidateCoverageIntervals),
|
||||
sortDailyWeeklyRestCandidateCoverageIntervals(unclassifiedDailyWeeklyRestCandidateCoverageIntervals),
|
||||
sortDrivingInterruptionIntervals(drivingInterruptionVehicleChangeIntervals),
|
||||
sortVuCardAbsentIntervals(vuCardAbsentIntervals),
|
||||
sortPotentialHomeOvernightStayIntervals(potentialHomeOvernightStayIntervals),
|
||||
sortPotentialInVehicleOvernightStayIntervals(potentialInVehicleOvernightStayIntervals),
|
||||
sortPotentialInVehicleTripIntervals(potentialInVehicleTripIntervals)
|
||||
);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildProjectionFinalizeEvents(
|
||||
List<Map<String, Object>> vehicleUsagePointInputEvents
|
||||
) {
|
||||
Map<String, Map<String, Object>> byDriver = new LinkedHashMap<>();
|
||||
for (Map<String, Object> point : safeList(vehicleUsagePointInputEvents)) {
|
||||
String driverKey = Objects.toString(point.get("driverKey"), null);
|
||||
if (driverKey == null || driverKey.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> finalizeEvent = byDriver.computeIfAbsent(driverKey, ignored -> {
|
||||
Map<String, Object> event = new LinkedHashMap<>();
|
||||
event.put("sessionId", point.get("sessionId"));
|
||||
event.put("driverKey", driverKey);
|
||||
event.put("finalizedAtEpochSecond", 0L);
|
||||
return event;
|
||||
});
|
||||
Long occurredAtEpochSecond = (Long) point.get("occurredAtEpochSecond");
|
||||
Long finalizedAtEpochSecond = (Long) finalizeEvent.get("finalizedAtEpochSecond");
|
||||
if (occurredAtEpochSecond != null
|
||||
&& (finalizedAtEpochSecond == null || occurredAtEpochSecond > finalizedAtEpochSecond)) {
|
||||
finalizeEvent.put("finalizedAtEpochSecond", occurredAtEpochSecond);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(byDriver.values());
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> buildActivityIntervalInputEvents(
|
||||
UUID sessionId,
|
||||
String driverKey,
|
||||
|
|
@ -324,6 +751,56 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> activityPointInputDefinition() {
|
||||
Map<String, Object> definition = new LinkedHashMap<>();
|
||||
definition.put("sessionId", UUID.class);
|
||||
definition.put("driverKey", String.class);
|
||||
definition.put("eventId", String.class);
|
||||
definition.put("intervalId", String.class);
|
||||
definition.put("sourceRowId", String.class);
|
||||
definition.put("sourceRowIds", java.util.List.class);
|
||||
definition.put("activityType", String.class);
|
||||
definition.put("lifecycle", String.class);
|
||||
definition.put("occurredAt", OffsetDateTime.class);
|
||||
definition.put("occurredAtEpochSecond", long.class);
|
||||
definition.put("cardSlot", String.class);
|
||||
definition.put("cardStatus", String.class);
|
||||
definition.put("drivingStatus", String.class);
|
||||
definition.put("registrationKey", String.class);
|
||||
definition.put("vehicleKey", String.class);
|
||||
definition.put("sourceKind", String.class);
|
||||
definition.put("synthetic", boolean.class);
|
||||
definition.put("clippedToRequestedPeriod", boolean.class);
|
||||
definition.put("level", String.class);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Map<String, Object> vehicleUsagePointInputDefinition() {
|
||||
Map<String, Object> definition = new LinkedHashMap<>();
|
||||
definition.put("sessionId", UUID.class);
|
||||
definition.put("driverKey", String.class);
|
||||
definition.put("eventId", String.class);
|
||||
definition.put("intervalId", String.class);
|
||||
definition.put("sourceRowId", String.class);
|
||||
definition.put("sourceRowIds", java.util.List.class);
|
||||
definition.put("lifecycle", String.class);
|
||||
definition.put("occurredAt", OffsetDateTime.class);
|
||||
definition.put("occurredAtEpochSecond", long.class);
|
||||
definition.put("registrationKey", String.class);
|
||||
definition.put("vehicleKey", String.class);
|
||||
definition.put("sourceKind", String.class);
|
||||
definition.put("odometerKm", Long.class);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Map<String, Object> projectionFinalizeInputDefinition() {
|
||||
Map<String, Object> definition = new LinkedHashMap<>();
|
||||
definition.put("sessionId", UUID.class);
|
||||
definition.put("driverKey", String.class);
|
||||
definition.put("finalizedAtEpochSecond", long.class);
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Map<String, Object> activityIntervalInputDefinition() {
|
||||
Map<String, Object> definition = new LinkedHashMap<>();
|
||||
definition.put("sessionId", UUID.class);
|
||||
|
|
@ -470,6 +947,120 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
return event;
|
||||
}
|
||||
|
||||
private Map<String, Object> toActivityPointInputMap(
|
||||
UUID fallbackSessionId,
|
||||
String fallbackDriverKey,
|
||||
EventHubEventDto sourceEvent
|
||||
) {
|
||||
if (sourceEvent == null
|
||||
|| sourceEvent.eventDomain() != EventDomain.DRIVER_ACTIVITY
|
||||
|| (sourceEvent.lifecycle() != EventLifecycle.START && sourceEvent.lifecycle() != EventLifecycle.END)
|
||||
|| sourceEvent.occurredAt() == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = rawPayload(sourceEvent);
|
||||
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
|
||||
String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent));
|
||||
if (driverKey == null || intervalId == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode attributes = attributes(sourceEvent);
|
||||
Map<String, Object> event = new LinkedHashMap<>();
|
||||
event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent));
|
||||
event.put("driverKey", driverKey);
|
||||
event.put("eventId", sourceEvent.externalSourceEventId());
|
||||
event.put("intervalId", intervalId);
|
||||
event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
|
||||
event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
|
||||
event.put("activityType", firstNonBlank(text(raw, "activityType"), eventTypeAsActivity(sourceEvent.eventType())));
|
||||
event.put("lifecycle", sourceEvent.lifecycle().name());
|
||||
event.put("occurredAt", sourceEvent.occurredAt());
|
||||
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
|
||||
event.put("cardSlot", firstNonBlank(text(raw, "cardSlot"), text(attributes, "cardSlot")));
|
||||
event.put("cardStatus", firstNonBlank(text(raw, "cardStatus"), text(attributes, "cardStatus")));
|
||||
event.put("drivingStatus", firstNonBlank(text(raw, "drivingStatus"), text(attributes, "drivingStatus")));
|
||||
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
|
||||
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
|
||||
event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
|
||||
event.put("synthetic", booleanValue(raw, "synthetic", false));
|
||||
event.put("clippedToRequestedPeriod", booleanValue(raw, "clippedToRequestedPeriod", false));
|
||||
event.put("level", firstNonBlank(text(raw, "level"), "RAW_EVENT"));
|
||||
return event;
|
||||
}
|
||||
|
||||
private Map<String, Object> toVehicleUsagePointInputMap(
|
||||
UUID fallbackSessionId,
|
||||
String fallbackDriverKey,
|
||||
EventHubEventDto sourceEvent
|
||||
) {
|
||||
if (sourceEvent == null
|
||||
|| sourceEvent.eventDomain() != EventDomain.DRIVER_CARD
|
||||
|| (sourceEvent.lifecycle() != EventLifecycle.INSERT && sourceEvent.lifecycle() != EventLifecycle.WITHDRAW)
|
||||
|| sourceEvent.occurredAt() == null) {
|
||||
return null;
|
||||
}
|
||||
boolean supportedType = sourceEvent.eventType() == EventType.CARD_INSERTED
|
||||
|| sourceEvent.eventType() == EventType.CARD_WITHDRAWN;
|
||||
if (!supportedType) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = rawPayload(sourceEvent);
|
||||
String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId());
|
||||
String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent));
|
||||
if (driverKey == null || intervalId == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> event = new LinkedHashMap<>();
|
||||
event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent));
|
||||
event.put("driverKey", driverKey);
|
||||
event.put("eventId", sourceEvent.externalSourceEventId());
|
||||
event.put("intervalId", intervalId);
|
||||
event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId));
|
||||
event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId));
|
||||
event.put("lifecycle", sourceEvent.lifecycle().name());
|
||||
event.put("occurredAt", sourceEvent.occurredAt());
|
||||
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
|
||||
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
|
||||
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
|
||||
event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent)));
|
||||
event.put("odometerKm", odometerKm(sourceEvent, raw));
|
||||
return event;
|
||||
}
|
||||
|
||||
private Map<String, Object> toSupportGeoEvidenceInputMap(
|
||||
UUID fallbackSessionId,
|
||||
String fallbackDriverKey,
|
||||
EventHubEventDto sourceEvent
|
||||
) {
|
||||
if (sourceEvent == null || sourceEvent.occurredAt() == null || sourceEvent.position() == null) {
|
||||
return null;
|
||||
}
|
||||
String eventDomain = sourceEvent.eventDomain() == null ? null : sourceEvent.eventDomain().name();
|
||||
int priority = supportGeoPriority(eventDomain);
|
||||
if (priority <= 0) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = rawPayload(sourceEvent);
|
||||
String driverKey = firstNonBlank(text(raw, "driverKey"), fallbackDriverKey, driverKey(sourceEvent));
|
||||
if (driverKey == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> event = new LinkedHashMap<>();
|
||||
event.put("sessionId", sessionId(fallbackSessionId, raw, sourceEvent));
|
||||
event.put("driverKey", driverKey);
|
||||
event.put("eventId", firstNonBlank(text(raw, "supportEventId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId()));
|
||||
event.put("eventDomain", eventDomain);
|
||||
event.put("occurredAt", sourceEvent.occurredAt());
|
||||
event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond());
|
||||
event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent)));
|
||||
event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent)));
|
||||
event.put("latitude", sourceEvent.position().latitude().doubleValue());
|
||||
event.put("longitude", sourceEvent.position().longitude().doubleValue());
|
||||
event.put("odometerKm", odometerKm(sourceEvent, raw));
|
||||
event.put("priority", priority);
|
||||
return event;
|
||||
}
|
||||
|
||||
private int supportGeoPriority(String eventDomain) {
|
||||
if (eventDomain == null || eventDomain.isBlank()) {
|
||||
return 0;
|
||||
|
|
@ -483,6 +1074,186 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
};
|
||||
}
|
||||
|
||||
private Comparator<Map<String, Object>> pointEventComparator() {
|
||||
return Comparator
|
||||
.comparing((Map<String, Object> event) -> (Long) event.get("occurredAtEpochSecond"))
|
||||
.thenComparing(event -> lifecycleOrder(Objects.toString(event.get("lifecycle"), "")))
|
||||
.thenComparing(event -> Objects.toString(event.get("driverKey"), ""))
|
||||
.thenComparing(event -> Objects.toString(event.get("intervalId"), ""))
|
||||
.thenComparing(event -> Objects.toString(event.get("eventId"), ""));
|
||||
}
|
||||
|
||||
private int lifecycleOrder(String lifecycle) {
|
||||
return switch (lifecycle) {
|
||||
case "INSERT", "START" -> 0;
|
||||
case "WITHDRAW", "END" -> 1;
|
||||
default -> 2;
|
||||
};
|
||||
}
|
||||
|
||||
private EventHubProperties.DrivingDerivedProjectionInputMode drivingDerivedProjectionInputMode() {
|
||||
return properties.getTachographFileSession().getProcessing().getDrivingDerivedProjectionInputMode();
|
||||
}
|
||||
|
||||
private RawSourceDriverTimelineEventBuilder rawSourceEventBuilder() {
|
||||
if (rawSourceEventBuilder == null) {
|
||||
throw new IllegalStateException(
|
||||
"Driving-derived projection input mode EVENTS requires RawSourceDriverTimelineEventBuilder"
|
||||
);
|
||||
}
|
||||
return rawSourceEventBuilder;
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private JsonNode attributes(EventHubEventDto event) {
|
||||
return event.eventDetails() == null ? null : event.eventDetails().attributes();
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text;
|
||||
}
|
||||
|
||||
private boolean booleanValue(JsonNode node, String field, boolean fallback) {
|
||||
if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
|
||||
return fallback;
|
||||
}
|
||||
return node.get(field).asBoolean(fallback);
|
||||
}
|
||||
|
||||
private Long longValue(JsonNode node, String field) {
|
||||
if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value.isNumber()) {
|
||||
return value.asLong();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.asText());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> stringList(JsonNode node, String field, String fallback) {
|
||||
JsonNode value = node == null || field == null ? null : node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return fallback == null ? List.of() : List.of(fallback);
|
||||
}
|
||||
if (value.isArray()) {
|
||||
List<String> result = new ArrayList<>();
|
||||
value.forEach(item -> {
|
||||
if (item != null && !item.isNull()) {
|
||||
String text = item.asText(null);
|
||||
if (text != null && !text.isBlank()) {
|
||||
result.add(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
return result.isEmpty() && fallback != null ? List.of(fallback) : List.copyOf(result);
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? (fallback == null ? List.of() : List.of(fallback)) : List.of(text);
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String eventTypeAsActivity(EventType eventType) {
|
||||
if (eventType == null) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
return switch (eventType) {
|
||||
case DRIVE -> "DRIVE";
|
||||
case WORK -> "WORK";
|
||||
case AVAILABILITY -> "AVAILABILITY";
|
||||
case BREAK_REST -> "BREAK_REST";
|
||||
default -> eventType.name();
|
||||
};
|
||||
}
|
||||
|
||||
private UUID sessionId(UUID fallbackSessionId, JsonNode raw, EventHubEventDto event) {
|
||||
String rawSessionId = firstNonBlank(
|
||||
text(raw, "sessionId"),
|
||||
event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId()
|
||||
);
|
||||
if (rawSessionId != null) {
|
||||
try {
|
||||
return UUID.fromString(rawSessionId);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// DB-acquired source packages need not be UUID-based file sessions.
|
||||
}
|
||||
}
|
||||
return fallbackSessionId == null ? new UUID(0L, 0L) : fallbackSessionId;
|
||||
}
|
||||
|
||||
private String driverKey(EventHubEventDto event) {
|
||||
if (event.driverRef() == null) {
|
||||
return null;
|
||||
}
|
||||
if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) {
|
||||
return event.driverRef().driverCard().stableKey();
|
||||
}
|
||||
return event.driverRef().sourceEntityId();
|
||||
}
|
||||
|
||||
private String registrationKey(EventHubEventDto event) {
|
||||
if (event.vehicleRef() == null || event.vehicleRef().vehicleRegistration() == null) {
|
||||
return null;
|
||||
}
|
||||
return event.vehicleRef().vehicleRegistration().stableKey();
|
||||
}
|
||||
|
||||
private String vehicleKey(EventHubEventDto event) {
|
||||
if (event.vehicleRef() == null) {
|
||||
return null;
|
||||
}
|
||||
return firstNonBlank(event.vehicleRef().vin(), event.vehicleRef().sourceVehicleEntityId());
|
||||
}
|
||||
|
||||
private String sourceKind(EventHubEventDto event) {
|
||||
return event.packageInfo() == null || event.packageInfo().eventSource() == null
|
||||
? null
|
||||
: event.packageInfo().eventSource().sourceKind();
|
||||
}
|
||||
|
||||
private Long odometerKm(EventHubEventDto event, JsonNode raw) {
|
||||
Long explicit = longValue(raw, event.lifecycle() == EventLifecycle.WITHDRAW ? "odometerEndKm" : "odometerBeginKm");
|
||||
if (explicit != null) {
|
||||
return explicit;
|
||||
}
|
||||
explicit = longValue(raw, "odometerKm");
|
||||
if (explicit != null) {
|
||||
return explicit;
|
||||
}
|
||||
return event.odometerM() == null ? null : event.odometerM() / 1_000L;
|
||||
}
|
||||
|
||||
private String firstSourceIntervalId(ResolvedActivityInterval interval) {
|
||||
return interval.sourceIntervalIds().isEmpty() ? interval.intervalId() : interval.sourceIntervalIds().get(0);
|
||||
}
|
||||
|
|
@ -901,6 +1672,12 @@ public class DriverTimelineReusableProjectionBuilder {
|
|||
.toList();
|
||||
}
|
||||
|
||||
private String renderDrivingDerivedProjectionEventsEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) {
|
||||
return DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL
|
||||
+ "\n\n"
|
||||
+ renderDrivingDerivedProjectionBundleEpl(significantDrivingMinutes, minimumRestPeriodMinutes);
|
||||
}
|
||||
|
||||
private String renderDrivingDerivedProjectionBundleEpl(int significantDrivingMinutes, int minimumRestPeriodMinutes) {
|
||||
return DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE
|
||||
.replace(
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
|||
|
||||
List<EventHubEventDto> activityEvents = buildActivityEvents(
|
||||
session,
|
||||
driverSession.driverKey(),
|
||||
timeline.activityIntervals(),
|
||||
driverRef,
|
||||
registrationsByKey,
|
||||
|
|
@ -104,6 +105,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
|||
);
|
||||
List<EventHubEventDto> vehicleUsageEvents = buildVehicleUsageEvents(
|
||||
session,
|
||||
driverSession.driverKey(),
|
||||
timeline.vehicleUsageIntervals(),
|
||||
driverRef,
|
||||
registrationsByKey,
|
||||
|
|
@ -125,6 +127,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
|||
|
||||
private List<EventHubEventDto> buildActivityEvents(
|
||||
TachographFileSession session,
|
||||
String driverKey,
|
||||
List<ResolvedActivityInterval> intervals,
|
||||
DriverRefDto driverRef,
|
||||
Map<String, ExtractedVehicleRegistration> registrationsByKey,
|
||||
|
|
@ -147,6 +150,8 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
|||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
raw.put("intervalId", interval.intervalId());
|
||||
raw.put("sourceRowId", interval.intervalId());
|
||||
raw.put("driverKey", driverKey);
|
||||
raw.put("activityType", interval.activityType());
|
||||
raw.put("sourceRowIds", interval.sourceIntervalIds());
|
||||
raw.put("startedAt", timeText(interval.from()));
|
||||
raw.put("endedAt", timeText(interval.to()));
|
||||
|
|
@ -200,6 +205,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
|||
|
||||
private List<EventHubEventDto> buildVehicleUsageEvents(
|
||||
TachographFileSession session,
|
||||
String driverKey,
|
||||
List<ResolvedVehicleUsageInterval> intervals,
|
||||
DriverRefDto driverRef,
|
||||
Map<String, ExtractedVehicleRegistration> registrationsByKey,
|
||||
|
|
@ -226,6 +232,7 @@ public class IntervalBackedDriverTimelineEventBuilder implements DriverTimelineE
|
|||
Map<String, Object> raw = new LinkedHashMap<>();
|
||||
raw.put("intervalId", interval.intervalId());
|
||||
raw.put("sourceRowId", interval.intervalId());
|
||||
raw.put("driverKey", driverKey);
|
||||
raw.put("sourceRowIds", interval.sourceIntervalIds());
|
||||
raw.put("startedAt", timeText(interval.from()));
|
||||
raw.put("endedAt", timeText(interval.to()));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedActivityInterval;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedVehicleUsageInterval;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographTimelineEventBundle;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Builds EventHub point events directly from the extracted tachograph source records.
|
||||
*
|
||||
* <p>This builder intentionally does not run the normal {@link DriverTimelineBuilder} interval
|
||||
* resolution/merging path. It only wraps each raw source interval as a START/END or
|
||||
* INSERT/WITHDRAW point-event pair. Any subsequent pairing, coalescing, or derived interval logic
|
||||
* belongs to the event-input EPL projection pipeline.</p>
|
||||
*/
|
||||
@Component
|
||||
public class RawSourceDriverTimelineEventBuilder {
|
||||
|
||||
private final IntervalBackedDriverTimelineEventBuilder intervalEventBuilder;
|
||||
|
||||
public RawSourceDriverTimelineEventBuilder(IntervalBackedDriverTimelineEventBuilder intervalEventBuilder) {
|
||||
this.intervalEventBuilder = intervalEventBuilder;
|
||||
}
|
||||
|
||||
public TachographTimelineEventBundle buildRawEventBundle(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession
|
||||
) {
|
||||
if (session == null || driverSession == null) {
|
||||
return new TachographTimelineEventBundle(List.of(), List.of(), List.of());
|
||||
}
|
||||
return intervalEventBuilder.buildEventBundle(session, driverSession, buildRawTimeline(session, driverSession));
|
||||
}
|
||||
|
||||
public ResolvedDriverTimeline buildRawTimeline(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession
|
||||
) {
|
||||
String sourceKind = session.metadata().driverCardFile() ? "DRIVER_CARD" : "VEHICLE_UNIT";
|
||||
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals = rawVehicleUsageIntervals(
|
||||
session,
|
||||
driverSession,
|
||||
sourceKind
|
||||
);
|
||||
List<ResolvedActivityInterval> activityIntervals = rawActivityIntervals(driverSession, sourceKind);
|
||||
List<ExtractedSupportEvent> supportEvents = sortedSupportEvents(driverSession.supportEvents());
|
||||
return new ResolvedDriverTimeline(
|
||||
sourceKind,
|
||||
minTimestamp(vehicleUsageIntervals, activityIntervals, supportEvents),
|
||||
maxTimestamp(vehicleUsageIntervals, activityIntervals, supportEvents),
|
||||
vehicleUsageIntervals,
|
||||
activityIntervals,
|
||||
supportEvents,
|
||||
mergeWarnings(session.warnings(), driverSession.warnings())
|
||||
);
|
||||
}
|
||||
|
||||
private List<ResolvedVehicleUsageInterval> rawVehicleUsageIntervals(
|
||||
TachographFileSession session,
|
||||
DriverExtractionSession driverSession,
|
||||
String sourceKind
|
||||
) {
|
||||
if (driverSession.cardVehicleUsageIntervals() == null || driverSession.cardVehicleUsageIntervals().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return driverSession.cardVehicleUsageIntervals().stream()
|
||||
.filter(interval -> interval.from() != null && (interval.to() == null || interval.to().isAfter(interval.from())))
|
||||
.map(interval -> ResolvedVehicleUsageInterval.resolved(
|
||||
session.sessionId(),
|
||||
driverSession.driverKey(),
|
||||
interval.intervalId(),
|
||||
interval.from(),
|
||||
interval.to(),
|
||||
interval.odometerBeginKm(),
|
||||
interval.odometerEndKm(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
sourceKind,
|
||||
List.of(interval.intervalId())
|
||||
))
|
||||
.sorted(Comparator.comparing(ResolvedVehicleUsageInterval::from)
|
||||
.thenComparing(ResolvedVehicleUsageInterval::to, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(ResolvedVehicleUsageInterval::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<ResolvedActivityInterval> rawActivityIntervals(
|
||||
DriverExtractionSession driverSession,
|
||||
String sourceKind
|
||||
) {
|
||||
if (driverSession.cardActivityIntervals() == null || driverSession.cardActivityIntervals().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return driverSession.cardActivityIntervals().stream()
|
||||
.filter(interval -> interval.from() != null && interval.to() != null && interval.to().isAfter(interval.from()))
|
||||
.map(interval -> ResolvedActivityInterval.raw(
|
||||
interval.intervalId(),
|
||||
interval.from(),
|
||||
interval.to(),
|
||||
normalizeActivity(interval.activityType()),
|
||||
interval.slot(),
|
||||
interval.cardStatus(),
|
||||
interval.drivingStatus(),
|
||||
interval.registrationKey(),
|
||||
interval.vehicleKey(),
|
||||
sourceKind,
|
||||
List.of(interval.intervalId())
|
||||
))
|
||||
.sorted(Comparator.comparing(ResolvedActivityInterval::from)
|
||||
.thenComparing(ResolvedActivityInterval::to)
|
||||
.thenComparing(ResolvedActivityInterval::intervalId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<ExtractedSupportEvent> sortedSupportEvents(List<ExtractedSupportEvent> supportEvents) {
|
||||
if (supportEvents == null || supportEvents.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return supportEvents.stream()
|
||||
.sorted(Comparator.comparing(ExtractedSupportEvent::occurredAt)
|
||||
.thenComparing(ExtractedSupportEvent::eventDomain, Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(ExtractedSupportEvent::eventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String normalizeActivity(String activityType) {
|
||||
if (activityType == null || activityType.isBlank()) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
if ("UNKNOWN_ACTIVITY".equals(activityType)) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
return activityType;
|
||||
}
|
||||
|
||||
private OffsetDateTime minTimestamp(
|
||||
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||
List<ResolvedActivityInterval> activityIntervals,
|
||||
List<ExtractedSupportEvent> supportEvents
|
||||
) {
|
||||
OffsetDateTime min = null;
|
||||
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
|
||||
min = min(min, interval.from());
|
||||
}
|
||||
for (ResolvedActivityInterval interval : activityIntervals) {
|
||||
min = min(min, interval.from());
|
||||
}
|
||||
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||
min = min(min, supportEvent.occurredAt());
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
private OffsetDateTime maxTimestamp(
|
||||
List<ResolvedVehicleUsageInterval> vehicleUsageIntervals,
|
||||
List<ResolvedActivityInterval> activityIntervals,
|
||||
List<ExtractedSupportEvent> supportEvents
|
||||
) {
|
||||
OffsetDateTime max = null;
|
||||
for (ResolvedVehicleUsageInterval interval : vehicleUsageIntervals) {
|
||||
max = max(max, interval.to());
|
||||
}
|
||||
for (ResolvedActivityInterval interval : activityIntervals) {
|
||||
max = max(max, interval.to());
|
||||
}
|
||||
for (ExtractedSupportEvent supportEvent : supportEvents) {
|
||||
max = max(max, supportEvent.occurredAt());
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
private List<ExtractionWarning> mergeWarnings(List<ExtractionWarning> sessionWarnings, List<ExtractionWarning> driverWarnings) {
|
||||
LinkedHashSet<ExtractionWarning> merged = new LinkedHashSet<>();
|
||||
if (sessionWarnings != null) {
|
||||
merged.addAll(sessionWarnings);
|
||||
}
|
||||
if (driverWarnings != null) {
|
||||
merged.addAll(driverWarnings);
|
||||
}
|
||||
return List.copyOf(merged);
|
||||
}
|
||||
|
||||
private OffsetDateTime min(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isBefore(right) ? left : right;
|
||||
}
|
||||
|
||||
private OffsetDateTime max(OffsetDateTime left, OffsetDateTime right) {
|
||||
if (left == null) {
|
||||
return right;
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left.isAfter(right) ? left : right;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue