Add runtime processing pipeline and validation flows
This commit is contained in:
parent
5c887e8cb2
commit
82e2bd0860
|
|
@ -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.
|
||||
|
|
@ -1,64 +1,67 @@
|
|||
# Runtime Event Processing
|
||||
# Runtime event processing
|
||||
|
||||
Runtime Processing is now source-neutral. The API receives a runtime scope, selects a processing profile, partitions the normalized EventHub-style events, and delegates execution to the selected profile.
|
||||
Runtime processing is now centered on **processing executions**. An execution loads canonical EventHub runtime events from selected sources, optionally partitions the event set, then runs a configured processing plan made of common EPL and/or Java modules.
|
||||
|
||||
The tachograph Esper processing is no longer the root concept. It is one profile:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
## Profile discovery endpoint
|
||||
The preferred endpoint is:
|
||||
|
||||
```http
|
||||
GET /api/eventhub/runtime-processing/event-processing/profiles
|
||||
POST /api/eventhub/runtime-processing/executions
|
||||
```
|
||||
|
||||
Example response:
|
||||
List available plans:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"displayName": "Tachograph Driver Esper Processing",
|
||||
"description": "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes.",
|
||||
"defaultPartitioningStrategy": "DRIVER",
|
||||
"supportedPartitioningStrategies": ["DRIVER"],
|
||||
"requiredParameters": [],
|
||||
"optionalParameters": [
|
||||
"significantDrivingMinutes",
|
||||
"minimumRestPeriodMinutes",
|
||||
"attachVehicleOnlyEvents",
|
||||
"vehicleEvidencePaddingMinutes",
|
||||
"includePartitionDebug"
|
||||
]
|
||||
}
|
||||
]
|
||||
```http
|
||||
GET /api/eventhub/runtime-processing/executions/plans
|
||||
```
|
||||
|
||||
Clients should prefer this endpoint instead of hardcoding profile metadata.
|
||||
|
||||
## Generic execution endpoint
|
||||
The older endpoint is still available for compatibility:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
## Request shape
|
||||
but it should be treated as a legacy profile adapter. New clients should use `processingPlanKey`, not `profileKey`.
|
||||
|
||||
## First predefined plan
|
||||
|
||||
The first predefined plan is:
|
||||
|
||||
```text
|
||||
driver-working-time-v1
|
||||
```
|
||||
|
||||
Legacy alias:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
This plan runs the source-neutral driver working-time chain over canonical runtime events. Tachograph file/database data is only one possible source of those canonical events. Its modules include:
|
||||
|
||||
```text
|
||||
event-to-activity-intervals
|
||||
event-to-vehicle-usage-intervals
|
||||
vehicle-evidence-attachment
|
||||
support-evidence-normalization
|
||||
driving-derived-projections
|
||||
```
|
||||
|
||||
The important design point is that runtime processing is not tachograph-specific. Tachograph is now treated as a source/compatibility layer; the working-time plan consumes canonical driver activity, vehicle usage, and support-evidence events from any supported source.
|
||||
|
||||
## Example execution
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"scope": {
|
||||
"processingPlanKey": "driver-working-time-v1",
|
||||
"sourceSelection": {
|
||||
"sessionIds": [
|
||||
"11111111-1111-1111-1111-111111111111",
|
||||
"22222222-2222-2222-2222-222222222222"
|
||||
],
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION"],
|
||||
"sourceFamilies": ["TACHOGRAPH_FILE_SESSION", "YELLOWFOX_DB"],
|
||||
"occurredFrom": "2026-05-01T00:00:00Z",
|
||||
"occurredTo": "2026-05-31T23:59:59Z",
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15
|
||||
"expandVehicleEvents": true
|
||||
},
|
||||
"partitioning": {
|
||||
"strategy": "DRIVER",
|
||||
|
|
@ -70,175 +73,44 @@ POST /api/eventhub/runtime-processing/event-processing
|
|||
"parameters": {
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720,
|
||||
"attachVehicleOnlyEvents": true,
|
||||
"vehicleEvidencePaddingMinutes": 15,
|
||||
"includePartitionDebug": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"partitioningStrategy": "DRIVER",
|
||||
"inputEventCount": 1234,
|
||||
"selectedPartitionCount": 2,
|
||||
"discoveredVehicleCount": 3,
|
||||
"partitionResults": {
|
||||
"12:12345678901234": {
|
||||
"partitionType": "DRIVER",
|
||||
"partitionKey": "12:12345678901234",
|
||||
"resultType": "UnifiedRuntimeDerivedProjectionResultDto",
|
||||
"result": {
|
||||
"projection": {
|
||||
"activityIntervals": [],
|
||||
"drivingIntervals": [],
|
||||
"drivingInterruptionIntervals": [],
|
||||
"dailyWeeklyRestCandidateIntervals": [],
|
||||
"dailyWeeklyRestCandidateCoverageIntervals": [],
|
||||
"unclassifiedDailyWeeklyRestCandidateCoverageIntervals": [],
|
||||
"potentialHomeOvernightStayIntervals": [],
|
||||
"potentialInVehicleOvernightStayIntervals": [],
|
||||
"potentialInVehicleTripIntervals": [],
|
||||
"vehicleUsageIntervals": [],
|
||||
"vuCardAbsentIntervals": [],
|
||||
"supportGeoEvents": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": [],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## Concepts
|
||||
|
||||
### Scope
|
||||
|
||||
`scope` is the existing runtime selection model. It can select events from:
|
||||
## Conceptual flow
|
||||
|
||||
```text
|
||||
TACHOGRAPH_FILE_SESSION
|
||||
TACHOGRAPH_DB
|
||||
YELLOWFOX_DB
|
||||
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
|
||||
```
|
||||
|
||||
and can use:
|
||||
## Why processing plans instead of profiles
|
||||
|
||||
A plan describes **which modules should run over a runtime event set**. This matches the EventHub goal better than domain-oriented profiles.
|
||||
|
||||
Future plans can reuse the same event-loading infrastructure:
|
||||
|
||||
```text
|
||||
SOURCE_DB
|
||||
EVENTHUB_DB
|
||||
```
|
||||
|
||||
where supported.
|
||||
|
||||
For uploaded tachograph files, the scope can use:
|
||||
|
||||
```text
|
||||
sessionId
|
||||
sessionIds
|
||||
compositeSessionId
|
||||
```
|
||||
|
||||
### Profile
|
||||
|
||||
A profile owns domain-specific processing semantics. It defines:
|
||||
|
||||
```text
|
||||
profile key
|
||||
expected partitioning strategy
|
||||
profile-specific parameters
|
||||
result type
|
||||
```
|
||||
|
||||
Current profile:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```
|
||||
|
||||
Future profiles can include:
|
||||
|
||||
```text
|
||||
vehicle-stop-detection-v1
|
||||
vehicle-trip-detection-v1
|
||||
telematics-poi-clustering-v1
|
||||
vehicle-stop-detection-v1
|
||||
driver-settlement-v1
|
||||
mixed-driver-vehicle-correlation-v1
|
||||
telematics-poi-clustering-v1
|
||||
```
|
||||
|
||||
### Partitioning
|
||||
Each plan may use different partitioning and modules, but the source loading and canonical event model remain common.
|
||||
|
||||
The common API supports generic partitioning options:
|
||||
## Compatibility
|
||||
|
||||
```text
|
||||
NONE
|
||||
DRIVER
|
||||
VEHICLE
|
||||
DRIVER_VEHICLE
|
||||
SOURCE_FAMILY
|
||||
CUSTOM_PROFILE
|
||||
```
|
||||
|
||||
The first tachograph profile currently supports `DRIVER` partitioning. The service partitions mixed event scopes in Java before invoking Esper so that existing single-driver EPL windows cannot mix driver states.
|
||||
|
||||
## Partition debug / audit output
|
||||
|
||||
For mixed-source scopes, request debug output when validating why events were or were not attached to a driver partition:
|
||||
|
||||
```json
|
||||
{
|
||||
"partitioning": {
|
||||
"strategy": "DRIVER",
|
||||
"includeAllPartitions": true,
|
||||
"attachVehicleEvidence": true,
|
||||
"vehicleEvidencePaddingMinutes": 15,
|
||||
"includeDebug": true
|
||||
},
|
||||
"parameters": {
|
||||
"includePartitionDebug": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When enabled, each generic `partitionResults[*].metadata.partitionDebug` contains:
|
||||
|
||||
```text
|
||||
directDriverEventCount
|
||||
vehicleUsageIntervalCount
|
||||
candidateVehicleEvidenceEventCount
|
||||
attachedVehicleEvidenceEventCount
|
||||
ignoredVehicleEvidenceEventCount
|
||||
mergedEventCount
|
||||
vehicleUsageIntervals
|
||||
vehicleEvidenceDecisions
|
||||
notes
|
||||
warnings
|
||||
```
|
||||
|
||||
`vehicleEvidenceDecisions` explains every relevant decision, for example:
|
||||
|
||||
```text
|
||||
DIRECT_DRIVER_EVENT
|
||||
ATTACHED_VEHICLE_EVIDENCE
|
||||
IGNORED_NO_OVERLAPPING_VEHICLE_USAGE
|
||||
IGNORED_ATTACHMENT_DISABLED
|
||||
```
|
||||
|
||||
The compatibility response type also has `partitionDebugByDriver`; use the generic endpoint when you need to enable debug output explicitly. Keep debug disabled for high-volume production requests unless you need attribution diagnostics, because the decision list can be large.
|
||||
|
||||
## Compatibility endpoint
|
||||
|
||||
The old tachograph endpoint remains available:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/tachograph/esper-processing
|
||||
```
|
||||
|
||||
It now acts as a compatibility adapter for:
|
||||
Legacy profile endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
|
|
@ -246,143 +118,72 @@ POST /api/eventhub/runtime-processing/event-processing
|
|||
|
||||
with:
|
||||
|
||||
```text
|
||||
profileKey = tachograph-driver-esper-v1
|
||||
partitioning.strategy = DRIVER
|
||||
```
|
||||
|
||||
## Tachograph profile processing flow
|
||||
|
||||
```text
|
||||
runtime event scope
|
||||
-> broad event assembly
|
||||
-> driver partition discovery
|
||||
-> for each driver:
|
||||
direct driver events
|
||||
reconstruct driver vehicle-usage intervals
|
||||
attach only vehicle-scoped events whose vehicle matches and whose timestamp falls inside a usage interval plus configured padding
|
||||
shared event-input Esper projection pipeline
|
||||
-> generic partitionResults map
|
||||
```
|
||||
|
||||
The tachograph profile reuses the same `TachographEsperProcessingCore` used by the file-session endpoint. This prevents the file-session API and runtime-processing API from drifting into separate rule chains.
|
||||
## Vehicle-only evidence attachment
|
||||
|
||||
For driver-partitioned profiles, vehicle-only events are no longer attached only by vehicle identity. They are attached to a driver partition only when there is temporal evidence:
|
||||
|
||||
```text
|
||||
vehicle-only event.vehicleKey/registrationKey matches a reconstructed driver vehicle-usage interval
|
||||
and event.occurredAt is inside [usage.from - padding, usage.to + padding]
|
||||
```
|
||||
|
||||
This prevents unrelated vehicle events from being copied into a driver result simply because the driver used the same vehicle on another day. The tachograph profile currently uses:
|
||||
|
||||
```json
|
||||
{
|
||||
"partitioning": {
|
||||
"attachVehicleEvidence": true,
|
||||
"vehicleEvidencePaddingMinutes": 15,
|
||||
"includeDebug": true
|
||||
},
|
||||
"parameters": {
|
||||
"attachVehicleOnlyEvents": true,
|
||||
"vehicleEvidencePaddingMinutes": 15,
|
||||
"includePartitionDebug": true
|
||||
}
|
||||
"profileKey": "tachograph-driver-esper-v1",
|
||||
"scope": {},
|
||||
"partitioning": {},
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
`parameters` take precedence in the tachograph profile. The compatibility endpoint maps these values to `expandVehicleEvents` and `vehicleExpansionPaddingMinutes`.
|
||||
|
||||
|
||||
## Tachograph parity validation
|
||||
|
||||
A validation endpoint is available to compare the legacy tachograph file-session Esper endpoint with the generic runtime event-processing profile:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing/validation/tachograph-parity
|
||||
```
|
||||
|
||||
This endpoint runs both paths for the selected session(s) and driver(s):
|
||||
internally delegates to:
|
||||
|
||||
```text
|
||||
legacy file-session path
|
||||
/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events
|
||||
|
||||
runtime event-processing path
|
||||
/api/eventhub/runtime-processing/event-processing
|
||||
profileKey = tachograph-driver-esper-v1
|
||||
processingPlanKey = driver-working-time-v1
|
||||
```
|
||||
|
||||
Example request:
|
||||
## Source-neutral driver working-time modules
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "{{sessionId}}",
|
||||
"driverKey": "{{driverKey}}",
|
||||
"occurredFrom": "2026-05-01T00:00:00Z",
|
||||
"occurredTo": "2026-05-31T23:59:59Z",
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15,
|
||||
"significantDrivingMinutes": 3,
|
||||
"minimumRestPeriodMinutes": 720,
|
||||
"includeDebug": true
|
||||
}
|
||||
```
|
||||
|
||||
For multiple uploaded tachograph sessions, use `sessionIds` or `compositeSessionId`. The validation service resolves the selected drivers, executes the generic runtime profile, then compares count-level parity per driver.
|
||||
|
||||
Compared categories include:
|
||||
The former tachograph-named processing artifacts are now represented by source-neutral driver working-time names:
|
||||
|
||||
```text
|
||||
activityIntervals
|
||||
drivingIntervals
|
||||
drivingInterruptionIntervals
|
||||
drivingInterruptionVehicleChangeIntervals
|
||||
dailyWeeklyRestCandidateIntervals
|
||||
dailyWeeklyRestCandidateCoverageIntervals
|
||||
unclassifiedDailyWeeklyRestCandidateCoverageIntervals
|
||||
potentialHomeOvernightStayIntervals
|
||||
potentialInVehicleOvernightStayIntervals
|
||||
potentialInVehicleTripIntervals
|
||||
vehicleUsageIntervals
|
||||
vuCardAbsentIntervals
|
||||
supportGeoEvents
|
||||
runtime-driver-event-interval-preprocessor.epl
|
||||
driver-working-time-derived-projections.epl
|
||||
DriverWorkingTimeProcessingResultDto
|
||||
DriverWorkingTimeProcessingCore
|
||||
RuntimeDriverWorkingTimeScopeProcessingService
|
||||
```
|
||||
|
||||
For a single session, this is a direct parity check against the original file-session endpoint. For multiple sessions, the reference side is the sum of the individual file-session endpoint results per driver; runtime processing may intentionally deduplicate or merge across session boundaries, so differences should be reviewed with the debug/audit output.
|
||||
Legacy tachograph names are kept as compatibility adapters for existing file-session endpoints and Postman requests. New runtime code should use the `driver-working-time-*` classes/resources and the `driver-working-time-v1` processing plan.
|
||||
|
||||
## Runtime support evidence normalization
|
||||
## Module execution results
|
||||
|
||||
The tachograph profile now normalizes mixed-source support events before invoking the shared Esper core:
|
||||
`/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly.
|
||||
|
||||
Top-level execution response includes:
|
||||
|
||||
```text
|
||||
runtime partition events
|
||||
-> RuntimeSupportEvidenceNormalizer
|
||||
-> tachograph-consumable support evidence view
|
||||
-> event-input Esper preprocessor / driving-derived bundle
|
||||
moduleResults
|
||||
```
|
||||
|
||||
The normalizer does not change driver activity events or driver-card usage events. It only adapts support/vehicle events that carry geo or odometer evidence. Provider-specific semantics are preserved in the payload under `raw.originalEventDomain`, `raw.originalEventType`, `raw.originalLifecycle`, `raw.supportEventDomain`, and `raw.supportEventType`.
|
||||
This map contains one entry per executed module. The top-level entries are intentionally sanitized: they expose status, warnings, and metadata without duplicating the full partition output payload.
|
||||
|
||||
Examples:
|
||||
Each partition result also includes:
|
||||
|
||||
```text
|
||||
IGNITION / IGNITION_ON with position
|
||||
-> POSITION / POSITION_RECORDED / SNAPSHOT
|
||||
-> raw.supportEventType = IGNITION_ON
|
||||
|
||||
TELEMATICS_DATA with position
|
||||
-> POSITION / POSITION_RECORDED / SNAPSHOT
|
||||
-> raw.supportEventType = TELEMATICS_DATA
|
||||
|
||||
BORDER_CROSSING with position
|
||||
-> BORDER_CROSSING, preserving the original border event type/lifecycle
|
||||
|
||||
LOAD_UNLOAD with position
|
||||
-> LOAD_UNLOAD, preserving the original load/unload event type/lifecycle
|
||||
partitionResults[*].moduleResults
|
||||
```
|
||||
|
||||
This keeps the EPL rules provider-neutral. YellowFox, tachograph VU, or future telematics events are converted to the common support-evidence shape before the tachograph profile consumes them.
|
||||
For `driver-working-time-v1`, the partition-level module results currently expose:
|
||||
|
||||
Runtime result notes include how many events were inspected and how many support events were adapted. Use partition debug together with normalization notes when validating mixed-source attribution.
|
||||
```text
|
||||
vehicle-evidence-attachment
|
||||
support-evidence-normalization
|
||||
driving-derived-projections
|
||||
```
|
||||
|
||||
The first implementation uses wrapper modules around the existing validated driver-working-time scope service. Logical phase modules such as `event-to-activity-intervals`, `event-to-vehicle-usage-intervals`, and `vehicle-usage-merge` are registered and executed as delegated modules so they can later be split into true standalone EPL/Java modules without changing the external processing-plan contract.
|
||||
|
||||
## First-class EPL modules
|
||||
|
||||
Runtime processing now supports modules implemented directly with Esper EPL through the common `RuntimeEplModuleExecutor`.
|
||||
|
||||
The first source-neutral EPL modules are:
|
||||
|
||||
| Module key | Engine | EPL resource | Output statement |
|
||||
|---|---|---|---|
|
||||
| `event-to-activity-intervals` | `EPL` | `esper/runtime-driver-activity-intervals.epl` | `driverActivityIntervals` |
|
||||
| `event-to-vehicle-usage-intervals` | `EPL` | `esper/runtime-driver-vehicle-usage-intervals.epl` | `driverVehicleUsageIntervals` |
|
||||
|
||||
These modules operate on canonical EventHub runtime events, not on tachograph-specific source rows. They are currently used as first-class phase modules in `driver-working-time-v1`; the final `driving-derived-projections` module remains a compatibility adapter over the validated working-time projection service until the remaining projection stages are split into direct EPL modules.
|
||||
|
|
|
|||
|
|
@ -1,60 +1,53 @@
|
|||
# Runtime tachograph Esper scope processing
|
||||
|
||||
This document is kept for compatibility with earlier patches.
|
||||
This document is kept for compatibility. The preferred architecture is now the common Runtime Processing execution model.
|
||||
|
||||
The preferred endpoint is now the generic Runtime Event Processing endpoint:
|
||||
Preferred endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
POST /api/eventhub/runtime-processing/executions
|
||||
```
|
||||
|
||||
Use the tachograph profile key:
|
||||
Use:
|
||||
|
||||
```text
|
||||
tachograph-driver-esper-v1
|
||||
```json
|
||||
{
|
||||
"processingPlanKey": "driver-working-time-v1"
|
||||
}
|
||||
```
|
||||
|
||||
The old endpoint remains available as an adapter:
|
||||
Legacy compatibility endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/tachograph/esper-processing
|
||||
```
|
||||
|
||||
It delegates to the same profile infrastructure used by the generic endpoint. See:
|
||||
still exists and delegates through the common runtime processing infrastructure.
|
||||
|
||||
Legacy profile endpoint:
|
||||
|
||||
```http
|
||||
POST /api/eventhub/runtime-processing/event-processing
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```json
|
||||
{
|
||||
"profileKey": "tachograph-driver-esper-v1"
|
||||
}
|
||||
```
|
||||
|
||||
also remains available, but new clients should use `processingPlanKey` and `/executions`.
|
||||
|
||||
The current `driver-working-time-v1` plan uses these modules:
|
||||
|
||||
```text
|
||||
docs/runtime-event-processing.md
|
||||
event-to-activity-intervals
|
||||
event-to-vehicle-usage-intervals
|
||||
vehicle-evidence-attachment
|
||||
support-evidence-normalization
|
||||
driving-derived-projections
|
||||
```
|
||||
|
||||
Vehicle-only evidence is now attached through the same common runtime event-processing profile. The old compatibility endpoint maps:
|
||||
|
||||
```json
|
||||
{
|
||||
"expandVehicleEvents": true,
|
||||
"vehicleExpansionPaddingMinutes": 15
|
||||
}
|
||||
```
|
||||
|
||||
to the generic profile behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"parameters": {
|
||||
"attachVehicleOnlyEvents": true,
|
||||
"vehicleEvidencePaddingMinutes": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Attachment is temporal: a vehicle-only event must match a reconstructed driver vehicle-usage interval and occur inside the interval plus configured padding.
|
||||
|
||||
|
||||
## Debugging vehicle evidence attachment
|
||||
|
||||
Prefer the generic `/api/eventhub/runtime-processing/event-processing` endpoint with `partitioning.includeDebug=true` or `parameters.includePartitionDebug=true`. The compatibility response type has `partitionDebugByDriver`, but the generic endpoint is the preferred way to enable debug output explicitly. The generic response exposes debug data under `partitionResults[*].metadata.partitionDebug`.
|
||||
|
||||
## Support evidence normalization
|
||||
|
||||
The tachograph runtime profile is now implemented as a specialization of the generic runtime event-processing framework. Before calling the shared tachograph Esper core, mixed support events are normalized by `RuntimeSupportEvidenceNormalizer`.
|
||||
|
||||
This means that attached vehicle-only evidence from sources such as YellowFox ignition/position events can be consumed by the tachograph profile as support geo evidence when the event contains a position or odometer value. The provider-specific event meaning is preserved in the raw payload, while the Esper-facing event domain is adapted to the common tachograph support evidence contract.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"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."
|
||||
"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": [
|
||||
{
|
||||
|
|
@ -27,6 +27,138 @@
|
|||
}
|
||||
],
|
||||
"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": {
|
||||
|
|
@ -189,6 +321,232 @@
|
|||
"raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includeDebug\": true\n}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingServi
|
|||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.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;
|
||||
|
|
@ -14,7 +21,7 @@ import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
|||
import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService;
|
||||
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
|
||||
import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
|
@ -32,27 +39,29 @@ public class UnifiedRuntimeProcessingController {
|
|||
private final UnifiedRuntimeEventAssemblyService eventAssemblyService;
|
||||
private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService;
|
||||
private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService;
|
||||
private final UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService;
|
||||
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,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService
|
||||
) {
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null);
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public UnifiedRuntimeProcessingController(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService
|
||||
) {
|
||||
this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService,
|
||||
tachographEsperScopeProcessingService, runtimeEventProcessingService, null);
|
||||
tachographEsperScopeProcessingService, runtimeEventProcessingService, null, null, null);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
|
|
@ -60,16 +69,33 @@ public class UnifiedRuntimeProcessingController {
|
|||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService,
|
||||
UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService,
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService,
|
||||
RuntimeEventProcessingService runtimeEventProcessingService,
|
||||
RuntimeTachographParityValidationService tachographParityValidationService
|
||||
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")
|
||||
|
|
@ -93,6 +119,25 @@ public class UnifiedRuntimeProcessingController {
|
|||
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) {
|
||||
|
|
@ -122,6 +167,17 @@ public class UnifiedRuntimeProcessingController {
|
|||
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
|
||||
|
|
@ -135,6 +191,8 @@ public class UnifiedRuntimeProcessingController {
|
|||
if (tachographEsperScopeProcessingService == null) {
|
||||
throw new IllegalStateException("Tachograph Esper scope processing service is not configured.");
|
||||
}
|
||||
return ResponseEntity.ok(tachographEsperScopeProcessingService.processScope(request));
|
||||
return ResponseEntity.ok(UnifiedRuntimeTachographEsperScopeResultDto.fromDriverWorkingTime(
|
||||
tachographEsperScopeProcessingService.processScope(request)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package at.procon.eventhub.processing.dto;
|
|||
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
||||
import java.util.List;
|
||||
|
||||
public record UnifiedRuntimeDerivedProjectionResultDto(
|
||||
|
|
@ -12,8 +12,9 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
|
|||
int expandedVehicleEventCount,
|
||||
int mergedEventCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
TachographEsperDriverProcessingResultDto projection,
|
||||
DriverWorkingTimeProcessingResultDto projection,
|
||||
List<String> notes,
|
||||
RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization,
|
||||
RuntimeDriverPartitionDebugDto partitionDebug
|
||||
) {
|
||||
public UnifiedRuntimeDerivedProjectionResultDto {
|
||||
|
|
@ -28,7 +29,7 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
|
|||
int expandedVehicleEventCount,
|
||||
int mergedEventCount,
|
||||
List<UnifiedDiscoveredVehicleRef> discoveredVehicles,
|
||||
TachographEsperDriverProcessingResultDto projection,
|
||||
DriverWorkingTimeProcessingResultDto projection,
|
||||
List<String> notes
|
||||
) {
|
||||
this(
|
||||
|
|
@ -40,10 +41,38 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
|
|||
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,
|
||||
|
|
@ -54,6 +83,7 @@ public record UnifiedRuntimeDerivedProjectionResultDto(
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -28,6 +28,25 @@ public record UnifiedRuntimeTachographEsperScopeResultDto(
|
|||
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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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;
|
||||
|
|
@ -9,9 +10,21 @@ public record RuntimeEventProcessingPartitionResultDto(
|
|||
String partitionKey,
|
||||
String resultType,
|
||||
Object result,
|
||||
Map<String, Object> metadata
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -26,4 +27,26 @@ public record RuntimeEventProcessingResultDto(
|
|||
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,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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,35 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.profile;
|
||||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService;
|
||||
import java.util.LinkedHashMap;
|
||||
import 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.Map;
|
||||
import java.util.Set;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile {
|
||||
|
||||
public static final String PROFILE_KEY = "tachograph-driver-esper-v1";
|
||||
/**
|
||||
* Legacy compatibility key. New clients should use processingPlanKey=driver-working-time-v1
|
||||
* via /api/eventhub/runtime-processing/executions.
|
||||
*/
|
||||
public static final String PROFILE_KEY = DriverWorkingTimeRuntimeProcessingPlan.LEGACY_PROFILE_ALIAS;
|
||||
|
||||
private final UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService;
|
||||
private final DriverWorkingTimeRuntimeProcessingPlan plan;
|
||||
|
||||
public TachographDriverEsperRuntimeEventProcessingProfile(
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService
|
||||
) {
|
||||
this.tachographScopeProcessingService = tachographScopeProcessingService;
|
||||
@Autowired
|
||||
public TachographDriverEsperRuntimeEventProcessingProfile(DriverWorkingTimeRuntimeProcessingPlan plan) {
|
||||
this.plan = plan;
|
||||
}
|
||||
|
||||
public TachographDriverEsperRuntimeEventProcessingProfile(RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService) {
|
||||
this(new DriverWorkingTimeRuntimeProcessingPlan(scopeProcessingService));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -34,7 +39,7 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti
|
|||
|
||||
@Override
|
||||
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
|
||||
return RuntimeEventPartitioningStrategy.DRIVER;
|
||||
return plan.defaultPartitioningStrategy();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -44,202 +49,35 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti
|
|||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes. "
|
||||
+ "The profile partitions mixed runtime events by driver, attaches vehicle evidence by vehicle/time, "
|
||||
+ "normalizes mixed-source support evidence, and then invokes the event-input EPL pipeline.";
|
||||
return "Compatibility adapter for legacy profileKey=" + PROFILE_KEY
|
||||
+ ". New clients should use processingPlanKey=" + plan.processingPlanKey() + ". "
|
||||
+ plan.description();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RuntimeEventPartitioningStrategy> supportedPartitioningStrategies() {
|
||||
return List.of(RuntimeEventPartitioningStrategy.DRIVER);
|
||||
return plan.supportedPartitioningStrategies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> optionalParameters() {
|
||||
return Set.of(
|
||||
"significantDrivingMinutes",
|
||||
"minimumRestPeriodMinutes",
|
||||
"attachVehicleOnlyEvents",
|
||||
"vehicleEvidencePaddingMinutes",
|
||||
"includePartitionDebug"
|
||||
);
|
||||
return plan.optionalParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> requiredParameters() {
|
||||
return plan.requiredParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) {
|
||||
boolean includePartitionDebug = booleanParameter(
|
||||
request.parameters(),
|
||||
"includePartitionDebug",
|
||||
request.partitioning() != null && request.partitioning().includeDebugOrDefault()
|
||||
);
|
||||
UnifiedRuntimeProcessingApiRequest tachographScopeRequest = applyGenericRequest(request.scope(), request.partitioning(), request.parameters());
|
||||
UnifiedRuntimeTachographEsperScopeResultDto tachographResult = tachographScopeProcessingService.processScope(
|
||||
tachographScopeRequest,
|
||||
includePartitionDebug
|
||||
);
|
||||
|
||||
Map<String, RuntimeEventProcessingPartitionResultDto> partitionResults = new LinkedHashMap<>();
|
||||
tachographResult.driverResults().forEach((driverKey, driverResult) -> {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "TachographEsperDriverProcessingResultDto");
|
||||
metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount());
|
||||
metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount());
|
||||
metadata.put("mergedEventCount", driverResult.mergedEventCount());
|
||||
if (driverResult.partitionDebug() != null) {
|
||||
metadata.put("partitionDebug", driverResult.partitionDebug());
|
||||
}
|
||||
partitionResults.put(
|
||||
driverKey,
|
||||
new RuntimeEventProcessingPartitionResultDto(
|
||||
"DRIVER",
|
||||
driverKey,
|
||||
"UnifiedRuntimeDerivedProjectionResultDto",
|
||||
driverResult,
|
||||
metadata
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return new RuntimeEventProcessingResultDto(
|
||||
profileKey(),
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
tachographResult.request(),
|
||||
tachographResult.inputEventCount(),
|
||||
tachographResult.selectedDriverCount(),
|
||||
tachographResult.discoveredVehicleCount(),
|
||||
tachographResult.discoveredVehicles(),
|
||||
partitionResults,
|
||||
tachographResult.notes(),
|
||||
tachographResult.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
private UnifiedRuntimeProcessingApiRequest applyGenericRequest(
|
||||
UnifiedRuntimeProcessingApiRequest scope,
|
||||
RuntimeEventPartitioningApiRequest partitioning,
|
||||
Map<String, Object> parameters
|
||||
) {
|
||||
if (scope == null) {
|
||||
throw new IllegalArgumentException("scope must not be null for profile " + PROFILE_KEY + ".");
|
||||
}
|
||||
boolean includeAllDrivers = scope.includeAllDrivers() != null && scope.includeAllDrivers();
|
||||
java.util.Set<String> driverKeys = scope.driverKeys();
|
||||
boolean includeAllVehicles = scope.includeAllVehicles() != null && scope.includeAllVehicles();
|
||||
java.util.Set<String> vehicleKeys = scope.vehicleKeys();
|
||||
|
||||
if (partitioning != null) {
|
||||
RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null
|
||||
? RuntimeEventPartitioningStrategy.DRIVER
|
||||
: partitioning.strategy();
|
||||
if (strategy != RuntimeEventPartitioningStrategy.DRIVER
|
||||
&& strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) {
|
||||
throw new IllegalArgumentException("Profile " + PROFILE_KEY
|
||||
+ " currently supports DRIVER partitioning only. Requested: " + strategy);
|
||||
}
|
||||
includeAllDrivers = includeAllDrivers || partitioning.allPartitions() || partitioning.allDrivers();
|
||||
if (!partitioning.driverKeys().isEmpty()) {
|
||||
driverKeys = partitioning.driverKeys();
|
||||
} else if (!partitioning.partitionKeys().isEmpty()) {
|
||||
driverKeys = partitioning.partitionKeys();
|
||||
}
|
||||
includeAllVehicles = includeAllVehicles || partitioning.allVehicles();
|
||||
if (!partitioning.vehicleKeys().isEmpty()) {
|
||||
vehicleKeys = partitioning.vehicleKeys();
|
||||
}
|
||||
}
|
||||
|
||||
Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", scope.significantDrivingMinutes());
|
||||
Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", scope.minimumRestPeriodMinutes());
|
||||
boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents",
|
||||
partitioning == null ? scope.expandVehicleEvents() == null || scope.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault());
|
||||
Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes",
|
||||
partitioning == null
|
||||
? scope.vehicleExpansionPaddingMinutes()
|
||||
: partitioning.vehicleEvidencePaddingMinutesOrDefault(scope.vehicleExpansionPaddingMinutes() == null ? 0 : scope.vehicleExpansionPaddingMinutes()));
|
||||
|
||||
return new UnifiedRuntimeProcessingApiRequest(
|
||||
scope.sessionId(),
|
||||
scope.sessionIds(),
|
||||
scope.compositeSessionId(),
|
||||
scope.tenantKey(),
|
||||
scope.sourceFamilies(),
|
||||
scope.eventBackend(),
|
||||
scope.driverKey(),
|
||||
driverKeys,
|
||||
includeAllDrivers,
|
||||
vehicleKeys,
|
||||
includeAllVehicles,
|
||||
scope.driverSourceEntityId(),
|
||||
scope.driverCardNation(),
|
||||
scope.driverCardNumber(),
|
||||
scope.occurredFrom(),
|
||||
scope.occurredTo(),
|
||||
attachVehicleOnlyEvents,
|
||||
vehicleEvidencePaddingMinutes,
|
||||
significantDrivingMinutes,
|
||||
minimumRestPeriodMinutes
|
||||
);
|
||||
}
|
||||
|
||||
private boolean booleanParameter(Map<String, Object> parameters, String key, boolean fallback) {
|
||||
if (parameters == null || !parameters.containsKey(key)) {
|
||||
return fallback;
|
||||
}
|
||||
Object value = parameters.get(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (value instanceof Boolean booleanValue) {
|
||||
return booleanValue;
|
||||
}
|
||||
String text = value.toString();
|
||||
if (text == null || text.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return Boolean.parseBoolean(text.trim());
|
||||
}
|
||||
|
||||
private Integer nonNegativeIntegerParameter(Map<String, Object> parameters, String key, Integer fallback) {
|
||||
if (parameters == null || !parameters.containsKey(key)) {
|
||||
return fallback;
|
||||
}
|
||||
Object value = parameters.get(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return Math.max(0, number.intValue());
|
||||
}
|
||||
String text = value.toString();
|
||||
if (text == null || text.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Math.max(0, Integer.parseInt(text.trim()));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer integerParameter(Map<String, Object> parameters, String key, Integer fallback) {
|
||||
if (parameters == null || !parameters.containsKey(key)) {
|
||||
return fallback;
|
||||
}
|
||||
Object value = parameters.get(key);
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return Math.max(1, number.intValue());
|
||||
}
|
||||
String text = value.toString();
|
||||
if (text == null || text.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Math.max(1, Integer.parseInt(text.trim()));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex);
|
||||
}
|
||||
RuntimeProcessingExecutionResultDto executionResult = plan.execute(new RuntimeProcessingExecutionApiRequest(
|
||||
plan.processingPlanKey(),
|
||||
request.scope(),
|
||||
request.partitioning(),
|
||||
List.of(),
|
||||
request.parameters()
|
||||
));
|
||||
return RuntimeEventProcessingResultDto.fromExecution(executionResult, profileKey());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
|
@ -347,6 +348,10 @@ public class RuntimeTachographParityValidationService {
|
|||
}
|
||||
|
||||
static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) {
|
||||
return projection == null ? empty() : from(projection.toDriverWorkingTime());
|
||||
}
|
||||
|
||||
static ProjectionCounts from(DriverWorkingTimeProcessingResultDto projection) {
|
||||
if (projection == null) {
|
||||
return empty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,13 @@ 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.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
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;
|
||||
|
|
@ -24,7 +25,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsag
|
|||
import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore;
|
||||
import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput;
|
||||
import java.time.Duration;
|
||||
import java.time.OffsetDateTime;
|
||||
|
|
@ -44,7 +45,7 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
private final UnifiedEventTimelineReconstructor timelineReconstructor;
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder;
|
||||
private final TachographEsperProcessingCore esperProcessingCore;
|
||||
private final DriverWorkingTimeProcessingCore workingTimeProcessingCore;
|
||||
private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer;
|
||||
private final EventHubProperties properties;
|
||||
|
||||
|
|
@ -62,7 +63,7 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
driverTimelineBuilder,
|
||||
reusableProjectionBuilder,
|
||||
properties,
|
||||
new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties),
|
||||
new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)),
|
||||
supportEvidenceNormalizer
|
||||
);
|
||||
}
|
||||
|
|
@ -74,7 +75,7 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
DriverTimelineBuilder driverTimelineBuilder,
|
||||
DriverTimelineReusableProjectionBuilder reusableProjectionBuilder,
|
||||
EventHubProperties properties,
|
||||
TachographEsperProcessingCore esperProcessingCore,
|
||||
DriverWorkingTimeProcessingCore workingTimeProcessingCore,
|
||||
RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer
|
||||
) {
|
||||
this.runtimeEventAssemblyService = runtimeEventAssemblyService;
|
||||
|
|
@ -82,7 +83,7 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
this.driverTimelineBuilder = driverTimelineBuilder;
|
||||
this.reusableProjectionBuilder = reusableProjectionBuilder;
|
||||
this.properties = properties;
|
||||
this.esperProcessingCore = esperProcessingCore;
|
||||
this.workingTimeProcessingCore = workingTimeProcessingCore;
|
||||
this.supportEvidenceNormalizer = supportEvidenceNormalizer;
|
||||
}
|
||||
|
||||
|
|
@ -133,14 +134,14 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
|
||||
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 tachograph Esper processing core.");
|
||||
notes.add("Runtime derived projections were evaluated from the unified merged event stream using normalized support evidence and the shared driver working-time processing core.");
|
||||
notes.add("Significant driving threshold minutes: " + significantDrivingMinutes + ".");
|
||||
notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + ".");
|
||||
if (request.occurredFrom() != null || request.occurredTo() != null) {
|
||||
notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request.");
|
||||
}
|
||||
|
||||
TachographEsperDriverProcessingResultDto projection = esperProcessingCore.process(TachographEsperProcessingInput.fromEvents(
|
||||
DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(TachographEsperProcessingInput.fromEvents(
|
||||
runtimeSessionId(request),
|
||||
driverKey,
|
||||
timeline,
|
||||
|
|
@ -153,6 +154,13 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
));
|
||||
notes = projection.notes();
|
||||
|
||||
RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto(
|
||||
normalizationResult.inputEventCount(),
|
||||
normalizationResult.normalizedSupportEvidenceEventCount(),
|
||||
normalizationResult.unchangedEventCount(),
|
||||
normalizationResult.notes()
|
||||
);
|
||||
|
||||
return new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
request,
|
||||
eventBundle.driverSeedEvents().size(),
|
||||
|
|
@ -161,7 +169,8 @@ public class UnifiedRuntimeDerivedProjectionService {
|
|||
eventBundle.mergedEvents().size(),
|
||||
eventBundle.discoveredVehicles(),
|
||||
projection,
|
||||
notes
|
||||
notes,
|
||||
normalizationDebug
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,319 +1,48 @@
|
|||
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.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef;
|
||||
import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
/**
|
||||
* @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 UnifiedRuntimeEventAssemblyService eventAssemblyService;
|
||||
private final UnifiedRuntimeDerivedProjectionService derivedProjectionService;
|
||||
private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService;
|
||||
private final RuntimeDriverWorkingTimeScopeProcessingService delegate;
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeProcessingService(
|
||||
UnifiedRuntimeEventAssemblyService eventAssemblyService,
|
||||
UnifiedRuntimeDerivedProjectionService derivedProjectionService,
|
||||
RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService
|
||||
RuntimeDriverWorkingTimeScopeProcessingService delegate
|
||||
) {
|
||||
this.eventAssemblyService = eventAssemblyService;
|
||||
this.derivedProjectionService = derivedProjectionService;
|
||||
this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) {
|
||||
return processScope(apiRequest, false);
|
||||
return toLegacy(delegate.processScope(apiRequest));
|
||||
}
|
||||
|
||||
public UnifiedRuntimeTachographEsperScopeResultDto 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 tachograph Esper scope processing used Java-side driver partitioning before calling the common event-input Esper pipeline.");
|
||||
notes.add("Selected driver partitions: " + selectedDriverKeys.size() + ".");
|
||||
if (!request.includeAllDrivers() && !request.driverKeys().isEmpty()) {
|
||||
notes.add("The broad runtime event set was filtered to the requested driverKeys.");
|
||||
}
|
||||
if (!request.vehicleKeys().isEmpty() || request.includeAllVehicles()) {
|
||||
notes.add("vehicleKeys/includeAllVehicles are accepted in the request model for source-neutral scopes; driver partitions are enriched only with vehicle evidence that overlaps reconstructed driver vehicle-usage intervals.");
|
||||
}
|
||||
notes.add("Vehicle-only evidence attachment is controlled by expandVehicleEvents/attachVehicleOnlyEvents and vehicleExpansionPaddingMinutes/vehicleEvidencePaddingMinutes.");
|
||||
return toLegacy(delegate.processScope(apiRequest, includePartitionDebug));
|
||||
}
|
||||
|
||||
private UnifiedRuntimeTachographEsperScopeResultDto toLegacy(
|
||||
UnifiedRuntimeDriverWorkingTimeScopeResultDto result
|
||||
) {
|
||||
return new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
request,
|
||||
broadBundle.mergedEvents().size(),
|
||||
driverResults.size(),
|
||||
broadBundle.discoveredVehicles().size(),
|
||||
broadBundle.discoveredVehicles(),
|
||||
driverResults,
|
||||
partitionDebugByDriver,
|
||||
notes,
|
||||
warnings
|
||||
result.request(),
|
||||
result.inputEventCount(),
|
||||
result.selectedDriverCount(),
|
||||
result.discoveredVehicleCount(),
|
||||
result.discoveredVehicles(),
|
||||
result.driverResults(),
|
||||
result.partitionDebugByDriver(),
|
||||
result.notes(),
|
||||
result.warnings()
|
||||
);
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> selectedDriverKeys(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
List<EventHubEventDto> events
|
||||
) {
|
||||
LinkedHashSet<String> allDrivers = discoverDriverKeys(events);
|
||||
if (request.includeAllDrivers()) {
|
||||
return allDrivers;
|
||||
}
|
||||
LinkedHashSet<String> requested = new LinkedHashSet<>(request.driverKeys());
|
||||
if (request.driverKey() != null) {
|
||||
requested.add(request.driverKey());
|
||||
}
|
||||
if (requested.isEmpty()) {
|
||||
return allDrivers;
|
||||
}
|
||||
LinkedHashSet<String> selected = new LinkedHashSet<>();
|
||||
for (String driverKey : allDrivers) {
|
||||
if (requested.contains(driverKey)) {
|
||||
selected.add(driverKey);
|
||||
}
|
||||
}
|
||||
for (String driverKey : requested) {
|
||||
selected.add(driverKey);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private DriverPartition partitionForDriver(
|
||||
UnifiedRuntimeProcessingRequest request,
|
||||
UnifiedRuntimeEventBundle broadBundle,
|
||||
String driverKey,
|
||||
boolean includePartitionDebug
|
||||
) {
|
||||
List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
|
||||
.filter(event -> Objects.equals(driverKey(event), driverKey))
|
||||
.toList();
|
||||
RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
|
||||
driverKey,
|
||||
directDriverEvents,
|
||||
broadBundle.mergedEvents(),
|
||||
request.expandVehicleEvents(),
|
||||
request.vehicleExpansionPaddingMinutes(),
|
||||
includePartitionDebug
|
||||
);
|
||||
List<UnifiedDiscoveredVehicleRef> driverVehicles = discoverVehicles(attachmentResult.mergedEvents());
|
||||
List<String> notes = new ArrayList<>(broadBundle.notes());
|
||||
notes.add("Partitioned mixed runtime event scope for driver " + driverKey + ".");
|
||||
notes.add("Driver direct events: " + attachmentResult.directDriverEvents().size() + ".");
|
||||
notes.add("Vehicle-only evidence events attached to driver partition: " + attachmentResult.attachedVehicleEvidenceEvents().size() + ".");
|
||||
notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + ".");
|
||||
notes.addAll(attachmentResult.notes());
|
||||
attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning));
|
||||
UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle(
|
||||
request.withDriverKey(driverKey),
|
||||
attachmentResult.directDriverEvents(),
|
||||
driverVehicles,
|
||||
attachmentResult.attachedVehicleEvidenceEvents(),
|
||||
attachmentResult.mergedEvents(),
|
||||
notes
|
||||
);
|
||||
return new DriverPartition(
|
||||
bundle,
|
||||
includePartitionDebug ? attachmentResult.toPartitionDebug() : null
|
||||
);
|
||||
}
|
||||
|
||||
private record DriverPartition(
|
||||
UnifiedRuntimeEventBundle bundle,
|
||||
RuntimeDriverPartitionDebugDto debug
|
||||
) {
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> discoverDriverKeys(List<EventHubEventDto> events) {
|
||||
LinkedHashSet<String> result = new LinkedHashSet<>();
|
||||
for (EventHubEventDto event : sort(events)) {
|
||||
String driverKey = driverKey(event);
|
||||
if (driverKey != null) {
|
||||
result.add(driverKey);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
|
||||
List<UnifiedDiscoveredVehicleRef> result = new ArrayList<>();
|
||||
for (EventHubEventDto event : events) {
|
||||
UnifiedDiscoveredVehicleRef candidate = vehicleRef(event.vehicleRef());
|
||||
if (candidate == null || !candidate.hasAnyReference()) {
|
||||
continue;
|
||||
}
|
||||
boolean merged = false;
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
UnifiedDiscoveredVehicleRef existing = result.get(i);
|
||||
if (existing.matches(candidate)) {
|
||||
result.set(i, existing.merge(candidate));
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!merged) {
|
||||
result.add(candidate);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(UnifiedDiscoveredVehicleRef::stableKey));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private boolean matchesAnyVehicle(VehicleRefDto vehicleRef, List<UnifiedDiscoveredVehicleRef> vehicles) {
|
||||
UnifiedDiscoveredVehicleRef candidate = vehicleRef(vehicleRef);
|
||||
if (candidate == null || !candidate.hasAnyReference()) {
|
||||
return false;
|
||||
}
|
||||
return vehicles.stream().anyMatch(vehicle -> vehicle.matches(candidate));
|
||||
}
|
||||
|
||||
private UnifiedDiscoveredVehicleRef vehicleRef(VehicleRefDto vehicleRef) {
|
||||
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
|
||||
return null;
|
||||
}
|
||||
return new UnifiedDiscoveredVehicleRef(
|
||||
vehicleRef.sourceVehicleEntityId(),
|
||||
vehicleRef.vin(),
|
||||
vehicleRef.vehicleRegistration() == null
|
||||
? null
|
||||
: vehicleRef.vehicleRegistration().nationNumericCode() == null
|
||||
? vehicleRef.vehicleRegistration().nation()
|
||||
: vehicleRef.vehicleRegistration().nationNumericCode().toString(),
|
||||
vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number()
|
||||
);
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> deduplicateAndSort(
|
||||
List<EventHubEventDto> directDriverEvents,
|
||||
List<EventHubEventDto> vehicleEvidenceEvents
|
||||
) {
|
||||
LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
|
||||
appendDeduplicated(byKey, directDriverEvents);
|
||||
appendDeduplicated(byKey, vehicleEvidenceEvents);
|
||||
return sort(new ArrayList<>(byKey.values()));
|
||||
}
|
||||
|
||||
private void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
|
||||
for (EventHubEventDto event : events) {
|
||||
byKey.putIfAbsent(dedupKey(event), event);
|
||||
}
|
||||
}
|
||||
|
||||
private String dedupKey(EventHubEventDto event) {
|
||||
String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null
|
||||
? event.packageInfo().eventSource().stableKey()
|
||||
: "NO_SOURCE";
|
||||
return sourceKey + "|" + event.externalSourceEventId();
|
||||
}
|
||||
|
||||
private List<EventHubEventDto> sort(List<EventHubEventDto> events) {
|
||||
return (events == null ? List.<EventHubEventDto>of() : events).stream()
|
||||
.sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name())
|
||||
.thenComparing(event -> event.eventType() == null ? "" : event.eventType().name())
|
||||
.thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name())
|
||||
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String driverKey(EventHubEventDto event) {
|
||||
if (event == null) {
|
||||
return null;
|
||||
}
|
||||
String rawDriverKey = text(rawPayload(event), "driverKey");
|
||||
if (rawDriverKey != null) {
|
||||
return rawDriverKey;
|
||||
}
|
||||
DriverRefDto driverRef = event.driverRef();
|
||||
if (driverRef != null && driverRef.hasAnyReference()) {
|
||||
return driverRef.stableKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private JsonNode rawPayload(EventHubEventDto event) {
|
||||
JsonNode payload = event.payload();
|
||||
if (payload == null || payload.isNull()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode raw = payload.get("raw");
|
||||
return raw == null || raw.isNull() ? payload : raw;
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || field == null) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
String text = value.asText(null);
|
||||
return text == null || text.isBlank() ? null : text.trim();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ 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/tachograph-driving-derived-projection-events-preprocessor.epl");
|
||||
loadResource("esper/runtime-driver-event-interval-preprocessor.epl");
|
||||
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto;
|
||||
import at.procon.eventhub.tachographfilesession.model.*;
|
||||
import java.time.Duration;
|
||||
|
|
@ -12,6 +13,7 @@ import java.util.Objects;
|
|||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Deprecated(forRemoval = false)
|
||||
public class TachographEsperProcessingCore {
|
||||
|
||||
private final DriverTimelineBuilder driverTimelineBuilder;
|
||||
|
|
@ -29,6 +31,10 @@ public class TachographEsperProcessingCore {
|
|||
}
|
||||
|
||||
public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) {
|
||||
return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(processDriverWorkingTime(input));
|
||||
}
|
||||
|
||||
public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) {
|
||||
Objects.requireNonNull(input, "input must not be null");
|
||||
ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null");
|
||||
String driverKey = input.driverKey();
|
||||
|
|
@ -132,7 +138,7 @@ public class TachographEsperProcessingCore {
|
|||
requestedTo
|
||||
);
|
||||
|
||||
return new TachographEsperDriverProcessingResultDto(
|
||||
return new DriverWorkingTimeProcessingResultDto(
|
||||
input.sessionId(),
|
||||
driverKey,
|
||||
timeline.sourceKind(),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Source-neutral EPL module: DriverActivityPointEvent -> DriverActivityIntervalEvent.
|
||||
* This module is intended for Runtime Processing plans and is independent of any concrete source provider.
|
||||
*/
|
||||
|
||||
create schema DriverActivityIntervalEvent(
|
||||
sessionId java.util.UUID,
|
||||
driverKey string,
|
||||
intervalId string,
|
||||
activityType string,
|
||||
cardSlot string,
|
||||
cardStatus string,
|
||||
drivingStatus string,
|
||||
registrationKey string,
|
||||
vehicleKey string,
|
||||
sourceKind string,
|
||||
firstSourceIntervalId string,
|
||||
lastSourceIntervalId string,
|
||||
startedAt java.time.OffsetDateTime,
|
||||
endedAt java.time.OffsetDateTime,
|
||||
startedAtEpochSecond long,
|
||||
endedAtEpochSecond long,
|
||||
durationSeconds long,
|
||||
sourceIntervalIds java.util.List,
|
||||
synthetic boolean,
|
||||
clippedToRequestedPeriod boolean,
|
||||
level string
|
||||
);
|
||||
|
||||
create window OpenDriverActivityPoint#unique(driverKey, intervalId) as DriverActivityPointEvent;
|
||||
|
||||
insert into OpenDriverActivityPoint
|
||||
select *
|
||||
from DriverActivityPointEvent(lifecycle = 'START');
|
||||
|
||||
@Priority(20)
|
||||
on DriverActivityPointEvent(lifecycle = 'END') as endEvent
|
||||
insert into DriverActivityIntervalEvent
|
||||
select
|
||||
startEvent.sessionId as sessionId,
|
||||
startEvent.driverKey as driverKey,
|
||||
startEvent.intervalId as intervalId,
|
||||
startEvent.activityType as activityType,
|
||||
startEvent.cardSlot as cardSlot,
|
||||
startEvent.cardStatus as cardStatus,
|
||||
startEvent.drivingStatus as drivingStatus,
|
||||
startEvent.registrationKey as registrationKey,
|
||||
startEvent.vehicleKey as vehicleKey,
|
||||
startEvent.sourceKind as sourceKind,
|
||||
startEvent.sourceRowId as firstSourceIntervalId,
|
||||
endEvent.sourceRowId as lastSourceIntervalId,
|
||||
startEvent.occurredAt as startedAt,
|
||||
endEvent.occurredAt as endedAt,
|
||||
startEvent.occurredAtEpochSecond as startedAtEpochSecond,
|
||||
endEvent.occurredAtEpochSecond as endedAtEpochSecond,
|
||||
endEvent.occurredAtEpochSecond - startEvent.occurredAtEpochSecond as durationSeconds,
|
||||
startEvent.sourceRowIds as sourceIntervalIds,
|
||||
startEvent.synthetic as synthetic,
|
||||
startEvent.clippedToRequestedPeriod as clippedToRequestedPeriod,
|
||||
startEvent.level as level
|
||||
from OpenDriverActivityPoint as startEvent
|
||||
where startEvent.driverKey = endEvent.driverKey
|
||||
and startEvent.intervalId = endEvent.intervalId
|
||||
and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond;
|
||||
|
||||
@Priority(10)
|
||||
on DriverActivityPointEvent(lifecycle = 'END') as endEvent
|
||||
delete from OpenDriverActivityPoint as openEvent
|
||||
where openEvent.driverKey = endEvent.driverKey
|
||||
and openEvent.intervalId = endEvent.intervalId;
|
||||
|
||||
@name('driverActivityIntervals')
|
||||
select *
|
||||
from DriverActivityIntervalEvent;
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
* Source-neutral event-input adapter for driver-working-time-derived-projections.epl.
|
||||
*
|
||||
* The old bundle consumes resolved interval input streams. This preprocessor lets the same
|
||||
* derived rules consume EventHub point events by pairing START/END activity events and
|
||||
* INSERT/WITHDRAW card-vehicle usage events inside Esper.
|
||||
*
|
||||
* Vehicle-usage intervals are additionally coalesced in EPL before they are forwarded to
|
||||
* the old interval bundle. This keeps event mode equivalent to DriverTimelineBuilder's
|
||||
* Java merge behavior for consecutive same-vehicle CardVehiclesUsed rows, including the
|
||||
* common midnight continuation 23:59:59 -> 00:00:00 on the next day.
|
||||
*/
|
||||
|
||||
create window OpenActivityPoint#unique(driverKey, intervalId) as TachographActivityPointInputEvent;
|
||||
|
||||
insert into OpenActivityPoint
|
||||
select *
|
||||
from TachographActivityPointInputEvent(lifecycle = 'START');
|
||||
|
||||
@Priority(20)
|
||||
on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent
|
||||
insert into TachographActivityIntervalInputEvent
|
||||
select
|
||||
startEvent.sessionId as sessionId,
|
||||
startEvent.driverKey as driverKey,
|
||||
startEvent.intervalId as intervalId,
|
||||
startEvent.activityType as activityType,
|
||||
startEvent.cardSlot as cardSlot,
|
||||
startEvent.cardStatus as cardStatus,
|
||||
startEvent.drivingStatus as drivingStatus,
|
||||
startEvent.registrationKey as registrationKey,
|
||||
startEvent.vehicleKey as vehicleKey,
|
||||
startEvent.sourceKind as sourceKind,
|
||||
startEvent.sourceRowId as firstSourceIntervalId,
|
||||
endEvent.sourceRowId as lastSourceIntervalId,
|
||||
startEvent.occurredAt as startedAt,
|
||||
endEvent.occurredAt as endedAt,
|
||||
startEvent.occurredAtEpochSecond as startedAtEpochSecond,
|
||||
endEvent.occurredAtEpochSecond as endedAtEpochSecond,
|
||||
endEvent.occurredAtEpochSecond - startEvent.occurredAtEpochSecond as durationSeconds,
|
||||
startEvent.sourceRowIds as sourceIntervalIds,
|
||||
startEvent.synthetic as synthetic,
|
||||
startEvent.clippedToRequestedPeriod as clippedToRequestedPeriod,
|
||||
startEvent.level as level
|
||||
from OpenActivityPoint as startEvent
|
||||
where startEvent.driverKey = endEvent.driverKey
|
||||
and startEvent.intervalId = endEvent.intervalId
|
||||
and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond;
|
||||
|
||||
@Priority(10)
|
||||
on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent
|
||||
delete from OpenActivityPoint as openEvent
|
||||
where openEvent.driverKey = endEvent.driverKey
|
||||
and openEvent.intervalId = endEvent.intervalId;
|
||||
|
||||
create schema RawVehicleUsageInterval(
|
||||
sessionId java.util.UUID,
|
||||
driverKey string,
|
||||
intervalId string,
|
||||
firstSourceIntervalId string,
|
||||
lastSourceIntervalId string,
|
||||
startedAt java.time.OffsetDateTime,
|
||||
endedAt java.time.OffsetDateTime,
|
||||
startedAtEpochSecond long,
|
||||
endedAtEpochSecond java.lang.Long,
|
||||
durationSeconds long,
|
||||
odometerBeginKm java.lang.Long,
|
||||
odometerEndKm java.lang.Long,
|
||||
registrationKey string,
|
||||
vehicleKey string,
|
||||
sourceKind string,
|
||||
sourceIntervalIds java.util.List
|
||||
);
|
||||
|
||||
create window OpenVehicleUsagePoint#unique(driverKey, intervalId) as TachographVehicleUsagePointInputEvent;
|
||||
|
||||
insert into OpenVehicleUsagePoint
|
||||
select *
|
||||
from TachographVehicleUsagePointInputEvent(lifecycle = 'INSERT');
|
||||
|
||||
@Priority(20)
|
||||
on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
||||
insert into RawVehicleUsageInterval
|
||||
select
|
||||
insertEvent.sessionId as sessionId,
|
||||
insertEvent.driverKey as driverKey,
|
||||
insertEvent.intervalId as intervalId,
|
||||
insertEvent.sourceRowId as firstSourceIntervalId,
|
||||
withdrawEvent.sourceRowId as lastSourceIntervalId,
|
||||
insertEvent.occurredAt as startedAt,
|
||||
withdrawEvent.occurredAt as endedAt,
|
||||
insertEvent.occurredAtEpochSecond as startedAtEpochSecond,
|
||||
withdrawEvent.occurredAtEpochSecond as endedAtEpochSecond,
|
||||
withdrawEvent.occurredAtEpochSecond - insertEvent.occurredAtEpochSecond as durationSeconds,
|
||||
insertEvent.odometerKm as odometerBeginKm,
|
||||
withdrawEvent.odometerKm as odometerEndKm,
|
||||
insertEvent.registrationKey as registrationKey,
|
||||
insertEvent.vehicleKey as vehicleKey,
|
||||
insertEvent.sourceKind as sourceKind,
|
||||
insertEvent.sourceRowIds as sourceIntervalIds
|
||||
from OpenVehicleUsagePoint as insertEvent
|
||||
where insertEvent.driverKey = withdrawEvent.driverKey
|
||||
and insertEvent.intervalId = withdrawEvent.intervalId
|
||||
and withdrawEvent.occurredAtEpochSecond > insertEvent.occurredAtEpochSecond;
|
||||
|
||||
@Priority(10)
|
||||
on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
||||
delete from OpenVehicleUsagePoint as openEvent
|
||||
where openEvent.driverKey = withdrawEvent.driverKey
|
||||
and openEvent.intervalId = withdrawEvent.intervalId;
|
||||
|
||||
create window MergedVehicleUsageAccumulator#unique(driverKey) as RawVehicleUsageInterval;
|
||||
create window MergedVehicleUsageNext#unique(driverKey) as RawVehicleUsageInterval;
|
||||
|
||||
/*
|
||||
* Case 1: first vehicle-usage interval for this driver. Start a new accumulator.
|
||||
*/
|
||||
@Priority(70)
|
||||
on RawVehicleUsageInterval as next
|
||||
insert into MergedVehicleUsageNext
|
||||
select
|
||||
next.sessionId as sessionId,
|
||||
next.driverKey as driverKey,
|
||||
next.intervalId as intervalId,
|
||||
next.firstSourceIntervalId as firstSourceIntervalId,
|
||||
next.lastSourceIntervalId as lastSourceIntervalId,
|
||||
next.startedAt as startedAt,
|
||||
next.endedAt as endedAt,
|
||||
next.startedAtEpochSecond as startedAtEpochSecond,
|
||||
next.endedAtEpochSecond as endedAtEpochSecond,
|
||||
next.durationSeconds as durationSeconds,
|
||||
next.odometerBeginKm as odometerBeginKm,
|
||||
next.odometerEndKm as odometerEndKm,
|
||||
next.registrationKey as registrationKey,
|
||||
next.vehicleKey as vehicleKey,
|
||||
next.sourceKind as sourceKind,
|
||||
next.sourceIntervalIds as sourceIntervalIds
|
||||
where not exists (
|
||||
select * from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = next.driverKey
|
||||
);
|
||||
|
||||
/*
|
||||
* Case 2: same vehicle and registration, and the next interval starts at or before
|
||||
* current.to + 1 second. This includes 23:59:59 -> 00:00:00. Keep one accumulated
|
||||
* interval and extend its end/last-source/ending odometer.
|
||||
*/
|
||||
@Priority(70)
|
||||
on RawVehicleUsageInterval as next
|
||||
insert into MergedVehicleUsageNext
|
||||
select
|
||||
current.sessionId as sessionId,
|
||||
current.driverKey as driverKey,
|
||||
current.intervalId as intervalId,
|
||||
current.firstSourceIntervalId as firstSourceIntervalId,
|
||||
next.lastSourceIntervalId as lastSourceIntervalId,
|
||||
current.startedAt as startedAt,
|
||||
next.endedAt as endedAt,
|
||||
current.startedAtEpochSecond as startedAtEpochSecond,
|
||||
next.endedAtEpochSecond as endedAtEpochSecond,
|
||||
next.endedAtEpochSecond - current.startedAtEpochSecond as durationSeconds,
|
||||
current.odometerBeginKm as odometerBeginKm,
|
||||
next.odometerEndKm as odometerEndKm,
|
||||
current.registrationKey as registrationKey,
|
||||
current.vehicleKey as vehicleKey,
|
||||
current.sourceKind as sourceKind,
|
||||
current.sourceIntervalIds as sourceIntervalIds
|
||||
from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = next.driverKey
|
||||
and current.endedAtEpochSecond is not null
|
||||
and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L
|
||||
and (
|
||||
(current.registrationKey is null and next.registrationKey is null)
|
||||
or (
|
||||
current.registrationKey is not null
|
||||
and next.registrationKey is not null
|
||||
and current.registrationKey = next.registrationKey
|
||||
)
|
||||
)
|
||||
and (
|
||||
(current.vehicleKey is null and next.vehicleKey is null)
|
||||
or (
|
||||
current.vehicleKey is not null
|
||||
and next.vehicleKey is not null
|
||||
and current.vehicleKey = next.vehicleKey
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Case 3a: next interval is not mergeable. Emit the accumulated interval to the old
|
||||
* interval-based bundle.
|
||||
*/
|
||||
@Priority(70)
|
||||
on RawVehicleUsageInterval as next
|
||||
insert into TachographVehicleUsageIntervalInputEvent
|
||||
select
|
||||
current.sessionId as sessionId,
|
||||
current.driverKey as driverKey,
|
||||
current.intervalId as intervalId,
|
||||
current.firstSourceIntervalId as firstSourceIntervalId,
|
||||
current.lastSourceIntervalId as lastSourceIntervalId,
|
||||
current.startedAt as startedAt,
|
||||
current.endedAt as endedAt,
|
||||
current.startedAtEpochSecond as startedAtEpochSecond,
|
||||
current.endedAtEpochSecond as endedAtEpochSecond,
|
||||
current.durationSeconds as durationSeconds,
|
||||
current.odometerBeginKm as odometerBeginKm,
|
||||
current.odometerEndKm as odometerEndKm,
|
||||
current.registrationKey as registrationKey,
|
||||
current.vehicleKey as vehicleKey,
|
||||
current.sourceKind as sourceKind,
|
||||
current.sourceIntervalIds as sourceIntervalIds
|
||||
from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = next.driverKey
|
||||
and not (
|
||||
current.endedAtEpochSecond is not null
|
||||
and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L
|
||||
and (
|
||||
(current.registrationKey is null and next.registrationKey is null)
|
||||
or (
|
||||
current.registrationKey is not null
|
||||
and next.registrationKey is not null
|
||||
and current.registrationKey = next.registrationKey
|
||||
)
|
||||
)
|
||||
and (
|
||||
(current.vehicleKey is null and next.vehicleKey is null)
|
||||
or (
|
||||
current.vehicleKey is not null
|
||||
and next.vehicleKey is not null
|
||||
and current.vehicleKey = next.vehicleKey
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Case 3b: next interval is not mergeable. Start a new accumulator with it.
|
||||
*/
|
||||
@Priority(70)
|
||||
on RawVehicleUsageInterval as next
|
||||
insert into MergedVehicleUsageNext
|
||||
select
|
||||
next.sessionId as sessionId,
|
||||
next.driverKey as driverKey,
|
||||
next.intervalId as intervalId,
|
||||
next.firstSourceIntervalId as firstSourceIntervalId,
|
||||
next.lastSourceIntervalId as lastSourceIntervalId,
|
||||
next.startedAt as startedAt,
|
||||
next.endedAt as endedAt,
|
||||
next.startedAtEpochSecond as startedAtEpochSecond,
|
||||
next.endedAtEpochSecond as endedAtEpochSecond,
|
||||
next.durationSeconds as durationSeconds,
|
||||
next.odometerBeginKm as odometerBeginKm,
|
||||
next.odometerEndKm as odometerEndKm,
|
||||
next.registrationKey as registrationKey,
|
||||
next.vehicleKey as vehicleKey,
|
||||
next.sourceKind as sourceKind,
|
||||
next.sourceIntervalIds as sourceIntervalIds
|
||||
where exists (
|
||||
select * from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = next.driverKey
|
||||
and not (
|
||||
current.endedAtEpochSecond is not null
|
||||
and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L
|
||||
and (
|
||||
(current.registrationKey is null and next.registrationKey is null)
|
||||
or (
|
||||
current.registrationKey is not null
|
||||
and next.registrationKey is not null
|
||||
and current.registrationKey = next.registrationKey
|
||||
)
|
||||
)
|
||||
and (
|
||||
(current.vehicleKey is null and next.vehicleKey is null)
|
||||
or (
|
||||
current.vehicleKey is not null
|
||||
and next.vehicleKey is not null
|
||||
and current.vehicleKey = next.vehicleKey
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@Priority(60)
|
||||
on RawVehicleUsageInterval as next
|
||||
delete from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = next.driverKey;
|
||||
|
||||
@Priority(50)
|
||||
on RawVehicleUsageInterval as next
|
||||
insert into MergedVehicleUsageAccumulator
|
||||
select
|
||||
candidate.sessionId as sessionId,
|
||||
candidate.driverKey as driverKey,
|
||||
candidate.intervalId as intervalId,
|
||||
candidate.firstSourceIntervalId as firstSourceIntervalId,
|
||||
candidate.lastSourceIntervalId as lastSourceIntervalId,
|
||||
candidate.startedAt as startedAt,
|
||||
candidate.endedAt as endedAt,
|
||||
candidate.startedAtEpochSecond as startedAtEpochSecond,
|
||||
candidate.endedAtEpochSecond as endedAtEpochSecond,
|
||||
candidate.durationSeconds as durationSeconds,
|
||||
candidate.odometerBeginKm as odometerBeginKm,
|
||||
candidate.odometerEndKm as odometerEndKm,
|
||||
candidate.registrationKey as registrationKey,
|
||||
candidate.vehicleKey as vehicleKey,
|
||||
candidate.sourceKind as sourceKind,
|
||||
candidate.sourceIntervalIds as sourceIntervalIds
|
||||
from MergedVehicleUsageNext as candidate
|
||||
where candidate.driverKey = next.driverKey;
|
||||
|
||||
@Priority(40)
|
||||
on RawVehicleUsageInterval as next
|
||||
delete from MergedVehicleUsageNext as candidate
|
||||
where candidate.driverKey = next.driverKey;
|
||||
|
||||
/*
|
||||
* The last accumulated interval cannot be emitted by looking at the next interval, so Java
|
||||
* sends one TachographProjectionFinalizeEvent per driver after all vehicle-usage point
|
||||
* events and before activity point events.
|
||||
*/
|
||||
@Priority(20)
|
||||
on TachographProjectionFinalizeEvent as finalizeEvent
|
||||
insert into TachographVehicleUsageIntervalInputEvent
|
||||
select
|
||||
current.sessionId as sessionId,
|
||||
current.driverKey as driverKey,
|
||||
current.intervalId as intervalId,
|
||||
current.firstSourceIntervalId as firstSourceIntervalId,
|
||||
current.lastSourceIntervalId as lastSourceIntervalId,
|
||||
current.startedAt as startedAt,
|
||||
current.endedAt as endedAt,
|
||||
current.startedAtEpochSecond as startedAtEpochSecond,
|
||||
current.endedAtEpochSecond as endedAtEpochSecond,
|
||||
current.durationSeconds as durationSeconds,
|
||||
current.odometerBeginKm as odometerBeginKm,
|
||||
current.odometerEndKm as odometerEndKm,
|
||||
current.registrationKey as registrationKey,
|
||||
current.vehicleKey as vehicleKey,
|
||||
current.sourceKind as sourceKind,
|
||||
current.sourceIntervalIds as sourceIntervalIds
|
||||
from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = finalizeEvent.driverKey;
|
||||
|
||||
@Priority(10)
|
||||
on TachographProjectionFinalizeEvent as finalizeEvent
|
||||
delete from MergedVehicleUsageAccumulator as current
|
||||
where current.driverKey = finalizeEvent.driverKey;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Source-neutral EPL module: DriverVehicleUsagePointEvent -> DriverVehicleUsageIntervalEvent.
|
||||
* Pairs INSERT/WITHDRAW point events for the same driver and interval id.
|
||||
*/
|
||||
|
||||
create schema DriverVehicleUsageIntervalEvent(
|
||||
sessionId java.util.UUID,
|
||||
driverKey string,
|
||||
intervalId string,
|
||||
firstSourceIntervalId string,
|
||||
lastSourceIntervalId string,
|
||||
startedAt java.time.OffsetDateTime,
|
||||
endedAt java.time.OffsetDateTime,
|
||||
startedAtEpochSecond long,
|
||||
endedAtEpochSecond java.lang.Long,
|
||||
durationSeconds long,
|
||||
odometerBeginKm java.lang.Long,
|
||||
odometerEndKm java.lang.Long,
|
||||
registrationKey string,
|
||||
vehicleKey string,
|
||||
sourceKind string,
|
||||
sourceIntervalIds java.util.List
|
||||
);
|
||||
|
||||
create window OpenDriverVehicleUsagePoint#unique(driverKey, intervalId) as DriverVehicleUsagePointEvent;
|
||||
|
||||
insert into OpenDriverVehicleUsagePoint
|
||||
select *
|
||||
from DriverVehicleUsagePointEvent(lifecycle = 'INSERT');
|
||||
|
||||
@Priority(20)
|
||||
on DriverVehicleUsagePointEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
||||
insert into DriverVehicleUsageIntervalEvent
|
||||
select
|
||||
insertEvent.sessionId as sessionId,
|
||||
insertEvent.driverKey as driverKey,
|
||||
insertEvent.intervalId as intervalId,
|
||||
insertEvent.sourceRowId as firstSourceIntervalId,
|
||||
withdrawEvent.sourceRowId as lastSourceIntervalId,
|
||||
insertEvent.occurredAt as startedAt,
|
||||
withdrawEvent.occurredAt as endedAt,
|
||||
insertEvent.occurredAtEpochSecond as startedAtEpochSecond,
|
||||
withdrawEvent.occurredAtEpochSecond as endedAtEpochSecond,
|
||||
withdrawEvent.occurredAtEpochSecond - insertEvent.occurredAtEpochSecond as durationSeconds,
|
||||
insertEvent.odometerKm as odometerBeginKm,
|
||||
withdrawEvent.odometerKm as odometerEndKm,
|
||||
insertEvent.registrationKey as registrationKey,
|
||||
insertEvent.vehicleKey as vehicleKey,
|
||||
insertEvent.sourceKind as sourceKind,
|
||||
insertEvent.sourceRowIds as sourceIntervalIds
|
||||
from OpenDriverVehicleUsagePoint as insertEvent
|
||||
where insertEvent.driverKey = withdrawEvent.driverKey
|
||||
and insertEvent.intervalId = withdrawEvent.intervalId
|
||||
and withdrawEvent.occurredAtEpochSecond >= insertEvent.occurredAtEpochSecond;
|
||||
|
||||
@Priority(10)
|
||||
on DriverVehicleUsagePointEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
||||
delete from OpenDriverVehicleUsagePoint as openEvent
|
||||
where openEvent.driverKey = withdrawEvent.driverKey
|
||||
and openEvent.intervalId = withdrawEvent.intervalId;
|
||||
|
||||
@name('driverVehicleUsageIntervals')
|
||||
select *
|
||||
from DriverVehicleUsageIntervalEvent;
|
||||
|
|
@ -244,7 +244,7 @@ class UnifiedRuntimeProcessingControllerTest {
|
|||
1,
|
||||
3,
|
||||
List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")),
|
||||
projection,
|
||||
projection.toDriverWorkingTime(),
|
||||
List.of("runtime derived")
|
||||
));
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ class UnifiedRuntimeProcessingControllerTest {
|
|||
.thenReturn(List.of(new RuntimeProcessingPlanDescriptorDto(
|
||||
"driver-working-time-v1",
|
||||
Set.of("tachograph-driver-esper-v1"),
|
||||
"Driver working-time and tachograph-derived processing",
|
||||
"Driver working-time processing",
|
||||
"Runs common runtime event processing modules.",
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
List.of(RuntimeEventPartitioningStrategy.DRIVER),
|
||||
|
|
@ -576,9 +576,7 @@ class UnifiedRuntimeProcessingControllerTest {
|
|||
derivedProjectionService,
|
||||
null,
|
||||
runtimeEventProcessingService,
|
||||
parityValidationService,
|
||||
null,
|
||||
null
|
||||
parityValidationService
|
||||
))
|
||||
.setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper))
|
||||
.setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.plan;
|
||||
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class RuntimeProcessingPlanRegistryTest {
|
||||
|
||||
@Test
|
||||
void resolvesPlansByPrimaryKeyAndAlias() {
|
||||
RuntimeProcessingPlanRegistry registry = new RuntimeProcessingPlanRegistry(List.of(new TestPlan("plan-a", Set.of("legacy-a"))));
|
||||
|
||||
assertThat(registry.require("plan-a").processingPlanKey()).isEqualTo("plan-a");
|
||||
assertThat(registry.require("legacy-a").processingPlanKey()).isEqualTo("plan-a");
|
||||
assertThat(registry.planDescriptors()).hasSize(1);
|
||||
assertThat(registry.planDescriptors().get(0).aliases()).contains("legacy-a");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicatePlanKeys() {
|
||||
assertThatThrownBy(() -> new RuntimeProcessingPlanRegistry(List.of(
|
||||
new TestPlan("plan-a", Set.of()),
|
||||
new TestPlan("plan-a", Set.of())
|
||||
)))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Duplicate runtime processingPlanKey");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateAliasesPointingToDifferentPlans() {
|
||||
assertThatThrownBy(() -> new RuntimeProcessingPlanRegistry(List.of(
|
||||
new TestPlan("plan-a", Set.of("alias")),
|
||||
new TestPlan("plan-b", Set.of("alias"))
|
||||
)))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Duplicate runtime processing plan alias/key");
|
||||
}
|
||||
|
||||
private record TestPlan(String key, Set<String> aliases) implements RuntimeProcessingPlan {
|
||||
|
||||
@Override
|
||||
public String processingPlanKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() {
|
||||
return RuntimeEventPartitioningStrategy.DRIVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) {
|
||||
throw new UnsupportedOperationException("not needed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,13 +8,13 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||
import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService;
|
||||
import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -28,7 +28,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
|
|||
@Test
|
||||
void exposesDiscoveryMetadata() {
|
||||
TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(
|
||||
org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class)
|
||||
org.mockito.Mockito.mock(RuntimeDriverWorkingTimeScopeProcessingService.class)
|
||||
);
|
||||
|
||||
assertThat(profile.profileKey()).isEqualTo("tachograph-driver-esper-v1");
|
||||
|
|
@ -46,7 +46,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
|
|||
|
||||
@Test
|
||||
void delegatesToTachographScopeServiceAndMapsPartitionResults() {
|
||||
UnifiedRuntimeTachographEsperScopeProcessingService scopeService = org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class);
|
||||
RuntimeDriverWorkingTimeScopeProcessingService scopeService = org.mockito.Mockito.mock(RuntimeDriverWorkingTimeScopeProcessingService.class);
|
||||
TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(scopeService);
|
||||
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
|
|
@ -127,7 +127,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest {
|
|||
List.of("driver processed")
|
||||
);
|
||||
when(scopeService.processScope(any(), anyBoolean()))
|
||||
.thenReturn(new UnifiedRuntimeTachographEsperScopeResultDto(
|
||||
.thenReturn(new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
|
||||
processedRequest,
|
||||
5,
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
package at.procon.eventhub.processing.eventprocessing.validation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto;
|
||||
import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto;
|
||||
import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy;
|
||||
import at.procon.eventhub.processing.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile;
|
||||
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class RuntimeMixedSourceEvidenceValidationServiceTest {
|
||||
|
||||
@Test
|
||||
void validatesAttachedAndNormalizedEvidenceCounts() {
|
||||
RuntimeEventProcessingService processingService = Mockito.mock(RuntimeEventProcessingService.class);
|
||||
RuntimeMixedSourceEvidenceValidationService service = new RuntimeMixedSourceEvidenceValidationService(processingService);
|
||||
|
||||
UnifiedRuntimeDerivedProjectionResultDto driverResult = new UnifiedRuntimeDerivedProjectionResultDto(
|
||||
null,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
List.of(),
|
||||
null,
|
||||
List.of("projection note"),
|
||||
new RuntimeSupportEvidenceNormalizationDebugDto(3, 1, 2, List.of("normalization note")),
|
||||
new RuntimeDriverPartitionDebugDto(
|
||||
"DRIVER:1",
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
3,
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of("debug note"),
|
||||
List.of()
|
||||
)
|
||||
);
|
||||
when(processingService.process(any())).thenReturn(new RuntimeEventProcessingResultDto(
|
||||
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
null,
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
List.of(),
|
||||
Map.of("DRIVER:1", new RuntimeEventProcessingPartitionResultDto(
|
||||
"DRIVER",
|
||||
"DRIVER:1",
|
||||
"UnifiedRuntimeDerivedProjectionResultDto",
|
||||
driverResult,
|
||||
Map.of()
|
||||
)),
|
||||
List.of("runtime note"),
|
||||
List.of()
|
||||
));
|
||||
|
||||
RuntimeMixedSourceEvidenceValidationResultDto result = service.validate(new RuntimeMixedSourceEvidenceValidationApiRequest(
|
||||
new RuntimeEventProcessingApiRequest(
|
||||
TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY,
|
||||
new UnifiedRuntimeProcessingApiRequest(
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
"default",
|
||||
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||
UnifiedRuntimeEventBackend.SOURCE_DB,
|
||||
"DRIVER:1",
|
||||
Set.of(),
|
||||
false,
|
||||
Set.of(),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
OffsetDateTime.parse("2026-05-01T00:00:00Z"),
|
||||
OffsetDateTime.parse("2026-05-02T00:00:00Z"),
|
||||
true,
|
||||
15,
|
||||
3,
|
||||
720
|
||||
),
|
||||
new RuntimeEventPartitioningApiRequest(
|
||||
RuntimeEventPartitioningStrategy.DRIVER,
|
||||
null,
|
||||
null,
|
||||
Set.of("DRIVER:1"),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
15,
|
||||
false
|
||||
),
|
||||
Map.of()
|
||||
),
|
||||
Set.of("DRIVER:1"),
|
||||
1,
|
||||
1,
|
||||
true
|
||||
));
|
||||
|
||||
assertThat(result.status()).isEqualTo("PASSED");
|
||||
assertThat(result.totalAttachedVehicleEvidenceEventCount()).isEqualTo(1);
|
||||
assertThat(result.totalNormalizedSupportEvidenceEventCount()).isEqualTo(1);
|
||||
assertThat(result.partitions().get("DRIVER:1").partitionDebugPresent()).isTrue();
|
||||
assertThat(result.partitions().get("DRIVER:1").normalizedSupportEvidenceEventCount()).isEqualTo(1);
|
||||
|
||||
ArgumentCaptor<RuntimeEventProcessingApiRequest> captor = ArgumentCaptor.forClass(RuntimeEventProcessingApiRequest.class);
|
||||
verify(processingService).process(captor.capture());
|
||||
assertThat(captor.getValue().partitioning().includeDebugOrDefault()).isTrue();
|
||||
assertThat(captor.getValue().parameters()).containsEntry("includePartitionDebug", true);
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ class RuntimeTachographParityValidationServiceTest {
|
|||
0,
|
||||
4,
|
||||
List.of(),
|
||||
projection(sessionId, driverKey, 2, 1, 1),
|
||||
projection(sessionId, driverKey, 2, 1, 1).toDriverWorkingTime(),
|
||||
List.of()
|
||||
);
|
||||
when(runtimeEventProcessingService.process(any()))
|
||||
|
|
|
|||
Loading…
Reference in New Issue