Compare commits
8 Commits
c066c5c777
...
3c5ce9a066
| Author | SHA1 | Date |
|---|---|---|
|
|
3c5ce9a066 | |
|
|
74d479454a | |
|
|
e24df88736 | |
|
|
e45fe29d3f | |
|
|
cdec89aa69 | |
|
|
dd5c32f44f | |
|
|
729e6fb261 | |
|
|
c1b4847cf0 |
131
README_PATCH.md
131
README_PATCH.md
|
|
@ -1,109 +1,40 @@
|
||||||
# Patch: Vehicle Usage Interval Reconciliation
|
# Cross-representation tachograph event mixing fix
|
||||||
|
|
||||||
This patch extends the already introduced runtime event-mixing architecture with an interval-level reconciliation step for tachograph vehicle-usage evidence.
|
## Problem
|
||||||
|
|
||||||
## New module
|
A runtime request that loads both `TACHOGRAPH_FILE_SESSION` and `TACHOGRAPH_DB` can contain the same tachograph observation twice. The earlier mixing implementation primarily handled CARD versus VU evidence. It did not reliably classify the semantic source role and physical representation of every event, and it did not suppress duplicate observations when both copies had the same source role (for example VU file-session plus VU database serialization).
|
||||||
|
|
||||||
Added runtime module:
|
This produced nearly doubled activity and support-event results in mixed executions.
|
||||||
|
|
||||||
```text
|
## Implementation
|
||||||
vehicle-usage-reconciliation
|
|
||||||
```
|
|
||||||
|
|
||||||
It runs after:
|
- Added semantic tachograph evidence source roles:
|
||||||
|
- `DRIVER_CARD`
|
||||||
|
- `VEHICLE_UNIT`
|
||||||
|
- `UNKNOWN`
|
||||||
|
- Added physical representation classification:
|
||||||
|
- `FILE_SESSION`
|
||||||
|
- `DATABASE`
|
||||||
|
- `UNKNOWN`
|
||||||
|
- Source-role inference now uses extraction code, raw source metadata, package event-source metadata, package kind and external event identifiers.
|
||||||
|
- Database runtime packages identified by `RUNTIME:TACHOGRAPH:*` take precedence over retained original file-session package metadata.
|
||||||
|
- Existing CARD/VU rules now use semantic source roles, with extraction codes retained only as fallback for unclassified events.
|
||||||
|
- Added same-source-role cross-representation rules for activity and support evidence:
|
||||||
|
- database representation is retained as primary;
|
||||||
|
- matching file-session representation is suppressed as a duplicate;
|
||||||
|
- exact timestamps remain required;
|
||||||
|
- semantic compatibility checks still protect meaningful conflicts.
|
||||||
|
- `CARD_VEHICLES_USED` and `IW_CYCLE` remain untouched by event mixing and continue to be handled by interval-level reconciliation.
|
||||||
|
- Added mixing diagnostics for source roles, representations, candidate groups, rejected compatibility pairs and suppressed events.
|
||||||
|
- Fixed tachograph place SQL so entry type `6` is classified as `START`, consistently with file-session parsing.
|
||||||
|
|
||||||
```text
|
## Tests added/updated
|
||||||
event-to-vehicle-usage-intervals
|
|
||||||
```
|
|
||||||
|
|
||||||
and before:
|
- Same VU place observation from file session and database is reduced to one event.
|
||||||
|
- Same CARD activity observation from file session and database is reduced to one event.
|
||||||
|
- Database/file-session representation classification remains correct even when DB rows retain original file-session package metadata.
|
||||||
|
- Existing CARD/VU parity and CVU/IW preservation tests remain covered.
|
||||||
|
|
||||||
```text
|
## Validation
|
||||||
vehicle-usage-merge
|
|
||||||
```
|
|
||||||
|
|
||||||
## Main behavior
|
The modified Java files were passed through a `javac` parser check. Only expected missing dependency/classpath errors were reported; no Java syntax errors were detected. Maven and a Maven wrapper are unavailable in the execution environment, so the full project test suite was not executed.
|
||||||
|
|
||||||
The module intentionally does not mix `CARD_VEHICLES_USED` and `IW_CYCLE` at event level. Instead, it reconciles the completed vehicle-usage intervals.
|
|
||||||
|
|
||||||
Processing phases:
|
|
||||||
|
|
||||||
1. Split raw vehicle-usage intervals by source type:
|
|
||||||
- `CARD_VEHICLES_USED`
|
|
||||||
- `IW_CYCLE`
|
|
||||||
- `OTHER`
|
|
||||||
2. Normalize `CARD_VEHICLES_USED` technical midnight splits.
|
|
||||||
3. Reconcile normalized `CARD_VEHICLES_USED` intervals with `IW_CYCLE` intervals.
|
|
||||||
4. Produce effective vehicle-usage intervals for downstream processing.
|
|
||||||
|
|
||||||
## CVU technical midnight split
|
|
||||||
|
|
||||||
The technical midnight split is handled only for `CARD_VEHICLES_USED` / CVU intervals, not for `IW_CYCLE`.
|
|
||||||
|
|
||||||
Pattern:
|
|
||||||
|
|
||||||
```text
|
|
||||||
CARD_VEHICLES_USED interval A ends at 23:59:59
|
|
||||||
CARD_VEHICLES_USED interval B starts at 00:00:00
|
|
||||||
same driver
|
|
||||||
same registration / compatible vehicle
|
|
||||||
max gap: 1 second
|
|
||||||
```
|
|
||||||
|
|
||||||
Result:
|
|
||||||
|
|
||||||
```text
|
|
||||||
A + B => one normalized CARD_VEHICLES_USED interval
|
|
||||||
```
|
|
||||||
|
|
||||||
## CVU vs IW reconciliation
|
|
||||||
|
|
||||||
After CVU normalization:
|
|
||||||
|
|
||||||
```text
|
|
||||||
normalized CARD_VEHICLES_USED interval
|
|
||||||
vs
|
|
||||||
IW_CYCLE interval
|
|
||||||
```
|
|
||||||
|
|
||||||
Rule:
|
|
||||||
|
|
||||||
```text
|
|
||||||
IW_CYCLE is primary for effective vehicle-usage identity.
|
|
||||||
CARD_VEHICLES_USED is fallback or corroborating evidence.
|
|
||||||
```
|
|
||||||
|
|
||||||
Matching currently supports exact or compatible start/end boundaries with a 60-second tolerance.
|
|
||||||
|
|
||||||
## New classes
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/module/VehicleUsageReconciliationModule.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageIntervalDescriptor.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageIntervalDescriptorFactory.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageIntervalRole.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageIntervalSourceType.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageReconciliationDecisionDto.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageReconciliationResult.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageReconciliationService.java
|
|
||||||
src/test/java/at/procon/eventhub/processing/eventprocessing/vehicleusage/RuntimeVehicleUsageReconciliationServiceTest.java
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modified existing files
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeModuleKeys.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageMergeModule.java
|
|
||||||
src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
`vehicle-usage-merge` now consumes the effective intervals from `vehicle-usage-reconciliation` when that module has run. If the reconciliation module is omitted from a custom module list, `vehicle-usage-merge` falls back to raw `event-to-vehicle-usage-intervals` output.
|
|
||||||
|
|
||||||
Tests were added for:
|
|
||||||
|
|
||||||
- CVU technical midnight split coalescing
|
|
||||||
- CVU + IW reconciliation with IW as primary
|
|
||||||
- CVU fallback when IW is missing
|
|
||||||
- IW primary when CVU is missing
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,20 @@ tachograph-driving-derived-projection-bundle.epl
|
||||||
|
|
||||||
New runtime-processing code should use the driver-working-time names.
|
New runtime-processing code should use the driver-working-time names.
|
||||||
|
|
||||||
|
The common Esper contract is source-neutral as well:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DriverWorkingTimeActivityPointInputEvent
|
||||||
|
DriverWorkingTimeVehicleUsagePointInputEvent
|
||||||
|
DriverWorkingTimeActivityIntervalInputEvent
|
||||||
|
DriverWorkingTimeVehicleUsageIntervalInputEvent
|
||||||
|
DriverWorkingTimeSupportEvidenceInputEvent
|
||||||
|
DriverWorkingTimeProjectionFinalizeEvent
|
||||||
|
DriverWorkingTimeVehicleUsageIntervalInputWindow
|
||||||
|
```
|
||||||
|
|
||||||
|
Tachograph-prefixed Esper types remain only inside the compatibility resources listed above.
|
||||||
|
|
||||||
## EPL-backed phase modules
|
## EPL-backed phase modules
|
||||||
|
|
||||||
The driver working-time plan now contains first-class EPL-backed phase modules for event-to-interval conversion:
|
The driver working-time plan now contains first-class EPL-backed phase modules for event-to-interval conversion:
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,10 @@ RuntimeDriverWorkingTimeScopeProcessingService
|
||||||
|
|
||||||
Legacy tachograph names are kept as compatibility adapters for existing file-session endpoints and Postman requests. New runtime code should use the `driver-working-time-*` classes/resources and the `driver-working-time-v1` processing plan.
|
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.
|
||||||
|
|
||||||
|
The active common EPL contracts use `DriverWorkingTime*InputEvent` and
|
||||||
|
`DriverWorkingTimeVehicleUsageIntervalInputWindow` names. Tachograph-prefixed Esper input
|
||||||
|
contracts are limited to legacy compatibility resources.
|
||||||
|
|
||||||
## Module execution results
|
## Module execution results
|
||||||
|
|
||||||
`/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly.
|
`/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Tachograph DB / file-session runtime parity
|
||||||
|
|
||||||
|
The runtime pipeline treats direct tachograph file-session events and events loaded from the tachograph database as two representations of the same tachograph facts.
|
||||||
|
|
||||||
|
## Canonical semantic boundary
|
||||||
|
|
||||||
|
`RuntimeTachographEventSemantics` normalizes representation-only differences without modifying the original event:
|
||||||
|
|
||||||
|
- file-session and DB source systems are exposed as `TACHOGRAPH`;
|
||||||
|
- extraction codes are read from DB raw metadata or inferred from source kind and event domain;
|
||||||
|
- `PLACE START` and `PLACE BEGIN` share the semantic lifecycle `BEGIN` for mixing.
|
||||||
|
|
||||||
|
The raw lifecycle, source package, payload, and external source event ID remain unchanged for audit and provenance.
|
||||||
|
|
||||||
|
## Runtime aggregation
|
||||||
|
|
||||||
|
`RuntimeEventAggregationService` is shared by:
|
||||||
|
|
||||||
|
- `TachographFileSessionRuntimeEventLoader`;
|
||||||
|
- `TachographDbRuntimeEventLoader`;
|
||||||
|
- `UnifiedRuntimeEventAssemblyService`.
|
||||||
|
|
||||||
|
Aggregation removes:
|
||||||
|
|
||||||
|
- repeated reads of the same source record;
|
||||||
|
- duplicate serialized representations of the same extraction observation.
|
||||||
|
|
||||||
|
It deliberately preserves evidence pairs that are resolved downstream:
|
||||||
|
|
||||||
|
- `CARD_ACTIVITY` / `VU_ACTIVITY`;
|
||||||
|
- card/VU support evidence;
|
||||||
|
- `CARD_VEHICLES_USED` / `IW_CYCLE`.
|
||||||
|
|
||||||
|
The first two are handled by `RuntimeEventMixingService`. Vehicle usage remains source-distinct until interval-level reconciliation.
|
||||||
|
|
||||||
|
## Regression coverage
|
||||||
|
|
||||||
|
`RuntimeTachographRepresentationParityTest` verifies equal source profiles and mixing outcomes for DB and file-session representations. `RuntimeEventAggregationServiceTest` verifies that technical duplicates are reduced while card/VU and CVU/IW evidence remains available to later modules.
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package at.procon.eventhub.processing.driverworkingtime.esper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical Esper type and state names used by the source-neutral driver-working-time pipeline.
|
||||||
|
*
|
||||||
|
* <p>Source-specific adapters may keep their own compatibility names, but common runtime modules
|
||||||
|
* must use these contracts after source events have been normalized.</p>
|
||||||
|
*/
|
||||||
|
public final class DriverWorkingTimeEsperContractNames {
|
||||||
|
|
||||||
|
public static final String ACTIVITY_POINT_INPUT_EVENT_TYPE =
|
||||||
|
"DriverWorkingTimeActivityPointInputEvent";
|
||||||
|
public static final String VEHICLE_USAGE_POINT_INPUT_EVENT_TYPE =
|
||||||
|
"DriverWorkingTimeVehicleUsagePointInputEvent";
|
||||||
|
public static final String ACTIVITY_INTERVAL_INPUT_EVENT_TYPE =
|
||||||
|
"DriverWorkingTimeActivityIntervalInputEvent";
|
||||||
|
public static final String VEHICLE_USAGE_INTERVAL_INPUT_EVENT_TYPE =
|
||||||
|
"DriverWorkingTimeVehicleUsageIntervalInputEvent";
|
||||||
|
public static final String SUPPORT_EVIDENCE_INPUT_EVENT_TYPE =
|
||||||
|
"DriverWorkingTimeSupportEvidenceInputEvent";
|
||||||
|
public static final String PROJECTION_FINALIZE_EVENT_TYPE =
|
||||||
|
"DriverWorkingTimeProjectionFinalizeEvent";
|
||||||
|
public static final String VEHICLE_USAGE_INTERVAL_INPUT_WINDOW =
|
||||||
|
"DriverWorkingTimeVehicleUsageIntervalInputWindow";
|
||||||
|
|
||||||
|
private DriverWorkingTimeEsperContractNames() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import com.espertech.esper.runtime.client.EPDeployment;
|
||||||
import com.espertech.esper.runtime.client.EPRuntime;
|
import com.espertech.esper.runtime.client.EPRuntime;
|
||||||
import com.espertech.esper.runtime.client.EPRuntimeProvider;
|
import com.espertech.esper.runtime.client.EPRuntimeProvider;
|
||||||
import at.procon.eventhub.config.EventHubProperties;
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.processing.driverworkingtime.esper.DriverWorkingTimeEsperContractNames;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDrivingInterruptionInterval;
|
||||||
|
|
@ -58,7 +59,8 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
private static final Map<String, Object> VEHICLE_USAGE_INTERVAL_INPUT_DEFINITION = vehicleUsageIntervalInputDefinitionStatic();
|
private static final Map<String, Object> VEHICLE_USAGE_INTERVAL_INPUT_DEFINITION = vehicleUsageIntervalInputDefinitionStatic();
|
||||||
private static final Map<String, Object> SUPPORT_GEO_EVIDENCE_INPUT_DEFINITION = supportGeoEvidenceInputDefinitionStatic();
|
private static final Map<String, Object> SUPPORT_GEO_EVIDENCE_INPUT_DEFINITION = supportGeoEvidenceInputDefinitionStatic();
|
||||||
private static final int MAX_IDLE_RUNTIMES_PER_DEFINITION = 2;
|
private static final int MAX_IDLE_RUNTIMES_PER_DEFINITION = 2;
|
||||||
private static final List<String> REUSABLE_RUNTIME_STATE_CLEANUP_QUERIES = List.of(
|
static final List<String> REUSABLE_RUNTIME_STATE_CLEANUP_QUERIES = List.of(
|
||||||
|
"delete from " + DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_WINDOW,
|
||||||
"delete from PreviousRestCandidateCoverageInterval",
|
"delete from PreviousRestCandidateCoverageInterval",
|
||||||
"delete from OpenPotentialInVehicleTripState",
|
"delete from OpenPotentialInVehicleTripState",
|
||||||
"delete from SupportGeoEvidenceWindow",
|
"delete from SupportGeoEvidenceWindow",
|
||||||
|
|
@ -76,6 +78,7 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
"delete from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow",
|
"delete from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow",
|
||||||
"delete from DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow",
|
"delete from DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow",
|
||||||
"delete from DailyWeeklyRestCandidateCoverageEmittedKeyWindow",
|
"delete from DailyWeeklyRestCandidateCoverageEmittedKeyWindow",
|
||||||
|
"delete from VuCardAbsentIntervalWindow",
|
||||||
"delete from PreviousSignificantDrivingInterval",
|
"delete from PreviousSignificantDrivingInterval",
|
||||||
"context PerDriver delete from PreviousVehicleUsageInterval"
|
"context PerDriver delete from PreviousVehicleUsageInterval"
|
||||||
);
|
);
|
||||||
|
|
@ -185,7 +188,7 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
long sortOutputMs = elapsedMillis(sortOutputStartedAtNanos);
|
long sortOutputMs = elapsedMillis(sortOutputStartedAtNanos);
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Driver working-time derived projection bundle built in {} ms (definitionCacheHit: {}, definitionPreparationMs: {}, runtimePoolHit: {}, runtimeInitMs: {}, deployMs: {}, listenerRegistrationMs: {}, runtimeResetMs: {}, sendSupportGeoMs: {}, sendVehicleUsageMs: {}, sendActivityMs: {}, destroyMs: {}, sortOutputMs: {}, activityInputEvents: {}, vehicleUsageInputEvents: {}, supportGeoInputEvents: {})",
|
"Driver working-time derived projection bundle built in {} ms (definitionCacheHit: {}, definitionPreparationMs: {}, runtimePoolHit: {}, runtimeInitMs: {}, deployMs: {}, listenerRegistrationMs: {}, runtimeResetBeforeMs: {}, runtimeResetAfterMs: {}, sendSupportGeoMs: {}, sendVehicleUsageMs: {}, sendActivityMs: {}, destroyMs: {}, sortOutputMs: {}, activityInputEvents: {}, vehicleUsageInputEvents: {}, supportGeoInputEvents: {})",
|
||||||
elapsedMillis(startedAtNanos),
|
elapsedMillis(startedAtNanos),
|
||||||
runtimeMetrics.definitionCacheHit(),
|
runtimeMetrics.definitionCacheHit(),
|
||||||
runtimeMetrics.definitionPreparationMs(),
|
runtimeMetrics.definitionPreparationMs(),
|
||||||
|
|
@ -193,7 +196,8 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
runtimeMetrics.runtimeInitMs(),
|
runtimeMetrics.runtimeInitMs(),
|
||||||
runtimeMetrics.deployMs(),
|
runtimeMetrics.deployMs(),
|
||||||
runtimeMetrics.listenerRegistrationMs(),
|
runtimeMetrics.listenerRegistrationMs(),
|
||||||
runtimeMetrics.runtimeResetMs(),
|
runtimeMetrics.runtimeResetBeforeMs(),
|
||||||
|
runtimeMetrics.runtimeResetAfterMs(),
|
||||||
runtimeMetrics.sendSupportGeoMs(),
|
runtimeMetrics.sendSupportGeoMs(),
|
||||||
runtimeMetrics.sendVehicleUsageMs(),
|
runtimeMetrics.sendVehicleUsageMs(),
|
||||||
runtimeMetrics.sendActivityMs(),
|
runtimeMetrics.sendActivityMs(),
|
||||||
|
|
@ -287,7 +291,8 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
long runtimeInitMs = 0L;
|
long runtimeInitMs = 0L;
|
||||||
long deployMs = 0L;
|
long deployMs = 0L;
|
||||||
long listenerRegistrationMs = 0L;
|
long listenerRegistrationMs = 0L;
|
||||||
long runtimeResetMs = 0L;
|
long runtimeResetBeforeMs = 0L;
|
||||||
|
long runtimeResetAfterMs = 0L;
|
||||||
long sendSupportGeoMs = 0L;
|
long sendSupportGeoMs = 0L;
|
||||||
long sendVehicleUsageMs = 0L;
|
long sendVehicleUsageMs = 0L;
|
||||||
long sendActivityMs = 0L;
|
long sendActivityMs = 0L;
|
||||||
|
|
@ -329,14 +334,15 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
listenerRegistrationMs = reusableRuntime.listenerRegistrationMs();
|
listenerRegistrationMs = reusableRuntime.listenerRegistrationMs();
|
||||||
|
|
||||||
ReusableProjectionRuntimeExecution execution = reusableRuntime.execute(listeners, sender);
|
ReusableProjectionRuntimeExecution execution = reusableRuntime.execute(listeners, sender);
|
||||||
runtimeResetMs = execution.runtimeResetMs();
|
runtimeResetBeforeMs = execution.runtimeResetBeforeMs();
|
||||||
|
runtimeResetAfterMs = execution.runtimeResetAfterMs();
|
||||||
sendSupportGeoMs = execution.sendSupportGeoMs();
|
sendSupportGeoMs = execution.sendSupportGeoMs();
|
||||||
sendVehicleUsageMs = execution.sendVehicleUsageMs();
|
sendVehicleUsageMs = execution.sendVehicleUsageMs();
|
||||||
sendActivityMs = execution.sendActivityMs();
|
sendActivityMs = execution.sendActivityMs();
|
||||||
} catch (EPCompileException | EPDeployException e) {
|
} catch (EPCompileException | EPDeployException e) {
|
||||||
discardRuntime = true;
|
discardRuntime = true;
|
||||||
throw new IllegalStateException("Cannot compile/deploy reusable driver working-time projection EPL bundle", e);
|
throw new IllegalStateException("Cannot compile/deploy reusable driver working-time projection EPL bundle", e);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException | Error e) {
|
||||||
discardRuntime = true;
|
discardRuntime = true;
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -355,7 +361,8 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
runtimeInitMs,
|
runtimeInitMs,
|
||||||
deployMs,
|
deployMs,
|
||||||
listenerRegistrationMs,
|
listenerRegistrationMs,
|
||||||
runtimeResetMs,
|
runtimeResetBeforeMs,
|
||||||
|
runtimeResetAfterMs,
|
||||||
sendSupportGeoMs,
|
sendSupportGeoMs,
|
||||||
sendVehicleUsageMs,
|
sendVehicleUsageMs,
|
||||||
sendActivityMs,
|
sendActivityMs,
|
||||||
|
|
@ -434,15 +441,15 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
private Configuration createRuntimeConfiguration() {
|
private Configuration createRuntimeConfiguration() {
|
||||||
Configuration configuration = new Configuration();
|
Configuration configuration = new Configuration();
|
||||||
configuration.getCommon().addEventType(
|
configuration.getCommon().addEventType(
|
||||||
"TachographActivityIntervalInputEvent",
|
DriverWorkingTimeEsperContractNames.ACTIVITY_INTERVAL_INPUT_EVENT_TYPE,
|
||||||
activityIntervalInputDefinition()
|
activityIntervalInputDefinition()
|
||||||
);
|
);
|
||||||
configuration.getCommon().addEventType(
|
configuration.getCommon().addEventType(
|
||||||
"TachographVehicleUsageIntervalInputEvent",
|
DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_EVENT_TYPE,
|
||||||
vehicleUsageIntervalInputDefinition()
|
vehicleUsageIntervalInputDefinition()
|
||||||
);
|
);
|
||||||
configuration.getCommon().addEventType(
|
configuration.getCommon().addEventType(
|
||||||
"TachographSupportGeoEvidenceInputEvent",
|
DriverWorkingTimeEsperContractNames.SUPPORT_EVIDENCE_INPUT_EVENT_TYPE,
|
||||||
supportGeoEvidenceInputDefinition()
|
supportGeoEvidenceInputDefinition()
|
||||||
);
|
);
|
||||||
return configuration;
|
return configuration;
|
||||||
|
|
@ -972,7 +979,8 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
long runtimeInitMs,
|
long runtimeInitMs,
|
||||||
long deployMs,
|
long deployMs,
|
||||||
long listenerRegistrationMs,
|
long listenerRegistrationMs,
|
||||||
long runtimeResetMs,
|
long runtimeResetBeforeMs,
|
||||||
|
long runtimeResetAfterMs,
|
||||||
long sendSupportGeoMs,
|
long sendSupportGeoMs,
|
||||||
long sendVehicleUsageMs,
|
long sendVehicleUsageMs,
|
||||||
long sendActivityMs,
|
long sendActivityMs,
|
||||||
|
|
@ -999,6 +1007,9 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
if (runtime == null) {
|
if (runtime == null) {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
|
if (!runtime.cleanForReuse()) {
|
||||||
|
return runtime.destroy();
|
||||||
|
}
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (idleRuntimes.size() < MAX_IDLE_RUNTIMES_PER_DEFINITION) {
|
if (idleRuntimes.size() < MAX_IDLE_RUNTIMES_PER_DEFINITION) {
|
||||||
runtime.poolHit(false);
|
runtime.poolHit(false);
|
||||||
|
|
@ -1018,6 +1029,7 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
private volatile long listenerRegistrationMs;
|
private volatile long listenerRegistrationMs;
|
||||||
private volatile List<EPCompiled> cleanupQueries = List.of();
|
private volatile List<EPCompiled> cleanupQueries = List.of();
|
||||||
private volatile boolean poolHit;
|
private volatile boolean poolHit;
|
||||||
|
private volatile boolean cleanForReuse = true;
|
||||||
private ExecutionListeners currentExecutionListeners;
|
private ExecutionListeners currentExecutionListeners;
|
||||||
|
|
||||||
private ReusableProjectionRuntime(
|
private ReusableProjectionRuntime(
|
||||||
|
|
@ -1036,24 +1048,48 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
Map<String, Consumer<EventBean[]>> listeners,
|
Map<String, Consumer<EventBean[]>> listeners,
|
||||||
Consumer<DerivedProjectionEventSender> sender
|
Consumer<DerivedProjectionEventSender> sender
|
||||||
) {
|
) {
|
||||||
|
cleanForReuse = false;
|
||||||
currentExecutionListeners = new ExecutionListeners(listeners);
|
currentExecutionListeners = new ExecutionListeners(listeners);
|
||||||
|
long runtimeResetBeforeMs = 0L;
|
||||||
|
long runtimeResetAfterMs = 0L;
|
||||||
|
DerivedProjectionEventSender timedSender = new DerivedProjectionEventSender(runtime);
|
||||||
|
Throwable executionFailure = null;
|
||||||
|
try {
|
||||||
|
runtimeResetBeforeMs = resetExecutionState();
|
||||||
|
sender.accept(timedSender);
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
executionFailure = failure;
|
||||||
|
throw failure;
|
||||||
|
} finally {
|
||||||
|
// Cleanup must not feed delete/old-data callbacks into result collectors.
|
||||||
|
currentExecutionListeners = null;
|
||||||
|
try {
|
||||||
|
runtimeResetAfterMs = resetExecutionState();
|
||||||
|
cleanForReuse = true;
|
||||||
|
} catch (RuntimeException | Error cleanupFailure) {
|
||||||
|
cleanForReuse = false;
|
||||||
|
if (executionFailure != null) {
|
||||||
|
executionFailure.addSuppressed(cleanupFailure);
|
||||||
|
} else {
|
||||||
|
throw cleanupFailure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ReusableProjectionRuntimeExecution(
|
||||||
|
runtimeResetBeforeMs,
|
||||||
|
runtimeResetAfterMs,
|
||||||
|
timedSender.sendSupportGeoMs(),
|
||||||
|
timedSender.sendVehicleUsageMs(),
|
||||||
|
timedSender.sendActivityMs()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long resetExecutionState() {
|
||||||
long runtimeResetStartedAtNanos = System.nanoTime();
|
long runtimeResetStartedAtNanos = System.nanoTime();
|
||||||
for (EPCompiled cleanupQuery : cleanupQueries) {
|
for (EPCompiled cleanupQuery : cleanupQueries) {
|
||||||
runtime.getFireAndForgetService().executeQuery(cleanupQuery);
|
runtime.getFireAndForgetService().executeQuery(cleanupQuery);
|
||||||
}
|
}
|
||||||
long runtimeResetMs = elapsedMillisStatic(runtimeResetStartedAtNanos);
|
return elapsedMillisStatic(runtimeResetStartedAtNanos);
|
||||||
try {
|
|
||||||
DerivedProjectionEventSender timedSender = new DerivedProjectionEventSender(runtime);
|
|
||||||
sender.accept(timedSender);
|
|
||||||
return new ReusableProjectionRuntimeExecution(
|
|
||||||
runtimeResetMs,
|
|
||||||
timedSender.sendSupportGeoMs(),
|
|
||||||
timedSender.sendVehicleUsageMs(),
|
|
||||||
timedSender.sendActivityMs()
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
currentExecutionListeners = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStatementEvents(String statementName, EventBean[] newData) {
|
private void onStatementEvents(String statementName, EventBean[] newData) {
|
||||||
|
|
@ -1097,6 +1133,10 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
return poolHit;
|
return poolHit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean cleanForReuse() {
|
||||||
|
return cleanForReuse;
|
||||||
|
}
|
||||||
|
|
||||||
private long runtimeInitMs() {
|
private long runtimeInitMs() {
|
||||||
return poolHit ? 0L : runtimeInitMs;
|
return poolHit ? 0L : runtimeInitMs;
|
||||||
}
|
}
|
||||||
|
|
@ -1119,7 +1159,8 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ReusableProjectionRuntimeExecution(
|
private record ReusableProjectionRuntimeExecution(
|
||||||
long runtimeResetMs,
|
long runtimeResetBeforeMs,
|
||||||
|
long runtimeResetAfterMs,
|
||||||
long sendSupportGeoMs,
|
long sendSupportGeoMs,
|
||||||
long sendVehicleUsageMs,
|
long sendVehicleUsageMs,
|
||||||
long sendActivityMs
|
long sendActivityMs
|
||||||
|
|
@ -1139,19 +1180,19 @@ public class DriverWorkingTimeReusableProjectionBuilder {
|
||||||
|
|
||||||
private void sendSupportGeoEvent(Map<String, Object> event) {
|
private void sendSupportGeoEvent(Map<String, Object> event) {
|
||||||
long startedAtNanos = System.nanoTime();
|
long startedAtNanos = System.nanoTime();
|
||||||
delegate.getEventService().sendEventMap(event, "TachographSupportGeoEvidenceInputEvent");
|
delegate.getEventService().sendEventMap(event, DriverWorkingTimeEsperContractNames.SUPPORT_EVIDENCE_INPUT_EVENT_TYPE);
|
||||||
sendSupportGeoMs += elapsedMillisStatic(startedAtNanos);
|
sendSupportGeoMs += elapsedMillisStatic(startedAtNanos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendVehicleUsageEvent(Map<String, Object> event) {
|
private void sendVehicleUsageEvent(Map<String, Object> event) {
|
||||||
long startedAtNanos = System.nanoTime();
|
long startedAtNanos = System.nanoTime();
|
||||||
delegate.getEventService().sendEventMap(event, "TachographVehicleUsageIntervalInputEvent");
|
delegate.getEventService().sendEventMap(event, DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_EVENT_TYPE);
|
||||||
sendVehicleUsageMs += elapsedMillisStatic(startedAtNanos);
|
sendVehicleUsageMs += elapsedMillisStatic(startedAtNanos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendActivityEvent(Map<String, Object> event) {
|
private void sendActivityEvent(Map<String, Object> event) {
|
||||||
long startedAtNanos = System.nanoTime();
|
long startedAtNanos = System.nanoTime();
|
||||||
delegate.getEventService().sendEventMap(event, "TachographActivityIntervalInputEvent");
|
delegate.getEventService().sendEventMap(event, DriverWorkingTimeEsperContractNames.ACTIVITY_INTERVAL_INPUT_EVENT_TYPE);
|
||||||
sendActivityMs += elapsedMillisStatic(startedAtNanos);
|
sendActivityMs += elapsedMillisStatic(startedAtNanos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ public record RuntimeEventDescriptor(
|
||||||
String eventIdentityKey,
|
String eventIdentityKey,
|
||||||
String eventKey,
|
String eventKey,
|
||||||
RuntimeEventSourceProfile sourceProfile,
|
RuntimeEventSourceProfile sourceProfile,
|
||||||
|
RuntimeTachographEvidenceSourceRole evidenceSourceRole,
|
||||||
|
RuntimeTachographRepresentation representation,
|
||||||
String compatibleActivityKey,
|
String compatibleActivityKey,
|
||||||
String compatibleSupportEvidenceKey,
|
String compatibleSupportEvidenceKey,
|
||||||
boolean driverActivityPoint,
|
boolean driverActivityPoint,
|
||||||
|
|
@ -37,6 +39,18 @@ public record RuntimeEventDescriptor(
|
||||||
return sourceProfile == null ? null : sourceProfile.extractionCode();
|
return sourceProfile == null ? null : sourceProfile.extractionCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographEvidenceSourceRole evidenceSourceRole() {
|
||||||
|
return evidenceSourceRole == null
|
||||||
|
? RuntimeTachographEvidenceSourceRole.UNKNOWN
|
||||||
|
: evidenceSourceRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographRepresentation representation() {
|
||||||
|
return representation == null
|
||||||
|
? RuntimeTachographRepresentation.UNKNOWN
|
||||||
|
: representation;
|
||||||
|
}
|
||||||
|
|
||||||
public String keyFor(String equivalenceType) {
|
public String keyFor(String equivalenceType) {
|
||||||
return switch (equivalenceType) {
|
return switch (equivalenceType) {
|
||||||
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> eventKey;
|
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> eventKey;
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,30 @@ package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
import at.procon.eventhub.dto.EventDomain;
|
import at.procon.eventhub.dto.EventDomain;
|
||||||
import at.procon.eventhub.dto.EventHubEventDto;
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
import at.procon.eventhub.dto.EventLifecycle;
|
import at.procon.eventhub.dto.EventLifecycle;
|
||||||
import at.procon.eventhub.dto.GeoPointDto;
|
|
||||||
import at.procon.eventhub.processing.support.RuntimeEntityReferenceResolver;
|
import at.procon.eventhub.processing.support.RuntimeEntityReferenceResolver;
|
||||||
import at.procon.eventhub.processing.support.RuntimeEventIdentityResolver;
|
import at.procon.eventhub.processing.support.RuntimeEventIdentityResolver;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class RuntimeEventDescriptorFactory {
|
public class RuntimeEventDescriptorFactory {
|
||||||
|
|
||||||
|
private final RuntimeTachographEventSemantics tachographSemantics;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public RuntimeEventDescriptorFactory(RuntimeTachographEventSemantics tachographSemantics) {
|
||||||
|
this.tachographSemantics = tachographSemantics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compatibility constructor used by unit tests. */
|
||||||
|
public RuntimeEventDescriptorFactory() {
|
||||||
|
this(new RuntimeTachographEventSemantics());
|
||||||
|
}
|
||||||
|
|
||||||
public List<RuntimeEventDescriptor> describeSorted(List<EventHubEventDto> events) {
|
public List<RuntimeEventDescriptor> describeSorted(List<EventHubEventDto> events) {
|
||||||
return sort(events).stream()
|
return sort(events).stream()
|
||||||
.map(this::describe)
|
.map(this::describe)
|
||||||
|
|
@ -30,6 +40,8 @@ public class RuntimeEventDescriptorFactory {
|
||||||
eventIdentityKey(event),
|
eventIdentityKey(event),
|
||||||
RuntimeEventIdentityResolver.canonicalEventKey(event),
|
RuntimeEventIdentityResolver.canonicalEventKey(event),
|
||||||
profile,
|
profile,
|
||||||
|
profile.evidenceSourceRole(),
|
||||||
|
profile.representation(),
|
||||||
compatibleActivityKey(event),
|
compatibleActivityKey(event),
|
||||||
compatibleSupportEvidenceKey(event),
|
compatibleSupportEvidenceKey(event),
|
||||||
isDriverActivityPoint(event),
|
isDriverActivityPoint(event),
|
||||||
|
|
@ -64,26 +76,7 @@ public class RuntimeEventDescriptorFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeEventSourceProfile sourceProfile(EventHubEventDto event) {
|
public RuntimeEventSourceProfile sourceProfile(EventHubEventDto event) {
|
||||||
JsonNode raw = rawPayload(event);
|
return tachographSemantics.sourceProfile(event);
|
||||||
String sourceKind = firstNonBlank(text(raw, "sourceKind"), sourceKind(event));
|
|
||||||
String extractionCode = firstNonBlank(
|
|
||||||
text(raw, "extractionCode"),
|
|
||||||
fileSessionExtractionCode(event, sourceKind),
|
|
||||||
extractionCodeFromExternalSourceEventId(event)
|
|
||||||
);
|
|
||||||
String sourceSystem = firstNonBlank(
|
|
||||||
text(raw, "sourceSystem"),
|
|
||||||
sourceProvider(event),
|
|
||||||
sourceSystemFromExternalSourceEventId(event)
|
|
||||||
);
|
|
||||||
if (sourceSystem == null && (extractionCode != null || isTachographFileSessionEvent(event))) {
|
|
||||||
sourceSystem = "TACHOGRAPH";
|
|
||||||
}
|
|
||||||
return new RuntimeEventSourceProfile(
|
|
||||||
normalizeUpper(sourceSystem),
|
|
||||||
normalizeUpper(sourceKind),
|
|
||||||
normalizeUpper(extractionCode)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String eventIdentityKey(EventHubEventDto event) {
|
public String eventIdentityKey(EventHubEventDto event) {
|
||||||
|
|
@ -91,8 +84,8 @@ public class RuntimeEventDescriptorFactory {
|
||||||
return "<null>";
|
return "<null>";
|
||||||
}
|
}
|
||||||
return firstNonBlank(
|
return firstNonBlank(
|
||||||
event.externalSourceEventId(),
|
|
||||||
event.eventId() == null ? null : event.eventId().toString(),
|
event.eventId() == null ? null : event.eventId().toString(),
|
||||||
|
event.externalSourceEventId(),
|
||||||
RuntimeEventIdentityResolver.canonicalEventKey(event)
|
RuntimeEventIdentityResolver.canonicalEventKey(event)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -106,187 +99,33 @@ public class RuntimeEventDescriptorFactory {
|
||||||
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo));
|
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String fileSessionExtractionCode(EventHubEventDto event, String sourceKind) {
|
|
||||||
if (!isTachographFileSessionEvent(event)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String normalizedSourceKind = normalizeUpper(sourceKind);
|
|
||||||
if (normalizedSourceKind == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (event != null && event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
|
|
||||||
return switch (normalizedSourceKind) {
|
|
||||||
case "DRIVER_CARD" -> "CARD_ACTIVITY";
|
|
||||||
case "VEHICLE_UNIT" -> "VU_ACTIVITY";
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (event != null && event.eventDomain() == EventDomain.DRIVER_CARD) {
|
|
||||||
return switch (normalizedSourceKind) {
|
|
||||||
case "DRIVER_CARD" -> "CARD_VEHICLES_USED";
|
|
||||||
case "VEHICLE_UNIT" -> "IW_CYCLE";
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (event == null || event.eventDomain() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String prefix = switch (normalizedSourceKind) {
|
|
||||||
case "DRIVER_CARD" -> "CARD";
|
|
||||||
case "VEHICLE_UNIT" -> "VU";
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
if (prefix == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return switch (event.eventDomain()) {
|
|
||||||
case POSITION -> prefix + "_POSITION";
|
|
||||||
case PLACE -> prefix + "_PLACE";
|
|
||||||
case BORDER_CROSSING -> prefix + "_BORDER_CROSSING";
|
|
||||||
case LOAD_UNLOAD -> prefix + "_LOAD_UNLOAD";
|
|
||||||
case SPECIFIC_CONDITION -> prefix + "_SPECIFIC_CONDITION";
|
|
||||||
case SPEEDING -> Objects.equals("VU", prefix) ? "SPEEDING_EVENTS" : null;
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTachographFileSessionEvent(EventHubEventDto event) {
|
|
||||||
if (event == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String packageKind = event.sourcePackageRef() == null ? null : normalizeUpper(event.sourcePackageRef().packageKind());
|
|
||||||
if (Objects.equals("TACHOGRAPH_FILE_SESSION", packageKind)
|
|
||||||
|| Objects.equals("COMPOSITE_TACHOGRAPH_FILE_SESSION", packageKind)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String provider = sourceProvider(event);
|
|
||||||
if (Objects.equals("TACHOGRAPH_FILE_SESSION", normalizeUpper(provider))
|
|
||||||
|| Objects.equals("COMPOSITE_TACHOGRAPH_FILE_SESSION", normalizeUpper(provider))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String sourceKey = event.packageInfo() == null || event.packageInfo().eventSource() == null
|
|
||||||
? null
|
|
||||||
: normalizeUpper(event.packageInfo().eventSource().sourceKey());
|
|
||||||
if (Objects.equals("TACHOGRAPH_FILE_SESSION", sourceKey)
|
|
||||||
|| Objects.equals("COMPOSITE_TACHOGRAPH_FILE_SESSION", sourceKey)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
String externalId = event.externalSourceEventId();
|
|
||||||
return externalId != null
|
|
||||||
&& (externalId.startsWith("TACHOGRAPH_FILE_SESSION:")
|
|
||||||
|| externalId.startsWith("COMPOSITE_TACHOGRAPH_FILE_SESSION:"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String compatibleActivityKey(EventHubEventDto event) {
|
private String compatibleActivityKey(EventHubEventDto event) {
|
||||||
JsonNode raw = rawPayload(event);
|
|
||||||
return String.join("|",
|
return String.join("|",
|
||||||
"ACTIVITY_COMPATIBLE",
|
"ACTIVITY_COMPATIBLE",
|
||||||
nullToEmpty(event == null || event.packageInfo() == null ? null : event.packageInfo().tenantKey()),
|
|
||||||
nullToEmpty(RuntimeEntityReferenceResolver.driverKey(event)),
|
nullToEmpty(RuntimeEntityReferenceResolver.driverKey(event)),
|
||||||
nullToEmpty(event == null || event.eventDomain() == null ? null : event.eventDomain().name()),
|
nullToEmpty(event == null || event.eventDomain() == null ? null : event.eventDomain().name()),
|
||||||
nullToEmpty(event == null || event.eventType() == null ? null : event.eventType().name()),
|
nullToEmpty(event == null || event.eventType() == null ? null : event.eventType().name()),
|
||||||
nullToEmpty(event == null || event.lifecycle() == null ? null : event.lifecycle().name()),
|
nullToEmpty(event == null || event.lifecycle() == null ? null : event.lifecycle().name()),
|
||||||
normalizeTime(event == null ? null : event.occurredAt()),
|
normalizeTime(event == null ? null : event.occurredAt()),
|
||||||
nullToEmpty(RuntimeEntityReferenceResolver.registrationKey(event)),
|
nullToEmpty(RuntimeEntityReferenceResolver.registrationKey(event))
|
||||||
nullToEmpty(firstNonBlank(text(raw, "startedAt"), text(raw, "intervalStartedAt"))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "endedAt"), text(raw, "intervalEndedAt"))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "slot"), text(raw, "cardSlot"))),
|
|
||||||
nullToEmpty(text(raw, "cardStatus")),
|
|
||||||
nullToEmpty(text(raw, "drivingStatus"))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String compatibleSupportEvidenceKey(EventHubEventDto event) {
|
private String compatibleSupportEvidenceKey(EventHubEventDto event) {
|
||||||
JsonNode raw = rawPayload(event);
|
|
||||||
GeoPointDto position = event == null ? null : event.position();
|
|
||||||
return String.join("|",
|
return String.join("|",
|
||||||
"SUPPORT_COMPATIBLE",
|
"SUPPORT_COMPATIBLE",
|
||||||
nullToEmpty(event == null || event.packageInfo() == null ? null : event.packageInfo().tenantKey()),
|
|
||||||
nullToEmpty(RuntimeEntityReferenceResolver.driverKey(event)),
|
nullToEmpty(RuntimeEntityReferenceResolver.driverKey(event)),
|
||||||
nullToEmpty(event == null || event.eventDomain() == null ? null : event.eventDomain().name()),
|
nullToEmpty(event == null || event.eventDomain() == null ? null : event.eventDomain().name()),
|
||||||
nullToEmpty(event == null || event.eventType() == null ? null : event.eventType().name()),
|
nullToEmpty(event == null || event.eventType() == null ? null : event.eventType().name()),
|
||||||
nullToEmpty(semanticSupportLifecycle(event)),
|
nullToEmpty(semanticSupportLifecycle(event)),
|
||||||
normalizeTime(event == null ? null : event.occurredAt()),
|
normalizeTime(event == null ? null : event.occurredAt()),
|
||||||
nullToEmpty(RuntimeEntityReferenceResolver.registrationKey(event)),
|
nullToEmpty(RuntimeEntityReferenceResolver.registrationKey(event))
|
||||||
nullToEmpty(firstNonBlank(text(raw, "latitude"), position == null || position.latitude() == null ? null : position.latitude().toPlainString())),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "longitude"), position == null || position.longitude() == null ? null : position.longitude().toPlainString())),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "odometerM"), event == null || event.odometerM() == null ? null : String.valueOf(event.odometerM()))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "country"), detailText(event, "country"))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "region"), detailText(event, "region"))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "countryFrom"), detailText(event, "countryFrom"))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "countryTo"), detailText(event, "countryTo"))),
|
|
||||||
nullToEmpty(firstNonBlank(text(raw, "operation"), detailText(event, "operation")))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String semanticSupportLifecycle(EventHubEventDto event) {
|
private String semanticSupportLifecycle(EventHubEventDto event) {
|
||||||
if (event == null || event.lifecycle() == null) {
|
return tachographSemantics.semanticLifecycle(event);
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
if (event.eventDomain() == EventDomain.PLACE
|
|
||||||
&& (event.lifecycle() == EventLifecycle.START || event.lifecycle() == EventLifecycle.BEGIN)) {
|
|
||||||
return EventLifecycle.BEGIN.name();
|
|
||||||
}
|
|
||||||
return event.lifecycle().name();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonNode rawPayload(EventHubEventDto event) {
|
|
||||||
return RuntimeEntityReferenceResolver.rawPayload(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sourceKind(EventHubEventDto event) {
|
|
||||||
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
|
||||||
? null
|
|
||||||
: event.packageInfo().eventSource().sourceKind();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sourceProvider(EventHubEventDto event) {
|
|
||||||
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
|
||||||
? null
|
|
||||||
: event.packageInfo().eventSource().providerKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sourceSystemFromExternalSourceEventId(EventHubEventDto event) {
|
|
||||||
String externalId = event == null ? null : event.externalSourceEventId();
|
|
||||||
if (externalId == null || externalId.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String[] parts = externalId.split(":");
|
|
||||||
return parts.length >= 1 ? parts[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractionCodeFromExternalSourceEventId(EventHubEventDto event) {
|
|
||||||
String externalId = event == null ? null : event.externalSourceEventId();
|
|
||||||
if (externalId == null || externalId.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String[] parts = externalId.split(":");
|
|
||||||
return parts.length >= 2 ? parts[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String detailText(EventHubEventDto event, String field) {
|
|
||||||
if (event == null || event.eventDetails() == null || event.eventDetails().attributes() == null || field == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
JsonNode value = event.eventDetails().attributes().get(field);
|
|
||||||
if (value == null || value.isNull()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String text = value.asText(null);
|
|
||||||
return text == null || text.isBlank() ? null : text.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String text(JsonNode node, String field) {
|
|
||||||
if (node == null || field == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
JsonNode value = node.get(field);
|
|
||||||
if (value == null || value.isNull()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String text = value.asText(null);
|
|
||||||
return text == null || text.isBlank() ? null : text.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String firstNonBlank(String... values) {
|
private String firstNonBlank(String... values) {
|
||||||
if (values == null) {
|
if (values == null) {
|
||||||
|
|
@ -300,10 +139,6 @@ public class RuntimeEventDescriptorFactory {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeUpper(String value) {
|
|
||||||
return value == null || value.isBlank() ? null : value.trim().toUpperCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String nullToEmpty(Object value) {
|
private String nullToEmpty(Object value) {
|
||||||
return value == null ? "" : String.valueOf(value);
|
return value == null ? "" : String.valueOf(value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.dto.GeoPointDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRefDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||||
|
import at.procon.eventhub.processing.support.RuntimeEntityReferenceResolver;
|
||||||
|
import at.procon.eventhub.reference.TachographNationRegistry;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs semantic compatibility checks after broad event candidates have been grouped.
|
||||||
|
*
|
||||||
|
* <p>File-session and database representations of the same tachograph fact can differ in
|
||||||
|
* serialization details such as decimal scale, nation representation, default region values,
|
||||||
|
* optional vehicle identity and interval metadata. Those representation details must not be part
|
||||||
|
* of the candidate key, but meaningful conflicts must still prevent fusion.</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RuntimeEventEvidenceCompatibilityMatcher {
|
||||||
|
|
||||||
|
private static final BigDecimal GEO_TOLERANCE = new BigDecimal("0.000000001");
|
||||||
|
|
||||||
|
public boolean compatible(
|
||||||
|
RuntimeEventMixingRule rule,
|
||||||
|
RuntimeEventDescriptor primary,
|
||||||
|
RuntimeEventDescriptor secondary
|
||||||
|
) {
|
||||||
|
if (rule == null || primary == null || secondary == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rule.requireSameSourceRole()
|
||||||
|
&& primary.evidenceSourceRole() != secondary.evidenceSourceRole()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return switch (rule.equivalenceType()) {
|
||||||
|
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> true;
|
||||||
|
case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY ->
|
||||||
|
activityCompatible(primary.event(), secondary.event());
|
||||||
|
case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_SUPPORT_KEY ->
|
||||||
|
supportCompatible(primary.event(), secondary.event());
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean activityCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
return tenantCompatible(left, right)
|
||||||
|
&& registrationCompatible(left, right)
|
||||||
|
&& vehicleIdentityCompatible(left, right)
|
||||||
|
&& optionalTokenCompatible(activitySlot(left), activitySlot(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean supportCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
return tenantCompatible(left, right)
|
||||||
|
&& registrationCompatible(left, right)
|
||||||
|
&& vehicleIdentityCompatible(left, right)
|
||||||
|
&& coordinatesCompatible(left, right)
|
||||||
|
&& odometerCompatible(left, right)
|
||||||
|
&& nationCompatible(detailValue(left, "country"), detailValue(right, "country"))
|
||||||
|
&& regionCompatible(detailValue(left, "region"), detailValue(right, "region"))
|
||||||
|
&& nationCompatible(detailValue(left, "countryFrom"), detailValue(right, "countryFrom"))
|
||||||
|
&& nationCompatible(detailValue(left, "countryTo"), detailValue(right, "countryTo"))
|
||||||
|
&& optionalTokenCompatible(detailValue(left, "operation"), detailValue(right, "operation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean tenantCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
return optionalTokenCompatible(normalizedTenant(left), normalizedTenant(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizedTenant(EventHubEventDto event) {
|
||||||
|
String value = event == null || event.packageInfo() == null ? null : event.packageInfo().tenantKey();
|
||||||
|
String normalized = normalizeToken(value);
|
||||||
|
return Objects.equals("DEFAULT", normalized) ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean registrationCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
return optionalTokenCompatible(normalizedRegistration(left), normalizedRegistration(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizedRegistration(EventHubEventDto event) {
|
||||||
|
VehicleRefDto vehicleRef = event == null ? null : event.vehicleRef();
|
||||||
|
VehicleRegistrationRefDto registration = vehicleRef == null ? null : vehicleRef.vehicleRegistration();
|
||||||
|
if (registration != null && registration.hasValue()) {
|
||||||
|
String nation = normalizedNation(registration.nation(), registration.nationNumericCode());
|
||||||
|
String number = normalizeIdentifier(registration.number());
|
||||||
|
return nullToEmpty(nation) + ":" + nullToEmpty(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = RuntimeEntityReferenceResolver.registrationKey(event);
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int separator = key.indexOf(':');
|
||||||
|
if (separator < 0) {
|
||||||
|
return normalizeIdentifier(key);
|
||||||
|
}
|
||||||
|
String nation = normalizedNation(key.substring(0, separator), null);
|
||||||
|
String number = normalizeIdentifier(key.substring(separator + 1));
|
||||||
|
return nullToEmpty(nation) + ":" + nullToEmpty(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean vehicleIdentityCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
String leftVin = normalizedVin(left);
|
||||||
|
String rightVin = normalizedVin(right);
|
||||||
|
return optionalTokenCompatible(leftVin, rightVin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizedVin(EventHubEventDto event) {
|
||||||
|
String vehicleKey = RuntimeEntityReferenceResolver.vehicleKey(event);
|
||||||
|
if (vehicleKey != null) {
|
||||||
|
return normalizeIdentifier(vehicleKey);
|
||||||
|
}
|
||||||
|
VehicleRefDto vehicleRef = event == null ? null : event.vehicleRef();
|
||||||
|
return vehicleRef == null ? null : normalizeIdentifier(vehicleRef.vin());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean coordinatesCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
BigDecimal leftLatitude = coordinate(left, true);
|
||||||
|
BigDecimal rightLatitude = coordinate(right, true);
|
||||||
|
BigDecimal leftLongitude = coordinate(left, false);
|
||||||
|
BigDecimal rightLongitude = coordinate(right, false);
|
||||||
|
return optionalDecimalCompatible(leftLatitude, rightLatitude, GEO_TOLERANCE)
|
||||||
|
&& optionalDecimalCompatible(leftLongitude, rightLongitude, GEO_TOLERANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal coordinate(EventHubEventDto event, boolean latitude) {
|
||||||
|
GeoPointDto position = event == null ? null : event.position();
|
||||||
|
BigDecimal value = position == null ? null : latitude ? position.latitude() : position.longitude();
|
||||||
|
if (value != null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return decimal(rawValue(event, latitude ? "latitude" : "longitude"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean odometerCompatible(EventHubEventDto left, EventHubEventDto right) {
|
||||||
|
BigDecimal leftValue = odometerM(left);
|
||||||
|
BigDecimal rightValue = odometerM(right);
|
||||||
|
return optionalDecimalCompatible(leftValue, rightValue, BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal odometerM(EventHubEventDto event) {
|
||||||
|
if (event != null && event.odometerM() != null) {
|
||||||
|
return BigDecimal.valueOf(event.odometerM());
|
||||||
|
}
|
||||||
|
BigDecimal meters = decimal(rawValue(event, "odometerM"));
|
||||||
|
if (meters != null) {
|
||||||
|
return meters;
|
||||||
|
}
|
||||||
|
BigDecimal kilometres = decimal(rawValue(event, "odometerKm"));
|
||||||
|
return kilometres == null ? null : kilometres.multiply(BigDecimal.valueOf(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String activitySlot(EventHubEventDto event) {
|
||||||
|
return firstNonBlank(
|
||||||
|
rawValue(event, "slot"),
|
||||||
|
rawValue(event, "cardSlot"),
|
||||||
|
detailAttribute(event, "cardSlot")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detailValue(EventHubEventDto event, String field) {
|
||||||
|
return firstNonBlank(rawValue(event, field), detailAttribute(event, field));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rawValue(EventHubEventDto event, String field) {
|
||||||
|
return text(RuntimeEntityReferenceResolver.rawPayload(event), field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detailAttribute(EventHubEventDto event, String field) {
|
||||||
|
JsonNode attributes = event == null || event.eventDetails() == null
|
||||||
|
? null
|
||||||
|
: event.eventDetails().attributes();
|
||||||
|
return text(attributes, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean nationCompatible(String left, String right) {
|
||||||
|
String normalizedLeft = normalizedNation(left, null);
|
||||||
|
String normalizedRight = normalizedNation(right, null);
|
||||||
|
return optionalTokenCompatible(normalizedLeft, normalizedRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizedNation(String nation, Integer numericCode) {
|
||||||
|
TachographNationRegistry.NationResolution resolution =
|
||||||
|
TachographNationRegistry.resolve(nation, numericCode);
|
||||||
|
if (resolution.numericCode() != null) {
|
||||||
|
return String.valueOf(resolution.numericCode());
|
||||||
|
}
|
||||||
|
return normalizeToken(resolution.legacyNation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean regionCompatible(String left, String right) {
|
||||||
|
return optionalTokenCompatible(normalizedRegion(left), normalizedRegion(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizedRegion(String value) {
|
||||||
|
String normalized = normalizeToken(value);
|
||||||
|
return normalized == null || Objects.equals("0", normalized) ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean optionalTokenCompatible(String left, String right) {
|
||||||
|
String normalizedLeft = normalizeToken(left);
|
||||||
|
String normalizedRight = normalizeToken(right);
|
||||||
|
return normalizedLeft == null || normalizedRight == null || normalizedLeft.equals(normalizedRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean optionalDecimalCompatible(BigDecimal left, BigDecimal right, BigDecimal tolerance) {
|
||||||
|
if (left == null || right == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return left.subtract(right).abs().compareTo(tolerance) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal decimal(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new BigDecimal(value.trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String field) {
|
||||||
|
if (node == null || field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(field);
|
||||||
|
if (value == null || value.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String text = value.asText(null);
|
||||||
|
return text == null || text.isBlank() ? null : text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeIdentifier(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = value.trim().toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
|
||||||
|
return normalized.isBlank() ? null : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeToken(String value) {
|
||||||
|
return value == null || value.isBlank() ? null : value.trim().toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nullToEmpty(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
/** Classification and rule-application counters for one event-mixing execution. */
|
||||||
|
public record RuntimeEventMixingDiagnostics(
|
||||||
|
int describedEventCount,
|
||||||
|
int tachographEventCount,
|
||||||
|
int driverCardSourceRoleCount,
|
||||||
|
int vehicleUnitSourceRoleCount,
|
||||||
|
int unknownSourceRoleCount,
|
||||||
|
int databaseRepresentationCount,
|
||||||
|
int fileSessionRepresentationCount,
|
||||||
|
int unknownRepresentationCount,
|
||||||
|
int candidateGroupCount,
|
||||||
|
int compatibilityRejectedCount,
|
||||||
|
int suppressedEventCount
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,11 @@ public record RuntimeEventMixingRule(
|
||||||
Set<EventLifecycle> lifecycles,
|
Set<EventLifecycle> lifecycles,
|
||||||
Set<String> primaryExtractionCodes,
|
Set<String> primaryExtractionCodes,
|
||||||
Set<String> secondaryExtractionCodes,
|
Set<String> secondaryExtractionCodes,
|
||||||
|
Set<RuntimeTachographEvidenceSourceRole> primarySourceRoles,
|
||||||
|
Set<RuntimeTachographEvidenceSourceRole> secondarySourceRoles,
|
||||||
|
Set<RuntimeTachographRepresentation> primaryRepresentations,
|
||||||
|
Set<RuntimeTachographRepresentation> secondaryRepresentations,
|
||||||
|
boolean requireSameSourceRole,
|
||||||
RuntimeResolvedEventRole primaryRole,
|
RuntimeResolvedEventRole primaryRole,
|
||||||
RuntimeResolvedEventRole secondaryRole,
|
RuntimeResolvedEventRole secondaryRole,
|
||||||
String decision,
|
String decision,
|
||||||
|
|
@ -29,6 +34,10 @@ public record RuntimeEventMixingRule(
|
||||||
lifecycles = lifecycles == null ? Set.of() : Set.copyOf(lifecycles);
|
lifecycles = lifecycles == null ? Set.of() : Set.copyOf(lifecycles);
|
||||||
primaryExtractionCodes = normalize(primaryExtractionCodes);
|
primaryExtractionCodes = normalize(primaryExtractionCodes);
|
||||||
secondaryExtractionCodes = normalize(secondaryExtractionCodes);
|
secondaryExtractionCodes = normalize(secondaryExtractionCodes);
|
||||||
|
primarySourceRoles = primarySourceRoles == null ? Set.of() : Set.copyOf(primarySourceRoles);
|
||||||
|
secondarySourceRoles = secondarySourceRoles == null ? Set.of() : Set.copyOf(secondarySourceRoles);
|
||||||
|
primaryRepresentations = primaryRepresentations == null ? Set.of() : Set.copyOf(primaryRepresentations);
|
||||||
|
secondaryRepresentations = secondaryRepresentations == null ? Set.of() : Set.copyOf(secondaryRepresentations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(RuntimeEventDescriptor descriptor) {
|
public boolean matches(RuntimeEventDescriptor descriptor) {
|
||||||
|
|
@ -53,16 +62,55 @@ public record RuntimeEventMixingRule(
|
||||||
if (channel == RuntimeEventMixingChannel.SUPPORT_EVIDENCE && !descriptor.supportEvidenceCandidate()) {
|
if (channel == RuntimeEventMixingChannel.SUPPORT_EVIDENCE && !descriptor.supportEvidenceCandidate()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String extractionCode = descriptor.extractionCode();
|
return isPrimary(descriptor) || isSecondary(descriptor);
|
||||||
return primaryExtractionCodes.contains(extractionCode) || secondaryExtractionCodes.contains(extractionCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPrimary(RuntimeEventDescriptor descriptor) {
|
public boolean isPrimary(RuntimeEventDescriptor descriptor) {
|
||||||
return descriptor != null && primaryExtractionCodes.contains(descriptor.extractionCode());
|
return sideMatches(
|
||||||
|
descriptor,
|
||||||
|
primaryExtractionCodes,
|
||||||
|
primarySourceRoles,
|
||||||
|
primaryRepresentations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSecondary(RuntimeEventDescriptor descriptor) {
|
public boolean isSecondary(RuntimeEventDescriptor descriptor) {
|
||||||
return descriptor != null && secondaryExtractionCodes.contains(descriptor.extractionCode());
|
return sideMatches(
|
||||||
|
descriptor,
|
||||||
|
secondaryExtractionCodes,
|
||||||
|
secondarySourceRoles,
|
||||||
|
secondaryRepresentations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sideMatches(
|
||||||
|
RuntimeEventDescriptor descriptor,
|
||||||
|
Set<String> extractionCodes,
|
||||||
|
Set<RuntimeTachographEvidenceSourceRole> sourceRoles,
|
||||||
|
Set<RuntimeTachographRepresentation> representations
|
||||||
|
) {
|
||||||
|
if (descriptor == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!representations.isEmpty() && !representations.contains(descriptor.representation())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceRoles.isEmpty()) {
|
||||||
|
RuntimeTachographEvidenceSourceRole role = descriptor.evidenceSourceRole();
|
||||||
|
if (role != RuntimeTachographEvidenceSourceRole.UNKNOWN) {
|
||||||
|
return sourceRoles.contains(role);
|
||||||
|
}
|
||||||
|
// Extraction code is a fallback only when the semantic source role could not be
|
||||||
|
// resolved. A conflicting explicit role must never make one event both primary and
|
||||||
|
// secondary.
|
||||||
|
return !extractionCodes.isEmpty()
|
||||||
|
&& extractionCodes.contains(normalize(descriptor.extractionCode()));
|
||||||
|
}
|
||||||
|
if (!extractionCodes.isEmpty()) {
|
||||||
|
return extractionCodes.contains(normalize(descriptor.extractionCode()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<String> normalize(Set<String> values) {
|
private static Set<String> normalize(Set<String> values) {
|
||||||
|
|
@ -71,7 +119,13 @@ public record RuntimeEventMixingRule(
|
||||||
}
|
}
|
||||||
return values.stream()
|
return values.stream()
|
||||||
.filter(value -> value != null && !value.isBlank())
|
.filter(value -> value != null && !value.isBlank())
|
||||||
.map(value -> value.trim().toUpperCase(java.util.Locale.ROOT))
|
.map(RuntimeEventMixingRule::normalize)
|
||||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
return value == null || value.isBlank()
|
||||||
|
? null
|
||||||
|
: value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
EventLifecycle.OUT_EU
|
EventLifecycle.OUT_EU
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final Set<EventType> ACTIVITY_EVENT_TYPES = Set.of(
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventType.BREAK_REST,
|
||||||
|
EventType.AVAILABILITY,
|
||||||
|
EventType.WORK,
|
||||||
|
EventType.UNKNOWN_ACTIVITY
|
||||||
|
);
|
||||||
|
|
||||||
private static final Set<String> CARD_SUPPORT_EXTRACTION_CODES = Set.of(
|
private static final Set<String> CARD_SUPPORT_EXTRACTION_CODES = Set.of(
|
||||||
"CARD_POSITION",
|
"CARD_POSITION",
|
||||||
"CARD_PLACE",
|
"CARD_PLACE",
|
||||||
|
|
@ -57,11 +65,18 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
"VU_SPECIFIC_CONDITION"
|
"VU_SPECIFIC_CONDITION"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final Set<RuntimeTachographEvidenceSourceRole> BOTH_TACHOGRAPH_ROLES = Set.of(
|
||||||
|
RuntimeTachographEvidenceSourceRole.DRIVER_CARD,
|
||||||
|
RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT
|
||||||
|
);
|
||||||
|
|
||||||
public List<RuntimeEventMixingRule> rulesForMode(String mode) {
|
public List<RuntimeEventMixingRule> rulesForMode(String mode) {
|
||||||
if (RuntimeEventMixingService.MODE_OFF.equals(mode)) {
|
if (RuntimeEventMixingService.MODE_OFF.equals(mode)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return List.of(
|
return List.of(
|
||||||
|
tachographDbFileSessionSameRoleActivityCompatibleKey(),
|
||||||
|
tachographDbFileSessionSameRoleSupportCompatibleKey(),
|
||||||
tachographCardVuActivityExactEventKey(),
|
tachographCardVuActivityExactEventKey(),
|
||||||
tachographCardVuSupportExactEventKey(),
|
tachographCardVuSupportExactEventKey(),
|
||||||
tachographCardVuActivityCompatibleKey(),
|
tachographCardVuActivityCompatibleKey(),
|
||||||
|
|
@ -69,16 +84,65 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RuntimeEventMixingRule tachographDbFileSessionSameRoleActivityCompatibleKey() {
|
||||||
|
return new RuntimeEventMixingRule(
|
||||||
|
RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_ACTIVITY_SAME_ROLE,
|
||||||
|
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
||||||
|
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY,
|
||||||
|
Set.of(EventDomain.DRIVER_ACTIVITY),
|
||||||
|
ACTIVITY_EVENT_TYPES,
|
||||||
|
Set.of(EventLifecycle.START, EventLifecycle.END),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
Set.of(RuntimeTachographRepresentation.DATABASE),
|
||||||
|
Set.of(RuntimeTachographRepresentation.FILE_SESSION),
|
||||||
|
true,
|
||||||
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
|
"CROSS_REPRESENTATION_DUPLICATE_SUPPRESSED",
|
||||||
|
"The tachograph database and file-session representations describe the same activity point from the same source role. The database representation is retained and the file-session representation is suppressed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeEventMixingRule tachographDbFileSessionSameRoleSupportCompatibleKey() {
|
||||||
|
return new RuntimeEventMixingRule(
|
||||||
|
RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_SUPPORT_SAME_ROLE,
|
||||||
|
RuntimeEventMixingChannel.SUPPORT_EVIDENCE,
|
||||||
|
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_SUPPORT_KEY,
|
||||||
|
SUPPORT_EVENT_DOMAINS,
|
||||||
|
SUPPORT_EVENT_TYPES,
|
||||||
|
SUPPORT_EVENT_LIFECYCLES,
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
Set.of(RuntimeTachographRepresentation.DATABASE),
|
||||||
|
Set.of(RuntimeTachographRepresentation.FILE_SESSION),
|
||||||
|
true,
|
||||||
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
|
"CROSS_REPRESENTATION_DUPLICATE_SUPPRESSED",
|
||||||
|
"The tachograph database and file-session representations describe the same support event from the same source role. The database representation is retained and the file-session representation is suppressed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private RuntimeEventMixingRule tachographCardVuActivityExactEventKey() {
|
private RuntimeEventMixingRule tachographCardVuActivityExactEventKey() {
|
||||||
return new RuntimeEventMixingRule(
|
return new RuntimeEventMixingRule(
|
||||||
RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_SAME_EVENT_KEY,
|
RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_SAME_EVENT_KEY,
|
||||||
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
||||||
RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY,
|
RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY,
|
||||||
Set.of(EventDomain.DRIVER_ACTIVITY),
|
Set.of(EventDomain.DRIVER_ACTIVITY),
|
||||||
Set.of(EventType.DRIVE, EventType.BREAK_REST, EventType.AVAILABILITY, EventType.WORK, EventType.UNKNOWN_ACTIVITY),
|
ACTIVITY_EVENT_TYPES,
|
||||||
Set.of(EventLifecycle.START, EventLifecycle.END),
|
Set.of(EventLifecycle.START, EventLifecycle.END),
|
||||||
Set.of("CARD_ACTIVITY"),
|
Set.of("CARD_ACTIVITY"),
|
||||||
Set.of("VU_ACTIVITY"),
|
Set.of("VU_ACTIVITY"),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
@ -92,10 +156,15 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
||||||
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY,
|
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY,
|
||||||
Set.of(EventDomain.DRIVER_ACTIVITY),
|
Set.of(EventDomain.DRIVER_ACTIVITY),
|
||||||
Set.of(EventType.DRIVE, EventType.BREAK_REST, EventType.AVAILABILITY, EventType.WORK, EventType.UNKNOWN_ACTIVITY),
|
ACTIVITY_EVENT_TYPES,
|
||||||
Set.of(EventLifecycle.START, EventLifecycle.END),
|
Set.of(EventLifecycle.START, EventLifecycle.END),
|
||||||
Set.of("CARD_ACTIVITY"),
|
Set.of("CARD_ACTIVITY"),
|
||||||
Set.of("VU_ACTIVITY"),
|
Set.of("VU_ACTIVITY"),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
@ -113,6 +182,11 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
SUPPORT_EVENT_LIFECYCLES,
|
SUPPORT_EVENT_LIFECYCLES,
|
||||||
CARD_SUPPORT_EXTRACTION_CODES,
|
CARD_SUPPORT_EXTRACTION_CODES,
|
||||||
VU_SUPPORT_EXTRACTION_CODES,
|
VU_SUPPORT_EXTRACTION_CODES,
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
@ -130,6 +204,11 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
SUPPORT_EVENT_LIFECYCLES,
|
SUPPORT_EVENT_LIFECYCLES,
|
||||||
CARD_SUPPORT_EXTRACTION_CODES,
|
CARD_SUPPORT_EXTRACTION_CODES,
|
||||||
VU_SUPPORT_EXTRACTION_CODES,
|
VU_SUPPORT_EXTRACTION_CODES,
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import at.procon.eventhub.dto.VehicleRefDto;
|
||||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -29,22 +30,33 @@ public class RuntimeEventMixingService {
|
||||||
"tachograph.support.card-vu.same-event-key";
|
"tachograph.support.card-vu.same-event-key";
|
||||||
public static final String RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_KEY =
|
public static final String RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_KEY =
|
||||||
"tachograph.support.card-vu.compatible-support-key";
|
"tachograph.support.card-vu.compatible-support-key";
|
||||||
|
public static final String RULE_TACHOGRAPH_DB_FILE_SESSION_ACTIVITY_SAME_ROLE =
|
||||||
|
"tachograph.activity.db-file-session.same-source-role";
|
||||||
|
public static final String RULE_TACHOGRAPH_DB_FILE_SESSION_SUPPORT_SAME_ROLE =
|
||||||
|
"tachograph.support.db-file-session.same-source-role";
|
||||||
|
|
||||||
private final RuntimeEventDescriptorFactory descriptorFactory;
|
private final RuntimeEventDescriptorFactory descriptorFactory;
|
||||||
private final RuntimeEventMixingRuleRegistry ruleRegistry;
|
private final RuntimeEventMixingRuleRegistry ruleRegistry;
|
||||||
|
private final RuntimeEventEvidenceCompatibilityMatcher compatibilityMatcher;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public RuntimeEventMixingService(
|
public RuntimeEventMixingService(
|
||||||
RuntimeEventDescriptorFactory descriptorFactory,
|
RuntimeEventDescriptorFactory descriptorFactory,
|
||||||
RuntimeEventMixingRuleRegistry ruleRegistry
|
RuntimeEventMixingRuleRegistry ruleRegistry,
|
||||||
|
RuntimeEventEvidenceCompatibilityMatcher compatibilityMatcher
|
||||||
) {
|
) {
|
||||||
this.descriptorFactory = descriptorFactory;
|
this.descriptorFactory = descriptorFactory;
|
||||||
this.ruleRegistry = ruleRegistry;
|
this.ruleRegistry = ruleRegistry;
|
||||||
|
this.compatibilityMatcher = compatibilityMatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compatibility constructor used by unit tests and local registries. */
|
/** Compatibility constructor used by unit tests and local registries. */
|
||||||
public RuntimeEventMixingService() {
|
public RuntimeEventMixingService() {
|
||||||
this(new RuntimeEventDescriptorFactory(), new RuntimeEventMixingRuleRegistry());
|
this(
|
||||||
|
new RuntimeEventDescriptorFactory(),
|
||||||
|
new RuntimeEventMixingRuleRegistry(),
|
||||||
|
new RuntimeEventEvidenceCompatibilityMatcher()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeMixedEventBundle mix(List<EventHubEventDto> events, String requestedMode) {
|
public RuntimeMixedEventBundle mix(List<EventHubEventDto> events, String requestedMode) {
|
||||||
|
|
@ -79,6 +91,7 @@ public class RuntimeEventMixingService {
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<RuntimeResolvedEvent> resolvedEvents = buildResolvedEvents(state, rawEvents);
|
List<RuntimeResolvedEvent> resolvedEvents = buildResolvedEvents(state, rawEvents);
|
||||||
|
RuntimeEventMixingDiagnostics diagnostics = diagnostics(descriptors, state);
|
||||||
List<String> notes = new ArrayList<>();
|
List<String> notes = new ArrayList<>();
|
||||||
notes.add("Runtime event mixing inspected " + rawEvents.size() + " event(s).");
|
notes.add("Runtime event mixing inspected " + rawEvents.size() + " event(s).");
|
||||||
notes.add("Runtime event mixing applied " + ruleRegistry.rulesForMode(mode).size() + " configured rule(s) in mode " + mode + ".");
|
notes.add("Runtime event mixing applied " + ruleRegistry.rulesForMode(mode).size() + " configured rule(s) in mode " + mode + ".");
|
||||||
|
|
@ -86,6 +99,12 @@ public class RuntimeEventMixingService {
|
||||||
+ " duplicate source event(s) from activity/support evidence channels.");
|
+ " duplicate source event(s) from activity/support evidence channels.");
|
||||||
notes.add("Runtime event mixing keeps CARD_POSITION, CARD_PLACE, and CARD_BORDER_CROSSING as primary when matching VU support evidence describes the same semantic event.");
|
notes.add("Runtime event mixing keeps CARD_POSITION, CARD_PLACE, and CARD_BORDER_CROSSING as primary when matching VU support evidence describes the same semantic event.");
|
||||||
notes.add("Runtime event mixing kept all CARD_VEHICLES_USED and IW_CYCLE card-usage point events unchanged for vehicle-usage processing.");
|
notes.add("Runtime event mixing kept all CARD_VEHICLES_USED and IW_CYCLE card-usage point events unchanged for vehicle-usage processing.");
|
||||||
|
notes.add("Runtime event mixing classified " + diagnostics.driverCardSourceRoleCount()
|
||||||
|
+ " DRIVER_CARD and " + diagnostics.vehicleUnitSourceRoleCount()
|
||||||
|
+ " VEHICLE_UNIT event(s); unknown source roles=" + diagnostics.unknownSourceRoleCount() + ".");
|
||||||
|
notes.add("Runtime event mixing classified " + diagnostics.databaseRepresentationCount()
|
||||||
|
+ " database and " + diagnostics.fileSessionRepresentationCount()
|
||||||
|
+ " file-session representation event(s).");
|
||||||
return new RuntimeMixedEventBundle(
|
return new RuntimeMixedEventBundle(
|
||||||
rawEvents,
|
rawEvents,
|
||||||
driverPartitionEvents,
|
driverPartitionEvents,
|
||||||
|
|
@ -95,6 +114,7 @@ public class RuntimeEventMixingService {
|
||||||
state.suppressedEvents(),
|
state.suppressedEvents(),
|
||||||
resolvedEvents,
|
resolvedEvents,
|
||||||
state.decisions(),
|
state.decisions(),
|
||||||
|
diagnostics,
|
||||||
notes,
|
notes,
|
||||||
state.warnings()
|
state.warnings()
|
||||||
);
|
);
|
||||||
|
|
@ -110,6 +130,7 @@ public class RuntimeEventMixingService {
|
||||||
List.of(),
|
List.of(),
|
||||||
descriptors.stream().map(this::defaultResolvedEvent).toList(),
|
descriptors.stream().map(this::defaultResolvedEvent).toList(),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
diagnostics(descriptors, new MixingState(descriptors)),
|
||||||
List.of(note),
|
List.of(note),
|
||||||
List.of()
|
List.of()
|
||||||
);
|
);
|
||||||
|
|
@ -149,23 +170,43 @@ public class RuntimeEventMixingService {
|
||||||
if (primaries.isEmpty() || secondaries.isEmpty()) {
|
if (primaries.isEmpty() || secondaries.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RuntimeEventDescriptor primary = primaries.getFirst();
|
state.incrementCandidateGroupCount();
|
||||||
List<RuntimeEventDescriptor> newlySuppressed = secondaries.stream()
|
|
||||||
.filter(descriptor -> !state.isSuppressed(descriptor))
|
|
||||||
.toList();
|
|
||||||
if (newlySuppressed.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (RuntimeEventDescriptor primary : primaries) {
|
||||||
|
List<RuntimeEventDescriptor> compatibleSecondaries = new ArrayList<>();
|
||||||
|
for (RuntimeEventDescriptor secondary : secondaries) {
|
||||||
|
if (state.isSuppressed(secondary)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (compatibilityMatcher.compatible(rule, primary, secondary)) {
|
||||||
|
compatibleSecondaries.add(secondary);
|
||||||
|
} else {
|
||||||
|
state.incrementCompatibilityRejectedCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (compatibleSecondaries.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fusePrimaryWithSecondaries(state, rule, eventKey, primary, compatibleSecondaries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fusePrimaryWithSecondaries(
|
||||||
|
MixingState state,
|
||||||
|
RuntimeEventMixingRule rule,
|
||||||
|
String eventKey,
|
||||||
|
RuntimeEventDescriptor primary,
|
||||||
|
List<RuntimeEventDescriptor> secondaries
|
||||||
|
) {
|
||||||
EventHubEventDto enrichedPrimary = enrichPrimaryVehicleRef(
|
EventHubEventDto enrichedPrimary = enrichPrimaryVehicleRef(
|
||||||
primary.event(),
|
primary.event(),
|
||||||
newlySuppressed.stream().map(RuntimeEventDescriptor::event).toList()
|
secondaries.stream().map(RuntimeEventDescriptor::event).toList()
|
||||||
);
|
);
|
||||||
if (enrichedPrimary != primary.event()) {
|
if (enrichedPrimary != primary.event()) {
|
||||||
state.replace(primary, enrichedPrimary);
|
state.replace(primary, enrichedPrimary);
|
||||||
}
|
}
|
||||||
newlySuppressed.forEach(descriptor -> state.suppress(descriptor, rule, primary, eventKey));
|
secondaries.forEach(descriptor -> state.suppress(descriptor, rule, primary, eventKey));
|
||||||
state.markPrimary(primary, rule, eventKey, newlySuppressed);
|
state.markPrimary(primary, rule, eventKey, secondaries);
|
||||||
state.decisions().add(new RuntimeEventMixingDecisionDto(
|
state.decisions().add(new RuntimeEventMixingDecisionDto(
|
||||||
rule.ruleId(),
|
rule.ruleId(),
|
||||||
rule.equivalenceType(),
|
rule.equivalenceType(),
|
||||||
|
|
@ -174,8 +215,8 @@ public class RuntimeEventMixingService {
|
||||||
rule.channel().name(),
|
rule.channel().name(),
|
||||||
primary.event().externalSourceEventId(),
|
primary.event().externalSourceEventId(),
|
||||||
primary.extractionCode(),
|
primary.extractionCode(),
|
||||||
newlySuppressed.stream().map(descriptor -> descriptor.event().externalSourceEventId()).toList(),
|
secondaries.stream().map(descriptor -> descriptor.event().externalSourceEventId()).toList(),
|
||||||
newlySuppressed.stream().map(RuntimeEventDescriptor::extractionCode).toList(),
|
secondaries.stream().map(RuntimeEventDescriptor::extractionCode).toList(),
|
||||||
primary.event().occurredAt(),
|
primary.event().occurredAt(),
|
||||||
primary.eventDomain() == null ? null : primary.eventDomain().name(),
|
primary.eventDomain() == null ? null : primary.eventDomain().name(),
|
||||||
primary.eventType() == null ? null : primary.eventType().name(),
|
primary.eventType() == null ? null : primary.eventType().name(),
|
||||||
|
|
@ -200,6 +241,54 @@ public class RuntimeEventMixingService {
|
||||||
return List.copyOf(resolved);
|
return List.copyOf(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RuntimeEventMixingDiagnostics diagnostics(
|
||||||
|
List<RuntimeEventDescriptor> descriptors,
|
||||||
|
MixingState state
|
||||||
|
) {
|
||||||
|
List<RuntimeEventDescriptor> safeDescriptors = descriptors == null ? List.of() : descriptors;
|
||||||
|
int tachographEventCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.sourceProfile() != null
|
||||||
|
&& descriptor.sourceProfile().isTachographRuntimeSource())
|
||||||
|
.count();
|
||||||
|
int driverCardCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.evidenceSourceRole()
|
||||||
|
== RuntimeTachographEvidenceSourceRole.DRIVER_CARD)
|
||||||
|
.count();
|
||||||
|
int vehicleUnitCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.evidenceSourceRole()
|
||||||
|
== RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT)
|
||||||
|
.count();
|
||||||
|
int unknownRoleCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.evidenceSourceRole()
|
||||||
|
== RuntimeTachographEvidenceSourceRole.UNKNOWN)
|
||||||
|
.count();
|
||||||
|
int databaseCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.representation()
|
||||||
|
== RuntimeTachographRepresentation.DATABASE)
|
||||||
|
.count();
|
||||||
|
int fileSessionCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.representation()
|
||||||
|
== RuntimeTachographRepresentation.FILE_SESSION)
|
||||||
|
.count();
|
||||||
|
int unknownRepresentationCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.representation()
|
||||||
|
== RuntimeTachographRepresentation.UNKNOWN)
|
||||||
|
.count();
|
||||||
|
return new RuntimeEventMixingDiagnostics(
|
||||||
|
safeDescriptors.size(),
|
||||||
|
tachographEventCount,
|
||||||
|
driverCardCount,
|
||||||
|
vehicleUnitCount,
|
||||||
|
unknownRoleCount,
|
||||||
|
databaseCount,
|
||||||
|
fileSessionCount,
|
||||||
|
unknownRepresentationCount,
|
||||||
|
state == null ? 0 : state.candidateGroupCount(),
|
||||||
|
state == null ? 0 : state.compatibilityRejectedCount(),
|
||||||
|
state == null ? 0 : state.suppressedEvents().size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private RuntimeResolvedEvent defaultResolvedEvent(RuntimeEventDescriptor descriptor) {
|
private RuntimeResolvedEvent defaultResolvedEvent(RuntimeEventDescriptor descriptor) {
|
||||||
RuntimeEventMixingChannel channel = defaultChannel(descriptor);
|
RuntimeEventMixingChannel channel = defaultChannel(descriptor);
|
||||||
RuntimeResolvedEventRole role = switch (channel) {
|
RuntimeResolvedEventRole role = switch (channel) {
|
||||||
|
|
@ -342,18 +431,22 @@ public class RuntimeEventMixingService {
|
||||||
|
|
||||||
private static final class MixingState {
|
private static final class MixingState {
|
||||||
private final List<RuntimeEventDescriptor> descriptors;
|
private final List<RuntimeEventDescriptor> descriptors;
|
||||||
private final Map<String, RuntimeEventDescriptor> descriptorsByEventId = new LinkedHashMap<>();
|
private final Map<EventHubEventDto, RuntimeEventDescriptor> descriptorsByEvent = new IdentityHashMap<>();
|
||||||
private final Set<String> suppressedEventIds = new LinkedHashSet<>();
|
private final Set<String> suppressedEventIds = new LinkedHashSet<>();
|
||||||
private final Map<String, EventHubEventDto> replacementsByEventId = new LinkedHashMap<>();
|
private final Map<String, EventHubEventDto> replacementsByEventId = new LinkedHashMap<>();
|
||||||
private final Map<String, RuntimeResolvedEvent> resolvedEventsByEventId = new LinkedHashMap<>();
|
private final Map<String, RuntimeResolvedEvent> resolvedEventsByEventId = new LinkedHashMap<>();
|
||||||
private final List<EventHubEventDto> suppressedEvents = new ArrayList<>();
|
private final List<EventHubEventDto> suppressedEvents = new ArrayList<>();
|
||||||
private final List<RuntimeEventMixingDecisionDto> decisions = new ArrayList<>();
|
private final List<RuntimeEventMixingDecisionDto> decisions = new ArrayList<>();
|
||||||
private final List<String> warnings = new ArrayList<>();
|
private final List<String> warnings = new ArrayList<>();
|
||||||
|
private int candidateGroupCount;
|
||||||
|
private int compatibilityRejectedCount;
|
||||||
|
|
||||||
private MixingState(List<RuntimeEventDescriptor> descriptors) {
|
private MixingState(List<RuntimeEventDescriptor> descriptors) {
|
||||||
this.descriptors = descriptors == null ? List.of() : List.copyOf(descriptors);
|
this.descriptors = descriptors == null ? List.of() : List.copyOf(descriptors);
|
||||||
for (RuntimeEventDescriptor descriptor : this.descriptors) {
|
for (RuntimeEventDescriptor descriptor : this.descriptors) {
|
||||||
descriptorsByEventId.put(descriptor.eventIdentityKey(), descriptor);
|
if (descriptor.event() != null) {
|
||||||
|
descriptorsByEvent.put(descriptor.event(), descriptor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -382,9 +475,9 @@ public class RuntimeEventMixingService {
|
||||||
if (event == null) {
|
if (event == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String externalId = event.externalSourceEventId();
|
RuntimeEventDescriptor direct = descriptorsByEvent.get(event);
|
||||||
if (externalId != null && descriptorsByEventId.containsKey(externalId)) {
|
if (direct != null) {
|
||||||
return descriptorsByEventId.get(externalId);
|
return direct;
|
||||||
}
|
}
|
||||||
return descriptors.stream()
|
return descriptors.stream()
|
||||||
.filter(descriptor -> descriptor.event() == event || Objects.equals(descriptor.event(), event))
|
.filter(descriptor -> descriptor.event() == event || Objects.equals(descriptor.event(), event))
|
||||||
|
|
@ -464,6 +557,22 @@ public class RuntimeEventMixingService {
|
||||||
return decisions;
|
return decisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void incrementCandidateGroupCount() {
|
||||||
|
candidateGroupCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int candidateGroupCount() {
|
||||||
|
return candidateGroupCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementCompatibilityRejectedCount() {
|
||||||
|
compatibilityRejectedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compatibilityRejectedCount() {
|
||||||
|
return compatibilityRejectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> warnings() {
|
private List<String> warnings() {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,34 @@ package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
public record RuntimeEventSourceProfile(
|
public record RuntimeEventSourceProfile(
|
||||||
String sourceSystem,
|
String sourceSystem,
|
||||||
String sourceKind,
|
String sourceKind,
|
||||||
String extractionCode
|
String extractionCode,
|
||||||
|
RuntimeTachographEvidenceSourceRole evidenceSourceRole,
|
||||||
|
RuntimeTachographRepresentation representation
|
||||||
) {
|
) {
|
||||||
|
/** Compatibility constructor retained for existing tests and direct callers. */
|
||||||
|
public RuntimeEventSourceProfile(
|
||||||
|
String sourceSystem,
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
sourceSystem,
|
||||||
|
sourceKind,
|
||||||
|
extractionCode,
|
||||||
|
RuntimeTachographEvidenceSourceRole.UNKNOWN,
|
||||||
|
RuntimeTachographRepresentation.UNKNOWN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeEventSourceProfile {
|
||||||
|
evidenceSourceRole = evidenceSourceRole == null
|
||||||
|
? RuntimeTachographEvidenceSourceRole.UNKNOWN
|
||||||
|
: evidenceSourceRole;
|
||||||
|
representation = representation == null
|
||||||
|
? RuntimeTachographRepresentation.UNKNOWN
|
||||||
|
: representation;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTachographRuntimeSource() {
|
public boolean isTachographRuntimeSource() {
|
||||||
return switch (sourceSystem == null ? "" : sourceSystem) {
|
return switch (sourceSystem == null ? "" : sourceSystem) {
|
||||||
case "TACHOGRAPH", "TACHOGRAPH_FILE_SESSION", "COMPOSITE_TACHOGRAPH_FILE_SESSION" -> true;
|
case "TACHOGRAPH", "TACHOGRAPH_FILE_SESSION", "COMPOSITE_TACHOGRAPH_FILE_SESSION" -> true;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ public record RuntimeMixedEventBundle(
|
||||||
List<EventHubEventDto> suppressedEvents,
|
List<EventHubEventDto> suppressedEvents,
|
||||||
List<RuntimeResolvedEvent> resolvedEvents,
|
List<RuntimeResolvedEvent> resolvedEvents,
|
||||||
List<RuntimeEventMixingDecisionDto> eventMixingDecisions,
|
List<RuntimeEventMixingDecisionDto> eventMixingDecisions,
|
||||||
|
RuntimeEventMixingDiagnostics diagnostics,
|
||||||
List<String> notes,
|
List<String> notes,
|
||||||
List<String> warnings
|
List<String> warnings
|
||||||
) {
|
) {
|
||||||
|
|
@ -24,6 +25,9 @@ public record RuntimeMixedEventBundle(
|
||||||
suppressedEvents = suppressedEvents == null ? List.of() : List.copyOf(suppressedEvents);
|
suppressedEvents = suppressedEvents == null ? List.of() : List.copyOf(suppressedEvents);
|
||||||
resolvedEvents = resolvedEvents == null ? List.of() : List.copyOf(resolvedEvents);
|
resolvedEvents = resolvedEvents == null ? List.of() : List.copyOf(resolvedEvents);
|
||||||
eventMixingDecisions = eventMixingDecisions == null ? List.of() : List.copyOf(eventMixingDecisions);
|
eventMixingDecisions = eventMixingDecisions == null ? List.of() : List.copyOf(eventMixingDecisions);
|
||||||
|
diagnostics = diagnostics == null
|
||||||
|
? new RuntimeEventMixingDiagnostics(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||||
|
: diagnostics;
|
||||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,415 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.EventDomain;
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.dto.EventLifecycle;
|
||||||
|
import at.procon.eventhub.processing.support.RuntimeEntityReferenceResolver;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical semantic view of tachograph runtime events.
|
||||||
|
*
|
||||||
|
* <p>The same tachograph package can be processed directly from a file session or loaded from
|
||||||
|
* the tachograph database after serialization. This component normalizes only representation
|
||||||
|
* differences relevant to runtime mixing and leaves the original event untouched.</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RuntimeTachographEventSemantics {
|
||||||
|
|
||||||
|
private static final Set<String> TACHOGRAPH_SOURCE_SYSTEMS = Set.of(
|
||||||
|
"TACHOGRAPH",
|
||||||
|
"TACHOGRAPH_FILE_SESSION",
|
||||||
|
"COMPOSITE_TACHOGRAPH_FILE_SESSION"
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final Set<String> KNOWN_EXTRACTION_CODES = Set.of(
|
||||||
|
"CARD_ACTIVITY",
|
||||||
|
"VU_ACTIVITY",
|
||||||
|
"CARD_VEHICLES_USED",
|
||||||
|
"IW_CYCLE",
|
||||||
|
"CARD_POSITION",
|
||||||
|
"VU_POSITION",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"VU_PLACE",
|
||||||
|
"CARD_BORDER_CROSSING",
|
||||||
|
"VU_BORDER_CROSSING",
|
||||||
|
"CARD_LOAD_UNLOAD",
|
||||||
|
"VU_LOAD_UNLOAD",
|
||||||
|
"CARD_SPECIFIC_CONDITION",
|
||||||
|
"VU_SPECIFIC_CONDITION",
|
||||||
|
"SPEEDING_EVENTS"
|
||||||
|
);
|
||||||
|
|
||||||
|
public RuntimeEventSourceProfile sourceProfile(EventHubEventDto event) {
|
||||||
|
JsonNode raw = rawPayload(event);
|
||||||
|
String explicitExtractionCode = normalizeUpper(firstNonBlank(
|
||||||
|
text(raw, "extractionCode"),
|
||||||
|
extractionCodeFromExternalSourceEventId(event)
|
||||||
|
));
|
||||||
|
|
||||||
|
String sourceKindCandidate = normalizeUpper(firstNonBlank(
|
||||||
|
text(raw, "sourceKind"),
|
||||||
|
sourceKind(event),
|
||||||
|
sourceKey(event),
|
||||||
|
sourcePackageKind(event),
|
||||||
|
sourceKindFromExtractionCode(explicitExtractionCode)
|
||||||
|
));
|
||||||
|
RuntimeTachographEvidenceSourceRole sourceRole = evidenceSourceRole(
|
||||||
|
sourceKindCandidate,
|
||||||
|
explicitExtractionCode,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
String sourceKind = canonicalSourceKind(sourceRole, sourceKindCandidate);
|
||||||
|
|
||||||
|
String extractionCode = normalizeUpper(firstNonBlank(
|
||||||
|
explicitExtractionCode,
|
||||||
|
inferExtractionCode(event, sourceRole)
|
||||||
|
));
|
||||||
|
if (sourceRole == RuntimeTachographEvidenceSourceRole.UNKNOWN) {
|
||||||
|
sourceRole = evidenceSourceRole(sourceKind, extractionCode, event);
|
||||||
|
sourceKind = canonicalSourceKind(sourceRole, sourceKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
String sourceSystemCandidate = normalizeUpper(firstNonBlank(
|
||||||
|
text(raw, "sourceSystem"),
|
||||||
|
sourceProvider(event),
|
||||||
|
sourceSystemFromExternalSourceEventId(event),
|
||||||
|
sourcePackageKind(event)
|
||||||
|
));
|
||||||
|
boolean tachograph = isTachographRepresentation(event, sourceSystemCandidate, extractionCode);
|
||||||
|
String sourceSystem = tachograph ? "TACHOGRAPH" : sourceSystemCandidate;
|
||||||
|
RuntimeTachographRepresentation representation = tachograph
|
||||||
|
? representation(event)
|
||||||
|
: RuntimeTachographRepresentation.UNKNOWN;
|
||||||
|
|
||||||
|
return new RuntimeEventSourceProfile(
|
||||||
|
sourceSystem,
|
||||||
|
sourceKind,
|
||||||
|
extractionCode,
|
||||||
|
sourceRole,
|
||||||
|
representation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a semantic lifecycle used only for equivalence matching.
|
||||||
|
* DB place events use START while file-session place events use BEGIN for the same fact.
|
||||||
|
*/
|
||||||
|
public String semanticLifecycle(EventHubEventDto event) {
|
||||||
|
if (event == null || event.lifecycle() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (event.eventDomain() == EventDomain.PLACE
|
||||||
|
&& (event.lifecycle() == EventLifecycle.START || event.lifecycle() == EventLifecycle.BEGIN)) {
|
||||||
|
return EventLifecycle.BEGIN.name();
|
||||||
|
}
|
||||||
|
return event.lifecycle().name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String inferExtractionCode(EventHubEventDto event, String sourceKind) {
|
||||||
|
return inferExtractionCode(event, evidenceSourceRole(sourceKind, null, event));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String inferExtractionCode(
|
||||||
|
EventHubEventDto event,
|
||||||
|
RuntimeTachographEvidenceSourceRole sourceRole
|
||||||
|
) {
|
||||||
|
if (event == null || event.eventDomain() == null || sourceRole == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
|
||||||
|
return switch (sourceRole) {
|
||||||
|
case DRIVER_CARD -> "CARD_ACTIVITY";
|
||||||
|
case VEHICLE_UNIT -> "VU_ACTIVITY";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (event.eventDomain() == EventDomain.DRIVER_CARD) {
|
||||||
|
return switch (sourceRole) {
|
||||||
|
case DRIVER_CARD -> "CARD_VEHICLES_USED";
|
||||||
|
case VEHICLE_UNIT -> "IW_CYCLE";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
String prefix = switch (sourceRole) {
|
||||||
|
case DRIVER_CARD -> "CARD";
|
||||||
|
case VEHICLE_UNIT -> "VU";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (prefix == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return switch (event.eventDomain()) {
|
||||||
|
case POSITION -> prefix + "_POSITION";
|
||||||
|
case PLACE -> prefix + "_PLACE";
|
||||||
|
case BORDER_CROSSING -> prefix + "_BORDER_CROSSING";
|
||||||
|
case LOAD_UNLOAD -> prefix + "_LOAD_UNLOAD";
|
||||||
|
case SPECIFIC_CONDITION -> prefix + "_SPECIFIC_CONDITION";
|
||||||
|
case SPEEDING -> Objects.equals("VU", prefix) ? "SPEEDING_EVENTS" : null;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographEvidenceSourceRole evidenceSourceRole(EventHubEventDto event) {
|
||||||
|
return sourceProfile(event).evidenceSourceRole();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographRepresentation representation(EventHubEventDto event) {
|
||||||
|
String externalId = normalizeUpper(event == null ? null : event.externalSourceEventId());
|
||||||
|
String packageKind = normalizeUpper(sourcePackageKind(event));
|
||||||
|
String externalPackageId = normalizeUpper(externalPackageId(event));
|
||||||
|
String rawSourceSystem = normalizeUpper(text(rawPayload(event), "sourceSystem"));
|
||||||
|
String provider = normalizeUpper(sourceProvider(event));
|
||||||
|
String sourceKey = normalizeUpper(sourceKey(event));
|
||||||
|
|
||||||
|
// The runtime DB loader intentionally keeps the original source-package metadata.
|
||||||
|
// Therefore RUNTIME:TACHOGRAPH must take precedence over packageKind markers that may
|
||||||
|
// still say TACHOGRAPH_FILE_SESSION after the package has been serialized in the DB.
|
||||||
|
if (startsWithAny(externalPackageId, "RUNTIME:TACHOGRAPH:")) {
|
||||||
|
return RuntimeTachographRepresentation.DATABASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWithAny(externalId,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:",
|
||||||
|
"COMPOSITE_TACHOGRAPH_FILE_SESSION:")
|
||||||
|
|| containsFileSessionMarker(externalPackageId)
|
||||||
|
|| containsFileSessionMarker(rawSourceSystem)
|
||||||
|
|| containsFileSessionMarker(provider)
|
||||||
|
|| containsFileSessionMarker(sourceKey)
|
||||||
|
|| containsFileSessionMarker(packageKind)) {
|
||||||
|
return RuntimeTachographRepresentation.FILE_SESSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWithAny(externalId, "TACHOGRAPH:")
|
||||||
|
|| TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(normalizeUpper(sourceProvider(event))))
|
||||||
|
|| KNOWN_EXTRACTION_CODES.contains(nullToEmpty(normalizeUpper(
|
||||||
|
firstNonBlank(
|
||||||
|
text(rawPayload(event), "extractionCode"),
|
||||||
|
extractionCodeFromExternalSourceEventId(event)
|
||||||
|
)
|
||||||
|
)))) {
|
||||||
|
return RuntimeTachographRepresentation.DATABASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RuntimeTachographRepresentation.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTachographRepresentation(EventHubEventDto event) {
|
||||||
|
return sourceProfile(event).isTachographRuntimeSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeTachographEvidenceSourceRole evidenceSourceRole(
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
EventHubEventDto event
|
||||||
|
) {
|
||||||
|
String normalizedExtraction = normalizeUpper(extractionCode);
|
||||||
|
if (normalizedExtraction != null) {
|
||||||
|
if (normalizedExtraction.startsWith("CARD_")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.DRIVER_CARD;
|
||||||
|
}
|
||||||
|
if (normalizedExtraction.startsWith("VU_")
|
||||||
|
|| Objects.equals("IW_CYCLE", normalizedExtraction)
|
||||||
|
|| Objects.equals("SPEEDING_EVENTS", normalizedExtraction)) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] candidates = {
|
||||||
|
sourceKind,
|
||||||
|
text(rawPayload(event), "sourceKind"),
|
||||||
|
sourceKind(event),
|
||||||
|
sourceKey(event),
|
||||||
|
sourcePackageKind(event)
|
||||||
|
};
|
||||||
|
for (String value : candidates) {
|
||||||
|
RuntimeTachographEvidenceSourceRole resolved = roleFromToken(value);
|
||||||
|
if (resolved != RuntimeTachographEvidenceSourceRole.UNKNOWN) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String externalId = normalizeUpper(event == null ? null : event.externalSourceEventId());
|
||||||
|
if (externalId != null) {
|
||||||
|
if (externalId.contains(":CARD_") || externalId.contains(":CARDPLACE-")
|
||||||
|
|| externalId.contains(":CARDGNSS-")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.DRIVER_CARD;
|
||||||
|
}
|
||||||
|
if (externalId.contains(":VU_") || externalId.contains(":VUPLACE-")
|
||||||
|
|| externalId.contains(":VUGNSS-")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RuntimeTachographEvidenceSourceRole.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeTachographEvidenceSourceRole roleFromToken(String value) {
|
||||||
|
String candidate = normalizeUpper(value);
|
||||||
|
if (candidate == null) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.UNKNOWN;
|
||||||
|
}
|
||||||
|
if (candidate.equals("DRIVER_CARD")
|
||||||
|
|| candidate.equals("CARD")
|
||||||
|
|| candidate.contains("DRIVER_CARD")
|
||||||
|
|| candidate.startsWith("CARD_")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.DRIVER_CARD;
|
||||||
|
}
|
||||||
|
if (candidate.equals("VEHICLE_UNIT")
|
||||||
|
|| candidate.equals("VU")
|
||||||
|
|| candidate.contains("VEHICLE_UNIT")
|
||||||
|
|| candidate.startsWith("VU_")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT;
|
||||||
|
}
|
||||||
|
return RuntimeTachographEvidenceSourceRole.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String canonicalSourceKind(
|
||||||
|
RuntimeTachographEvidenceSourceRole role,
|
||||||
|
String fallback
|
||||||
|
) {
|
||||||
|
return switch (role == null ? RuntimeTachographEvidenceSourceRole.UNKNOWN : role) {
|
||||||
|
case DRIVER_CARD -> "DRIVER_CARD";
|
||||||
|
case VEHICLE_UNIT -> "VEHICLE_UNIT";
|
||||||
|
case UNKNOWN -> normalizeUpper(fallback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTachographRepresentation(
|
||||||
|
EventHubEventDto event,
|
||||||
|
String sourceSystemCandidate,
|
||||||
|
String extractionCode
|
||||||
|
) {
|
||||||
|
if (TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(sourceSystemCandidate))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (KNOWN_EXTRACTION_CODES.contains(nullToEmpty(extractionCode))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String packageKind = normalizeUpper(sourcePackageKind(event));
|
||||||
|
if (TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(packageKind))
|
||||||
|
|| containsFileSessionMarker(packageKind)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String sourceKey = normalizeUpper(sourceKey(event));
|
||||||
|
if (sourceKey != null && sourceKey.startsWith("TACHOGRAPH")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String externalId = event == null ? null : event.externalSourceEventId();
|
||||||
|
return externalId != null
|
||||||
|
&& (externalId.startsWith("TACHOGRAPH:")
|
||||||
|
|| externalId.startsWith("TACHOGRAPH_FILE_SESSION:")
|
||||||
|
|| externalId.startsWith("COMPOSITE_TACHOGRAPH_FILE_SESSION:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode rawPayload(EventHubEventDto event) {
|
||||||
|
return RuntimeEntityReferenceResolver.rawPayload(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceKind(EventHubEventDto event) {
|
||||||
|
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
||||||
|
? null
|
||||||
|
: event.packageInfo().eventSource().sourceKind();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceKey(EventHubEventDto event) {
|
||||||
|
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
||||||
|
? null
|
||||||
|
: event.packageInfo().eventSource().sourceKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceProvider(EventHubEventDto event) {
|
||||||
|
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
||||||
|
? null
|
||||||
|
: event.packageInfo().eventSource().providerKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourcePackageKind(EventHubEventDto event) {
|
||||||
|
String fromRef = event == null || event.sourcePackageRef() == null
|
||||||
|
? null
|
||||||
|
: event.sourcePackageRef().packageKind();
|
||||||
|
return firstNonBlank(fromRef, text(rawPayload(event), "sourcePackageKind"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String externalPackageId(EventHubEventDto event) {
|
||||||
|
return event == null || event.packageInfo() == null ? null : event.packageInfo().externalPackageId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceSystemFromExternalSourceEventId(EventHubEventDto event) {
|
||||||
|
String externalId = event == null ? null : event.externalSourceEventId();
|
||||||
|
if (externalId == null || externalId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] parts = externalId.split(":");
|
||||||
|
return parts.length >= 1 ? parts[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractionCodeFromExternalSourceEventId(EventHubEventDto event) {
|
||||||
|
String externalId = event == null ? null : event.externalSourceEventId();
|
||||||
|
if (externalId == null || externalId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] parts = externalId.split(":");
|
||||||
|
if (parts.length < 2 || !"TACHOGRAPH".equals(normalizeUpper(parts[0]))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String candidate = normalizeUpper(parts[1]);
|
||||||
|
return KNOWN_EXTRACTION_CODES.contains(nullToEmpty(candidate)) ? candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceKindFromExtractionCode(String extractionCode) {
|
||||||
|
RuntimeTachographEvidenceSourceRole role = evidenceSourceRole(null, extractionCode, null);
|
||||||
|
return canonicalSourceKind(role, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsFileSessionMarker(String value) {
|
||||||
|
return value != null && value.contains("FILE_SESSION");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startsWithAny(String value, String... prefixes) {
|
||||||
|
if (value == null || prefixes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String prefix : prefixes) {
|
||||||
|
if (prefix != null && value.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String field) {
|
||||||
|
if (node == null || field == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JsonNode value = node.get(field);
|
||||||
|
if (value == null || value.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String text = value.asText(null);
|
||||||
|
return text == null || text.isBlank() ? null : text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeUpper(String value) {
|
||||||
|
return value == null || value.isBlank() ? null : value.trim().toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nullToEmpty(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic tachograph evidence side used by runtime mixing rules.
|
||||||
|
*
|
||||||
|
* <p>This is intentionally independent from the physical representation. The same
|
||||||
|
* driver-card or vehicle-unit fact can be loaded from a file session or from the
|
||||||
|
* tachograph database.</p>
|
||||||
|
*/
|
||||||
|
public enum RuntimeTachographEvidenceSourceRole {
|
||||||
|
DRIVER_CARD,
|
||||||
|
VEHICLE_UNIT,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
/** Physical representation from which a tachograph runtime event was loaded. */
|
||||||
|
public enum RuntimeTachographRepresentation {
|
||||||
|
FILE_SESSION,
|
||||||
|
DATABASE,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
@ -129,7 +129,7 @@ public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcess
|
||||||
|
|
||||||
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
|
UnifiedRuntimeDriverWorkingTimeScopeResultDto result = new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
|
||||||
broadBundle.request(),
|
broadBundle.request(),
|
||||||
broadBundle.mergedEvents().size(),
|
broadBundle.aggregatedEventCount(),
|
||||||
driverResults.size(),
|
driverResults.size(),
|
||||||
broadBundle.discoveredVehicles().size(),
|
broadBundle.discoveredVehicles().size(),
|
||||||
broadBundle.discoveredVehicles(),
|
broadBundle.discoveredVehicles(),
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
|
||||||
return new RuntimeProcessingModuleDescriptorDto(
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
"Event evidence mixing",
|
"Event evidence mixing",
|
||||||
"Applies source-aware runtime evidence rules before intervalization. The rule registry currently collapses duplicate tachograph card/VU activity, position, place, and border evidence while keeping CARD_VEHICLES_USED/IW_CYCLE unchanged for vehicle-usage processing.",
|
"Applies source-aware runtime evidence rules before intervalization. The rule registry collapses duplicate tachograph card/VU evidence and duplicate file-session/database representations while keeping CARD_VEHICLES_USED/IW_CYCLE unchanged for vehicle-usage processing.",
|
||||||
"JAVA",
|
"JAVA",
|
||||||
Set.of(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY),
|
Set.of(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY),
|
||||||
Set.of("UnifiedRuntimeEventBundle"),
|
Set.of("UnifiedRuntimeEventBundle"),
|
||||||
|
|
@ -64,6 +64,15 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
|
||||||
metadata.put("resolvedEventCount", mixed.resolvedEvents().size());
|
metadata.put("resolvedEventCount", mixed.resolvedEvents().size());
|
||||||
metadata.put("eventMixingDecisionCount", mixed.eventMixingDecisions().size());
|
metadata.put("eventMixingDecisionCount", mixed.eventMixingDecisions().size());
|
||||||
metadata.put("eventMixingMode", eventMixingMode(context));
|
metadata.put("eventMixingMode", eventMixingMode(context));
|
||||||
|
metadata.put("tachographEventCount", mixed.diagnostics().tachographEventCount());
|
||||||
|
metadata.put("driverCardSourceRoleCount", mixed.diagnostics().driverCardSourceRoleCount());
|
||||||
|
metadata.put("vehicleUnitSourceRoleCount", mixed.diagnostics().vehicleUnitSourceRoleCount());
|
||||||
|
metadata.put("unknownSourceRoleCount", mixed.diagnostics().unknownSourceRoleCount());
|
||||||
|
metadata.put("databaseRepresentationCount", mixed.diagnostics().databaseRepresentationCount());
|
||||||
|
metadata.put("fileSessionRepresentationCount", mixed.diagnostics().fileSessionRepresentationCount());
|
||||||
|
metadata.put("unknownRepresentationCount", mixed.diagnostics().unknownRepresentationCount());
|
||||||
|
metadata.put("candidateGroupCount", mixed.diagnostics().candidateGroupCount());
|
||||||
|
metadata.put("compatibilityRejectedCount", mixed.diagnostics().compatibilityRejectedCount());
|
||||||
return new RuntimeProcessingModuleResult(
|
return new RuntimeProcessingModuleResult(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
RuntimeProcessingModuleStatus.SUCCESS,
|
RuntimeProcessingModuleStatus.SUCCESS,
|
||||||
|
|
@ -89,7 +98,7 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
|
||||||
private List<EventHubEventDto> sourceEvents(RuntimeProcessingModuleContext context) {
|
private List<EventHubEventDto> sourceEvents(RuntimeProcessingModuleContext context) {
|
||||||
RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
|
RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
|
||||||
if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) {
|
if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) {
|
||||||
return bundle.mergedEvents();
|
return bundle.aggregatedEvents();
|
||||||
}
|
}
|
||||||
return context.events();
|
return context.events();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ public class RuntimeEventAssemblyModule implements RuntimeProcessingModule {
|
||||||
return new RuntimeProcessingModuleDescriptorDto(
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
"Runtime event assembly",
|
"Runtime event assembly",
|
||||||
"Loads and merges canonical runtime events from the selected source scope before plan-specific processing.",
|
"Loads driver seed events and optionally aggregates additional events for discovered vehicles into a broad runtime scope before plan-specific mixing and reconciliation.",
|
||||||
"JAVA",
|
"JAVA",
|
||||||
Set.of(),
|
Set.of(),
|
||||||
Set.of("runtime source selection"),
|
Set.of("runtime source selection"),
|
||||||
|
|
@ -45,7 +45,9 @@ public class RuntimeEventAssemblyModule implements RuntimeProcessingModule {
|
||||||
metadata.put("vehicleExpansionPaddingMinutes", scopeRequest.vehicleExpansionPaddingMinutes() == null ? 0 : scopeRequest.vehicleExpansionPaddingMinutes());
|
metadata.put("vehicleExpansionPaddingMinutes", scopeRequest.vehicleExpansionPaddingMinutes() == null ? 0 : scopeRequest.vehicleExpansionPaddingMinutes());
|
||||||
metadata.put("driverSeedEventCount", bundle.driverSeedEvents().size());
|
metadata.put("driverSeedEventCount", bundle.driverSeedEvents().size());
|
||||||
metadata.put("expandedVehicleEventCount", bundle.expandedVehicleEvents().size());
|
metadata.put("expandedVehicleEventCount", bundle.expandedVehicleEvents().size());
|
||||||
metadata.put("mergedEventCount", bundle.mergedEvents().size());
|
metadata.put("aggregatedEventCount", bundle.aggregatedEventCount());
|
||||||
|
// Compatibility alias retained for existing API clients and stored execution metadata.
|
||||||
|
metadata.put("mergedEventCount", bundle.aggregatedEventCount());
|
||||||
metadata.put("discoveredVehicleCount", bundle.discoveredVehicles().size());
|
metadata.put("discoveredVehicleCount", bundle.discoveredVehicles().size());
|
||||||
return new RuntimeProcessingModuleResult(
|
return new RuntimeProcessingModuleResult(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,9 @@ public class VehicleEvidenceAttachmentModule implements RuntimeProcessingModule
|
||||||
.mapToInt(partition -> partition.attachedVehicleEvidenceEvents().size())
|
.mapToInt(partition -> partition.attachedVehicleEvidenceEvents().size())
|
||||||
.sum());
|
.sum());
|
||||||
metadata.put("partitionSourceEventCount", partitionSourceEvents.size());
|
metadata.put("partitionSourceEventCount", partitionSourceEvents.size());
|
||||||
metadata.put("rawMergedEventCount", broadBundle.mergedEvents().size());
|
metadata.put("rawAggregatedEventCount", broadBundle.aggregatedEventCount());
|
||||||
|
// Compatibility alias retained for existing execution metadata consumers.
|
||||||
|
metadata.put("rawMergedEventCount", broadBundle.aggregatedEventCount());
|
||||||
return new RuntimeProcessingModuleResult(
|
return new RuntimeProcessingModuleResult(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
RuntimeProcessingModuleStatus.SUCCESS,
|
RuntimeProcessingModuleStatus.SUCCESS,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public final class DriverWorkingTimeEplEventMapper {
|
||||||
public static List<EventHubEventDto> sourceEvents(RuntimeProcessingModuleContext context) {
|
public static List<EventHubEventDto> sourceEvents(RuntimeProcessingModuleContext context) {
|
||||||
RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
|
RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY);
|
||||||
if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) {
|
if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) {
|
||||||
return safeList(bundle.mergedEvents());
|
return safeList(bundle.aggregatedEvents());
|
||||||
}
|
}
|
||||||
return safeList(context.events());
|
return safeList(context.events());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessing
|
||||||
descriptors.add(new RuntimeProcessingModuleDescriptorDto(
|
descriptors.add(new RuntimeProcessingModuleDescriptorDto(
|
||||||
DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY,
|
DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY,
|
||||||
"Runtime event assembly",
|
"Runtime event assembly",
|
||||||
"Loads and merges canonical runtime events from the selected source scope before plan-specific processing.",
|
"Loads driver seed events and optionally aggregates additional events for discovered vehicles into a broad runtime scope before plan-specific mixing and reconciliation.",
|
||||||
"JAVA",
|
"JAVA",
|
||||||
Set.of(),
|
Set.of(),
|
||||||
Set.of("runtime source selection"),
|
Set.of("runtime source selection"),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
package at.procon.eventhub.processing.model;
|
package at.procon.eventhub.processing.model;
|
||||||
|
|
||||||
import at.procon.eventhub.dto.EventHubEventDto;
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime event scope assembled for subsequent processing modules.
|
||||||
|
*
|
||||||
|
* <p>The legacy {@code mergedEvents} component contains the event aggregation represented by this
|
||||||
|
* bundle. For the assembly-stage bundle, this is the broad scope of driver seed events and optionally
|
||||||
|
* expanded vehicle events after canonical de-duplication and ordering; it is not the later semantic
|
||||||
|
* card/VU mixing or interval-reconciliation result. New assembly-stage code should prefer
|
||||||
|
* {@link #aggregatedEvents()}.
|
||||||
|
*/
|
||||||
public record UnifiedRuntimeEventBundle(
|
public record UnifiedRuntimeEventBundle(
|
||||||
UnifiedRuntimeProcessingRequest request,
|
UnifiedRuntimeProcessingRequest request,
|
||||||
List<EventHubEventDto> driverSeedEvents,
|
List<EventHubEventDto> driverSeedEvents,
|
||||||
|
|
@ -18,4 +28,19 @@ public record UnifiedRuntimeEventBundle(
|
||||||
mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents);
|
mergedEvents = mergedEvents == null ? List.of() : List.copyOf(mergedEvents);
|
||||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferred terminology for the broad event scope assembled before plan-specific mixing.
|
||||||
|
*
|
||||||
|
* @return the same immutable list exposed by the legacy {@link #mergedEvents()} accessor
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
public List<EventHubEventDto> aggregatedEvents() {
|
||||||
|
return mergedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public int aggregatedEventCount() {
|
||||||
|
return mergedEvents.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ public class RuntimeDriverWorkingTimeScopeProcessingService {
|
||||||
) {
|
) {
|
||||||
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
|
UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest();
|
||||||
UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request);
|
UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request);
|
||||||
LinkedHashSet<String> selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents());
|
LinkedHashSet<String> selectedDriverKeys = selectedDriverKeys(request, broadBundle.aggregatedEvents());
|
||||||
if (selectedDriverKeys.isEmpty()) {
|
if (selectedDriverKeys.isEmpty()) {
|
||||||
throw new IllegalArgumentException("No driver partitions could be resolved from the runtime event scope.");
|
throw new IllegalArgumentException("No driver partitions could be resolved from the runtime event scope.");
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +107,7 @@ public class RuntimeDriverWorkingTimeScopeProcessingService {
|
||||||
|
|
||||||
return new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
|
return new UnifiedRuntimeDriverWorkingTimeScopeResultDto(
|
||||||
request,
|
request,
|
||||||
broadBundle.mergedEvents().size(),
|
broadBundle.aggregatedEventCount(),
|
||||||
driverResults.size(),
|
driverResults.size(),
|
||||||
broadBundle.discoveredVehicles().size(),
|
broadBundle.discoveredVehicles().size(),
|
||||||
broadBundle.discoveredVehicles(),
|
broadBundle.discoveredVehicles(),
|
||||||
|
|
@ -151,13 +151,13 @@ public class RuntimeDriverWorkingTimeScopeProcessingService {
|
||||||
String driverKey,
|
String driverKey,
|
||||||
boolean includePartitionDebug
|
boolean includePartitionDebug
|
||||||
) {
|
) {
|
||||||
List<EventHubEventDto> directDriverEvents = broadBundle.mergedEvents().stream()
|
List<EventHubEventDto> directDriverEvents = broadBundle.aggregatedEvents().stream()
|
||||||
.filter(event -> Objects.equals(driverKey(event), driverKey))
|
.filter(event -> Objects.equals(driverKey(event), driverKey))
|
||||||
.toList();
|
.toList();
|
||||||
RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
|
RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence(
|
||||||
driverKey,
|
driverKey,
|
||||||
directDriverEvents,
|
directDriverEvents,
|
||||||
broadBundle.mergedEvents(),
|
broadBundle.aggregatedEvents(),
|
||||||
request.expandVehicleEvents(),
|
request.expandVehicleEvents(),
|
||||||
request.vehicleExpansionPaddingMinutes(),
|
request.vehicleExpansionPaddingMinutes(),
|
||||||
includePartitionDebug
|
includePartitionDebug
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
package at.procon.eventhub.processing.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.dto.SourcePackageRefDto;
|
||||||
|
import at.procon.eventhub.processing.eventprocessing.mixing.RuntimeEventSourceProfile;
|
||||||
|
import at.procon.eventhub.processing.eventprocessing.mixing.RuntimeTachographEventSemantics;
|
||||||
|
import at.procon.eventhub.processing.support.RuntimeEntityReferenceResolver;
|
||||||
|
import at.procon.eventhub.processing.support.RuntimeEventIdentityResolver;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates runtime events before plan-specific semantic processing.
|
||||||
|
*
|
||||||
|
* <p>This service removes only repeated reads of the same physical source record. It deliberately
|
||||||
|
* does not collapse merely semantically equal events. Card/VU observations must remain available
|
||||||
|
* for {@code RuntimeEventMixingService}, while CARD_VEHICLES_USED/IW_CYCLE observations must remain
|
||||||
|
* available for interval-level reconciliation.</p>
|
||||||
|
*
|
||||||
|
* <p>A source record is identified from the strongest available provenance value. File-session
|
||||||
|
* support events use {@code rawRecordPath}; database events normally use {@code sourceRowId}.
|
||||||
|
* Generated external event IDs are only a fallback because they are presentation identifiers and
|
||||||
|
* are not guaranteed to be unique for every extractor section.</p>
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class RuntimeEventAggregationService {
|
||||||
|
|
||||||
|
private final RuntimeTachographEventSemantics tachographSemantics;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public RuntimeEventAggregationService(RuntimeTachographEventSemantics tachographSemantics) {
|
||||||
|
this.tachographSemantics = tachographSemantics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compatibility constructor used by unit tests and direct loader construction. */
|
||||||
|
public RuntimeEventAggregationService() {
|
||||||
|
this(new RuntimeTachographEventSemantics());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public final List<EventHubEventDto> aggregateRuntimeEvents(List<EventHubEventDto>... eventGroups) {
|
||||||
|
LinkedHashMap<String, EventHubEventDto> exactSourceRecords = new LinkedHashMap<>();
|
||||||
|
if (eventGroups != null) {
|
||||||
|
for (List<EventHubEventDto> eventGroup : eventGroups) {
|
||||||
|
appendExactSourceRecords(exactSourceRecords, eventGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exactSourceRecords.values().stream()
|
||||||
|
.sorted(eventComparator())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compatibility alias for the first implementation name. */
|
||||||
|
@SafeVarargs
|
||||||
|
public final List<EventHubEventDto> aggregateExactSourceRecords(List<EventHubEventDto>... eventGroups) {
|
||||||
|
return aggregateRuntimeEvents(eventGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String exactSourceRecordKey(EventHubEventDto event) {
|
||||||
|
if (event == null) {
|
||||||
|
return "NULL_EVENT";
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeEventSourceProfile profile = tachographSemantics.sourceProfile(event);
|
||||||
|
SourcePackageRefDto sourcePackage = event.sourcePackageRef();
|
||||||
|
JsonNode raw = RuntimeEntityReferenceResolver.rawPayload(event);
|
||||||
|
|
||||||
|
return String.join("|",
|
||||||
|
"SOURCE_RECORD",
|
||||||
|
nullToEmpty(event.packageInfo() == null ? null : event.packageInfo().tenantKey()),
|
||||||
|
nullToEmpty(profile.sourceSystem()),
|
||||||
|
nullToEmpty(profile.sourceKind()),
|
||||||
|
nullToEmpty(profile.extractionCode()),
|
||||||
|
nullToEmpty(sourcePackage == null ? null : sourcePackage.packageKind()),
|
||||||
|
nullToEmpty(sourcePackage == null ? null : sourcePackage.sourcePackageId()),
|
||||||
|
nullToEmpty(sourcePackage == null ? null : sourcePackage.sourceEntityId()),
|
||||||
|
sourceRecordIdentity(event, raw),
|
||||||
|
nullToEmpty(event.eventDomain() == null ? null : event.eventDomain().name()),
|
||||||
|
nullToEmpty(event.eventType() == null ? null : event.eventType().name()),
|
||||||
|
nullToEmpty(tachographSemantics.semanticLifecycle(event)),
|
||||||
|
event.occurredAt() == null ? "" : event.occurredAt().toInstant().toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceRecordIdentity(EventHubEventDto event, JsonNode raw) {
|
||||||
|
String rawRecordPath = text(raw, "rawRecordPath");
|
||||||
|
if (rawRecordPath != null) {
|
||||||
|
return "RAW_PATH:" + rawRecordPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String sourceRowId = text(raw, "sourceRowId");
|
||||||
|
if (sourceRowId != null) {
|
||||||
|
return "SOURCE_ROW:" + sourceRowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String supportEventId = text(raw, "supportEventId");
|
||||||
|
if (supportEventId != null) {
|
||||||
|
return "SUPPORT_EVENT:" + supportEventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.externalSourceEventId() != null && !event.externalSourceEventId().isBlank()) {
|
||||||
|
return "EXTERNAL:" + event.externalSourceEventId().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.eventId() != null) {
|
||||||
|
return "EVENT_ID:" + event.eventId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "CANONICAL:" + RuntimeEventIdentityResolver.canonicalEventKey(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendExactSourceRecords(
|
||||||
|
LinkedHashMap<String, EventHubEventDto> target,
|
||||||
|
List<EventHubEventDto> events
|
||||||
|
) {
|
||||||
|
if (events == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (EventHubEventDto event : events) {
|
||||||
|
if (event != null) {
|
||||||
|
target.putIfAbsent(exactSourceRecordKey(event), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<EventHubEventDto> eventComparator() {
|
||||||
|
return 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 -> tachographSemantics.semanticLifecycle(event), Comparator.nullsLast(String::compareTo))
|
||||||
|
.thenComparing(event -> tachographSemantics.sourceProfile(event).extractionCode(), Comparator.nullsLast(String::compareTo))
|
||||||
|
.thenComparing(this::stableSourceIdentityForSort)
|
||||||
|
.thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stableSourceIdentityForSort(EventHubEventDto event) {
|
||||||
|
return sourceRecordIdentity(event, RuntimeEntityReferenceResolver.rawPayload(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String field) {
|
||||||
|
return RuntimeEntityReferenceResolver.text(node, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nullToEmpty(Object value) {
|
||||||
|
return value == null ? "" : String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,11 +83,11 @@ public class UnifiedRuntimeDerivedProjectionService {
|
||||||
String explicitDriverKey
|
String explicitDriverKey
|
||||||
) {
|
) {
|
||||||
String driverKey = explicitDriverKey == null
|
String driverKey = explicitDriverKey == null
|
||||||
? resolveDriverKey(request, eventBundle.mergedEvents())
|
? resolveDriverKey(request, eventBundle.aggregatedEvents())
|
||||||
: explicitDriverKey;
|
: explicitDriverKey;
|
||||||
RuntimeSupportEvidenceNormalizationResult normalizationResult = supportEvidenceNormalizer.normalizeForDriverWorkingTime(
|
RuntimeSupportEvidenceNormalizationResult normalizationResult = supportEvidenceNormalizer.normalizeForDriverWorkingTime(
|
||||||
driverKey,
|
driverKey,
|
||||||
eventBundle.mergedEvents()
|
eventBundle.aggregatedEvents()
|
||||||
);
|
);
|
||||||
List<EventHubEventDto> normalizedEvents = normalizationResult.normalizedEvents();
|
List<EventHubEventDto> normalizedEvents = normalizationResult.normalizedEvents();
|
||||||
RuntimeDriverTimeline timeline = timelineReconstructor.reconstruct(
|
RuntimeDriverTimeline timeline = timelineReconstructor.reconstruct(
|
||||||
|
|
@ -169,7 +169,7 @@ public class UnifiedRuntimeDerivedProjectionService {
|
||||||
eventBundle.driverSeedEvents().size(),
|
eventBundle.driverSeedEvents().size(),
|
||||||
eventBundle.discoveredVehicles().size(),
|
eventBundle.discoveredVehicles().size(),
|
||||||
eventBundle.expandedVehicleEvents().size(),
|
eventBundle.expandedVehicleEvents().size(),
|
||||||
eventBundle.mergedEvents().size(),
|
eventBundle.aggregatedEventCount(),
|
||||||
eventBundle.discoveredVehicles(),
|
eventBundle.discoveredVehicles(),
|
||||||
projection,
|
projection,
|
||||||
notes,
|
notes,
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ public class UnifiedRuntimeDriverTimelineService {
|
||||||
UnifiedRuntimeEventBundle bundle = runtimeEventAssemblyService.assembleDriverScopedEvents(request);
|
UnifiedRuntimeEventBundle bundle = runtimeEventAssemblyService.assembleDriverScopedEvents(request);
|
||||||
return timelineReconstructor.reconstruct(
|
return timelineReconstructor.reconstruct(
|
||||||
null,
|
null,
|
||||||
resolveDriverKey(request, bundle.mergedEvents()),
|
resolveDriverKey(request, bundle.aggregatedEvents()),
|
||||||
bundle.mergedEvents()
|
bundle.aggregatedEvents()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,13 @@ import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeSourceInput;
|
import at.procon.eventhub.processing.model.UnifiedRuntimeSourceInput;
|
||||||
import at.procon.eventhub.processing.support.RuntimeEventIdentityResolver;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -25,13 +24,25 @@ public class UnifiedRuntimeEventAssemblyService {
|
||||||
|
|
||||||
private final List<RuntimeDriverEventLoader> driverEventLoaders;
|
private final List<RuntimeDriverEventLoader> driverEventLoaders;
|
||||||
private final List<RuntimeVehicleEventLoader> vehicleEventLoaders;
|
private final List<RuntimeVehicleEventLoader> vehicleEventLoaders;
|
||||||
|
private final RuntimeEventAggregationService eventAggregationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UnifiedRuntimeEventAssemblyService(
|
||||||
|
List<RuntimeDriverEventLoader> driverEventLoaders,
|
||||||
|
List<RuntimeVehicleEventLoader> vehicleEventLoaders,
|
||||||
|
RuntimeEventAggregationService eventAggregationService
|
||||||
|
) {
|
||||||
|
this.driverEventLoaders = List.copyOf(driverEventLoaders);
|
||||||
|
this.vehicleEventLoaders = List.copyOf(vehicleEventLoaders);
|
||||||
|
this.eventAggregationService = eventAggregationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compatibility constructor retained for existing tests and direct construction. */
|
||||||
public UnifiedRuntimeEventAssemblyService(
|
public UnifiedRuntimeEventAssemblyService(
|
||||||
List<RuntimeDriverEventLoader> driverEventLoaders,
|
List<RuntimeDriverEventLoader> driverEventLoaders,
|
||||||
List<RuntimeVehicleEventLoader> vehicleEventLoaders
|
List<RuntimeVehicleEventLoader> vehicleEventLoaders
|
||||||
) {
|
) {
|
||||||
this.driverEventLoaders = List.copyOf(driverEventLoaders);
|
this(driverEventLoaders, vehicleEventLoaders, new RuntimeEventAggregationService());
|
||||||
this.vehicleEventLoaders = List.copyOf(vehicleEventLoaders);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnifiedRuntimeEventBundle assembleDriverScopedEvents(UnifiedRuntimeProcessingRequest request) {
|
public UnifiedRuntimeEventBundle assembleDriverScopedEvents(UnifiedRuntimeProcessingRequest request) {
|
||||||
|
|
@ -42,8 +53,8 @@ public class UnifiedRuntimeEventAssemblyService {
|
||||||
List<EventHubEventDto> expandedVehicleEvents = expandVehicleEvents
|
List<EventHubEventDto> expandedVehicleEvents = expandVehicleEvents
|
||||||
? loadExpandedVehicleEvents(request, discoveredVehicles)
|
? loadExpandedVehicleEvents(request, discoveredVehicles)
|
||||||
: List.of();
|
: List.of();
|
||||||
List<EventHubEventDto> mergedEvents = expandVehicleEvents
|
List<EventHubEventDto> aggregatedEvents = expandVehicleEvents
|
||||||
? deduplicateAndSort(driverSeedEvents, expandedVehicleEvents)
|
? eventAggregationService.aggregateRuntimeEvents(driverSeedEvents, expandedVehicleEvents)
|
||||||
: driverSeedEvents;
|
: driverSeedEvents;
|
||||||
|
|
||||||
List<String> notes = new ArrayList<>();
|
List<String> notes = new ArrayList<>();
|
||||||
|
|
@ -71,26 +82,28 @@ public class UnifiedRuntimeEventAssemblyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (expandVehicleEvents) {
|
if (expandVehicleEvents) {
|
||||||
notes.add("Vehicle expansion loaded additional events for vehicles discovered in the driver seed set.");
|
notes.add("Vehicle expansion aggregated additional events for vehicles discovered in the driver seed set.");
|
||||||
notes.add("Vehicle expansion padding minutes: " + request.vehicleExpansionPaddingMinutes() + ".");
|
notes.add("Vehicle expansion padding minutes: " + request.vehicleExpansionPaddingMinutes() + ".");
|
||||||
} else {
|
} else {
|
||||||
notes.add("Vehicle expansion was disabled for this runtime request.");
|
notes.add("Vehicle expansion was disabled for this runtime request.");
|
||||||
}
|
}
|
||||||
|
notes.add("Runtime aggregation removes repeated reads and duplicate serialized representations of the same extraction observation while preserving card/VU equivalents and CARD_VEHICLES_USED/IW_CYCLE evidence for later processing.");
|
||||||
|
notes.add("The assembled event set is a broad aggregated runtime scope; semantic card/VU mixing and interval reconciliation are performed by later modules.");
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Runtime event assembly completed (expandVehicleEvents: {}, sourceInputs: {}, driverSeedEvents: {}, discoveredVehicles: {}, expandedVehicleEvents: {}, mergedEvents: {})",
|
"Runtime event assembly completed (expandVehicleEvents: {}, sourceInputs: {}, driverSeedEvents: {}, discoveredVehicles: {}, expandedVehicleEvents: {}, aggregatedEvents: {})",
|
||||||
expandVehicleEvents,
|
expandVehicleEvents,
|
||||||
sourceInputs.size(),
|
sourceInputs.size(),
|
||||||
driverSeedEvents.size(),
|
driverSeedEvents.size(),
|
||||||
discoveredVehicles.size(),
|
discoveredVehicles.size(),
|
||||||
expandedVehicleEvents.size(),
|
expandedVehicleEvents.size(),
|
||||||
mergedEvents.size()
|
aggregatedEvents.size()
|
||||||
);
|
);
|
||||||
return new UnifiedRuntimeEventBundle(
|
return new UnifiedRuntimeEventBundle(
|
||||||
request,
|
request,
|
||||||
driverSeedEvents,
|
driverSeedEvents,
|
||||||
discoveredVehicles,
|
discoveredVehicles,
|
||||||
expandedVehicleEvents,
|
expandedVehicleEvents,
|
||||||
mergedEvents,
|
aggregatedEvents,
|
||||||
notes
|
notes
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +119,7 @@ public class UnifiedRuntimeEventAssemblyService {
|
||||||
result.addAll(loader.loadDriverEvents(sourceRequest));
|
result.addAll(loader.loadDriverEvents(sourceRequest));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deduplicateAndSort(result, List.of());
|
return eventAggregationService.aggregateRuntimeEvents(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EventHubEventDto> loadExpandedVehicleEvents(
|
private List<EventHubEventDto> loadExpandedVehicleEvents(
|
||||||
|
|
@ -125,7 +138,7 @@ public class UnifiedRuntimeEventAssemblyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deduplicateAndSort(result, List.of());
|
return eventAggregationService.aggregateRuntimeEvents(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
|
private List<UnifiedDiscoveredVehicleRef> discoverVehicles(List<EventHubEventDto> events) {
|
||||||
|
|
@ -165,28 +178,6 @@ public class UnifiedRuntimeEventAssemblyService {
|
||||||
return List.copyOf(result);
|
return List.copyOf(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EventHubEventDto> deduplicateAndSort(
|
|
||||||
List<EventHubEventDto> left,
|
|
||||||
List<EventHubEventDto> right
|
|
||||||
) {
|
|
||||||
LinkedHashMap<String, EventHubEventDto> byKey = new LinkedHashMap<>();
|
|
||||||
appendDeduplicated(byKey, left);
|
|
||||||
appendDeduplicated(byKey, right);
|
|
||||||
return byKey.values().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 void appendDeduplicated(LinkedHashMap<String, EventHubEventDto> byKey, List<EventHubEventDto> events) {
|
|
||||||
for (EventHubEventDto event : events) {
|
|
||||||
byKey.putIfAbsent(RuntimeEventIdentityResolver.canonicalEventKey(event), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private RuntimeDriverEventLoader driverLoader(UnifiedRuntimeProcessingRequest request, UnifiedEventSourceFamily sourceFamily) {
|
private RuntimeDriverEventLoader driverLoader(UnifiedRuntimeProcessingRequest request, UnifiedEventSourceFamily sourceFamily) {
|
||||||
return driverEventLoaders.stream()
|
return driverEventLoaders.stream()
|
||||||
.filter(loader -> loader.supports(request, sourceFamily))
|
.filter(loader -> loader.supports(request, sourceFamily))
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend;
|
||||||
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest;
|
||||||
import at.procon.eventhub.processing.service.RuntimeDriverEventLoader;
|
import at.procon.eventhub.processing.service.RuntimeDriverEventLoader;
|
||||||
|
import at.procon.eventhub.processing.service.RuntimeEventAggregationService;
|
||||||
import at.procon.eventhub.processing.service.RuntimeVehicleEventLoader;
|
import at.procon.eventhub.processing.service.RuntimeVehicleEventLoader;
|
||||||
import at.procon.eventhub.reference.TachographNationRegistry;
|
import at.procon.eventhub.reference.TachographNationRegistry;
|
||||||
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||||
|
|
@ -30,6 +31,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
@ -45,15 +47,28 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader,
|
||||||
private final NamedParameterJdbcTemplate jdbcTemplate;
|
private final NamedParameterJdbcTemplate jdbcTemplate;
|
||||||
private final TachographExtractionDefinitionRegistry definitionRegistry;
|
private final TachographExtractionDefinitionRegistry definitionRegistry;
|
||||||
private final ResourceLoader resourceLoader;
|
private final ResourceLoader resourceLoader;
|
||||||
|
private final RuntimeEventAggregationService eventAggregationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
public TachographDbRuntimeEventLoader(
|
public TachographDbRuntimeEventLoader(
|
||||||
@Qualifier("tachographNamedParameterJdbcTemplate") NamedParameterJdbcTemplate jdbcTemplate,
|
@Qualifier("tachographNamedParameterJdbcTemplate") NamedParameterJdbcTemplate jdbcTemplate,
|
||||||
TachographExtractionDefinitionRegistry definitionRegistry,
|
TachographExtractionDefinitionRegistry definitionRegistry,
|
||||||
ResourceLoader resourceLoader
|
ResourceLoader resourceLoader,
|
||||||
|
RuntimeEventAggregationService eventAggregationService
|
||||||
) {
|
) {
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.definitionRegistry = definitionRegistry;
|
this.definitionRegistry = definitionRegistry;
|
||||||
this.resourceLoader = resourceLoader;
|
this.resourceLoader = resourceLoader;
|
||||||
|
this.eventAggregationService = eventAggregationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Compatibility constructor retained for direct construction. */
|
||||||
|
public TachographDbRuntimeEventLoader(
|
||||||
|
NamedParameterJdbcTemplate jdbcTemplate,
|
||||||
|
TachographExtractionDefinitionRegistry definitionRegistry,
|
||||||
|
ResourceLoader resourceLoader
|
||||||
|
) {
|
||||||
|
this(jdbcTemplate, definitionRegistry, resourceLoader, new RuntimeEventAggregationService());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -74,7 +89,7 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader,
|
||||||
request.occurredTo()
|
request.occurredTo()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return List.copyOf(result);
|
return eventAggregationService.aggregateRuntimeEvents(result);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +108,7 @@ public class TachographDbRuntimeEventLoader implements RuntimeDriverEventLoader,
|
||||||
request.vehicleOccurredTo()
|
request.vehicleOccurredTo()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return List.copyOf(result);
|
return eventAggregationService.aggregateRuntimeEvents(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EventHubEventDto> queryDefinition(
|
private List<EventHubEventDto> queryDefinition(
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,15 @@ import at.procon.eventhub.processing.service.RuntimeDriverEventLoader;
|
||||||
import at.procon.eventhub.processing.service.RuntimeVehicleEventLoader;
|
import at.procon.eventhub.processing.service.RuntimeVehicleEventLoader;
|
||||||
import at.procon.eventhub.processing.service.UnifiedDriverEventSourceService;
|
import at.procon.eventhub.processing.service.UnifiedDriverEventSourceService;
|
||||||
import at.procon.eventhub.processing.service.UnifiedVehicleEventSourceService;
|
import at.procon.eventhub.processing.service.UnifiedVehicleEventSourceService;
|
||||||
|
import at.procon.eventhub.processing.service.RuntimeEventAggregationService;
|
||||||
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
||||||
import at.procon.eventhub.service.EventHubEventSorter;
|
import at.procon.eventhub.service.EventHubEventSorter;
|
||||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionNotFoundException;
|
||||||
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
|
import at.procon.eventhub.tachographfilesession.service.TachographCompositeSessionRepository;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|
@ -27,21 +28,35 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
||||||
private final UnifiedDriverEventSourceService driverEventSourceService;
|
private final UnifiedDriverEventSourceService driverEventSourceService;
|
||||||
private final UnifiedVehicleEventSourceService vehicleEventSourceService;
|
private final UnifiedVehicleEventSourceService vehicleEventSourceService;
|
||||||
private final TachographCompositeSessionRepository compositeSessionRepository;
|
private final TachographCompositeSessionRepository compositeSessionRepository;
|
||||||
private final EventAcquisitionRecordKeyService eventKeyService;
|
private final RuntimeEventAggregationService eventAggregationService;
|
||||||
private final EventHubEventSorter eventSorter;
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
public TachographFileSessionRuntimeEventLoader(
|
public TachographFileSessionRuntimeEventLoader(
|
||||||
UnifiedDriverEventSourceService driverEventSourceService,
|
UnifiedDriverEventSourceService driverEventSourceService,
|
||||||
UnifiedVehicleEventSourceService vehicleEventSourceService,
|
UnifiedVehicleEventSourceService vehicleEventSourceService,
|
||||||
TachographCompositeSessionRepository compositeSessionRepository,
|
TachographCompositeSessionRepository compositeSessionRepository,
|
||||||
EventAcquisitionRecordKeyService eventKeyService,
|
RuntimeEventAggregationService eventAggregationService
|
||||||
EventHubEventSorter eventSorter
|
|
||||||
) {
|
) {
|
||||||
this.driverEventSourceService = driverEventSourceService;
|
this.driverEventSourceService = driverEventSourceService;
|
||||||
this.vehicleEventSourceService = vehicleEventSourceService;
|
this.vehicleEventSourceService = vehicleEventSourceService;
|
||||||
this.compositeSessionRepository = compositeSessionRepository;
|
this.compositeSessionRepository = compositeSessionRepository;
|
||||||
this.eventKeyService = eventKeyService;
|
this.eventAggregationService = eventAggregationService;
|
||||||
this.eventSorter = eventSorter;
|
}
|
||||||
|
|
||||||
|
/** Compatibility constructor retained for existing tests and direct construction. */
|
||||||
|
public TachographFileSessionRuntimeEventLoader(
|
||||||
|
UnifiedDriverEventSourceService driverEventSourceService,
|
||||||
|
UnifiedVehicleEventSourceService vehicleEventSourceService,
|
||||||
|
TachographCompositeSessionRepository compositeSessionRepository,
|
||||||
|
EventAcquisitionRecordKeyService ignoredEventKeyService,
|
||||||
|
EventHubEventSorter ignoredEventSorter
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
driverEventSourceService,
|
||||||
|
vehicleEventSourceService,
|
||||||
|
compositeSessionRepository,
|
||||||
|
new RuntimeEventAggregationService()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -64,7 +79,7 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return deduplicateBySignatureAndSort(result);
|
return eventAggregationService.aggregateRuntimeEvents(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -87,7 +102,7 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return deduplicateBySignatureAndSort(result);
|
return eventAggregationService.aggregateRuntimeEvents(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UUID> resolveSessionIds(UnifiedRuntimeProcessingRequest request) {
|
private List<UUID> resolveSessionIds(UnifiedRuntimeProcessingRequest request) {
|
||||||
|
|
@ -99,11 +114,5 @@ public class TachographFileSessionRuntimeEventLoader implements RuntimeDriverEve
|
||||||
return request.sessionIds();
|
return request.sessionIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<EventHubEventDto> deduplicateBySignatureAndSort(List<EventHubEventDto> events) {
|
|
||||||
LinkedHashMap<String, EventHubEventDto> bySignature = new LinkedHashMap<>();
|
|
||||||
for (EventHubEventDto event : events) {
|
|
||||||
bySignature.putIfAbsent(eventKeyService.buildEventSignatureHash(event), event);
|
|
||||||
}
|
|
||||||
return eventSorter.sort(new ArrayList<>(bySignature.values()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,13 @@ create schema DailyWeeklyRestCandidateCoverageEmittedKey(
|
||||||
endedAtEpochSecond long
|
endedAtEpochSecond long
|
||||||
);
|
);
|
||||||
|
|
||||||
@public create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent;
|
@public create context PerDriver partition by driverKey from DriverWorkingTimeVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
|
@public create window DriverWorkingTimeVehicleUsageIntervalInputWindow#keepall as DriverWorkingTimeVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
|
insert into DriverWorkingTimeVehicleUsageIntervalInputWindow
|
||||||
|
select *
|
||||||
|
from DriverWorkingTimeVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
create schema VuCardAbsentInterval(
|
create schema VuCardAbsentInterval(
|
||||||
sessionId java.util.UUID,
|
sessionId java.util.UUID,
|
||||||
|
|
@ -304,6 +310,8 @@ create schema VuCardAbsentInterval(
|
||||||
nextVehicleKey string
|
nextVehicleKey string
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@public create window VuCardAbsentIntervalWindow#keepall as VuCardAbsentInterval;
|
||||||
|
|
||||||
create schema PotentialHomeOvernightStayInterval(
|
create schema PotentialHomeOvernightStayInterval(
|
||||||
sessionId java.util.UUID,
|
sessionId java.util.UUID,
|
||||||
driverKey string,
|
driverKey string,
|
||||||
|
|
@ -473,7 +481,7 @@ select
|
||||||
longitude,
|
longitude,
|
||||||
odometerKm,
|
odometerKm,
|
||||||
priority
|
priority
|
||||||
from TachographSupportGeoEvidenceInputEvent;
|
from DriverWorkingTimeSupportEvidenceInputEvent;
|
||||||
|
|
||||||
insert into SignificantDrivingInterval
|
insert into SignificantDrivingInterval
|
||||||
select
|
select
|
||||||
|
|
@ -486,7 +494,7 @@ select
|
||||||
durationSeconds,
|
durationSeconds,
|
||||||
registrationKey,
|
registrationKey,
|
||||||
vehicleKey
|
vehicleKey
|
||||||
from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS});
|
from DriverWorkingTimeActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS});
|
||||||
|
|
||||||
@public create window PreviousSignificantDrivingInterval#unique(driverKey) as SignificantDrivingInterval;
|
@public create window PreviousSignificantDrivingInterval#unique(driverKey) as SignificantDrivingInterval;
|
||||||
|
|
||||||
|
|
@ -562,7 +570,7 @@ select
|
||||||
c.previousVehicleKey as previousVehicleKey,
|
c.previousVehicleKey as previousVehicleKey,
|
||||||
c.nextVehicleKey as nextVehicleKey
|
c.nextVehicleKey as nextVehicleKey
|
||||||
from DailyWeeklyRestCandidateInterval as c unidirectional,
|
from DailyWeeklyRestCandidateInterval as c unidirectional,
|
||||||
VuCardAbsentInterval#keepall as u
|
VuCardAbsentIntervalWindow as u
|
||||||
where u.driverKey = c.driverKey
|
where u.driverKey = c.driverKey
|
||||||
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
||||||
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
||||||
|
|
@ -595,7 +603,7 @@ select
|
||||||
c.nextVehicleKey as nextVehicleKey
|
c.nextVehicleKey as nextVehicleKey
|
||||||
from DailyWeeklyRestCandidateInterval as c
|
from DailyWeeklyRestCandidateInterval as c
|
||||||
where not exists (
|
where not exists (
|
||||||
select * from VuCardAbsentInterval#keepall as u
|
select * from VuCardAbsentIntervalWindow as u
|
||||||
where u.driverKey = c.driverKey
|
where u.driverKey = c.driverKey
|
||||||
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
||||||
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
||||||
|
|
@ -649,7 +657,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
DriverWorkingTimeVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.odometerBeginKm is not null
|
and v.odometerBeginKm is not null
|
||||||
and v.startedAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
and v.startedAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
||||||
|
|
@ -696,7 +704,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
DriverWorkingTimeVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.endedAtEpochSecond is not null
|
and v.endedAtEpochSecond is not null
|
||||||
and v.odometerEndKm is not null
|
and v.odometerEndKm is not null
|
||||||
|
|
@ -772,7 +780,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
DriverWorkingTimeVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.odometerBeginKm is not null
|
and v.odometerBeginKm is not null
|
||||||
and v.startedAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
and v.startedAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
||||||
|
|
@ -819,7 +827,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
DriverWorkingTimeVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.endedAtEpochSecond is not null
|
and v.endedAtEpochSecond is not null
|
||||||
and v.odometerEndKm is not null
|
and v.odometerEndKm is not null
|
||||||
|
|
@ -1484,11 +1492,11 @@ select
|
||||||
from DailyWeeklyRestCandidateCoverageInterval;
|
from DailyWeeklyRestCandidateCoverageInterval;
|
||||||
|
|
||||||
@public context PerDriver
|
@public context PerDriver
|
||||||
create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent;
|
create window PreviousVehicleUsageInterval#lastevent as DriverWorkingTimeVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
@Priority(30)
|
@Priority(30)
|
||||||
context PerDriver
|
context PerDriver
|
||||||
on TachographVehicleUsageIntervalInputEvent as next
|
on DriverWorkingTimeVehicleUsageIntervalInputEvent as next
|
||||||
insert into VuCardAbsentInterval
|
insert into VuCardAbsentInterval
|
||||||
select
|
select
|
||||||
priorInterval.sessionId as sessionId,
|
priorInterval.sessionId as sessionId,
|
||||||
|
|
@ -1509,12 +1517,12 @@ where priorInterval.endedAt is not null
|
||||||
|
|
||||||
@Priority(20)
|
@Priority(20)
|
||||||
context PerDriver
|
context PerDriver
|
||||||
on TachographVehicleUsageIntervalInputEvent
|
on DriverWorkingTimeVehicleUsageIntervalInputEvent
|
||||||
delete from PreviousVehicleUsageInterval;
|
delete from PreviousVehicleUsageInterval;
|
||||||
|
|
||||||
@Priority(10)
|
@Priority(10)
|
||||||
context PerDriver
|
context PerDriver
|
||||||
on TachographVehicleUsageIntervalInputEvent as current
|
on DriverWorkingTimeVehicleUsageIntervalInputEvent as current
|
||||||
insert into PreviousVehicleUsageInterval
|
insert into PreviousVehicleUsageInterval
|
||||||
select *;
|
select *;
|
||||||
|
|
||||||
|
|
@ -1776,6 +1784,9 @@ select * from DailyWeeklyRestCandidateCoverageInterval;
|
||||||
@name('drivingInterruptionVehicleChangeIntervals')
|
@name('drivingInterruptionVehicleChangeIntervals')
|
||||||
select * from DrivingInterruptionVehicleChangeInterval;
|
select * from DrivingInterruptionVehicleChangeInterval;
|
||||||
|
|
||||||
|
insert into VuCardAbsentIntervalWindow
|
||||||
|
select * from VuCardAbsentInterval;
|
||||||
|
|
||||||
@name('vuCardAbsentIntervals')
|
@name('vuCardAbsentIntervals')
|
||||||
select * from VuCardAbsentInterval;
|
select * from VuCardAbsentInterval;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Source-neutral event-input adapter for driver-working-time-derived-projections.epl.
|
* 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
|
* The projection bundle consumes resolved interval input streams. This preprocessor lets the
|
||||||
* derived rules consume EventHub point events by pairing START/END activity events and
|
* same derived rules consume EventHub point events by pairing START/END activity events and
|
||||||
* INSERT/WITHDRAW card-vehicle usage events inside Esper.
|
* INSERT/WITHDRAW card-vehicle usage events inside Esper.
|
||||||
*
|
*
|
||||||
* Vehicle-usage intervals are additionally coalesced in EPL before they are forwarded to
|
* Vehicle-usage intervals are additionally coalesced in EPL before they are forwarded to
|
||||||
|
|
@ -11,15 +11,15 @@
|
||||||
* common midnight continuation 23:59:59 -> 00:00:00 on the next day.
|
* common midnight continuation 23:59:59 -> 00:00:00 on the next day.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
create window OpenActivityPoint#unique(driverKey, intervalId) as TachographActivityPointInputEvent;
|
create window OpenActivityPoint#unique(driverKey, intervalId) as DriverWorkingTimeActivityPointInputEvent;
|
||||||
|
|
||||||
insert into OpenActivityPoint
|
insert into OpenActivityPoint
|
||||||
select *
|
select *
|
||||||
from TachographActivityPointInputEvent(lifecycle = 'START');
|
from DriverWorkingTimeActivityPointInputEvent(lifecycle = 'START');
|
||||||
|
|
||||||
@Priority(20)
|
@Priority(20)
|
||||||
on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent
|
on DriverWorkingTimeActivityPointInputEvent(lifecycle = 'END') as endEvent
|
||||||
insert into TachographActivityIntervalInputEvent
|
insert into DriverWorkingTimeActivityIntervalInputEvent
|
||||||
select
|
select
|
||||||
startEvent.sessionId as sessionId,
|
startEvent.sessionId as sessionId,
|
||||||
startEvent.driverKey as driverKey,
|
startEvent.driverKey as driverKey,
|
||||||
|
|
@ -48,7 +48,7 @@ where startEvent.driverKey = endEvent.driverKey
|
||||||
and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond;
|
and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond;
|
||||||
|
|
||||||
@Priority(10)
|
@Priority(10)
|
||||||
on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent
|
on DriverWorkingTimeActivityPointInputEvent(lifecycle = 'END') as endEvent
|
||||||
delete from OpenActivityPoint as openEvent
|
delete from OpenActivityPoint as openEvent
|
||||||
where openEvent.driverKey = endEvent.driverKey
|
where openEvent.driverKey = endEvent.driverKey
|
||||||
and openEvent.intervalId = endEvent.intervalId;
|
and openEvent.intervalId = endEvent.intervalId;
|
||||||
|
|
@ -72,14 +72,14 @@ create schema RawVehicleUsageInterval(
|
||||||
sourceIntervalIds java.util.List
|
sourceIntervalIds java.util.List
|
||||||
);
|
);
|
||||||
|
|
||||||
create window OpenVehicleUsagePoint#unique(driverKey, intervalId) as TachographVehicleUsagePointInputEvent;
|
create window OpenVehicleUsagePoint#unique(driverKey, intervalId) as DriverWorkingTimeVehicleUsagePointInputEvent;
|
||||||
|
|
||||||
insert into OpenVehicleUsagePoint
|
insert into OpenVehicleUsagePoint
|
||||||
select *
|
select *
|
||||||
from TachographVehicleUsagePointInputEvent(lifecycle = 'INSERT');
|
from DriverWorkingTimeVehicleUsagePointInputEvent(lifecycle = 'INSERT');
|
||||||
|
|
||||||
@Priority(20)
|
@Priority(20)
|
||||||
on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
on DriverWorkingTimeVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
||||||
insert into RawVehicleUsageInterval
|
insert into RawVehicleUsageInterval
|
||||||
select
|
select
|
||||||
insertEvent.sessionId as sessionId,
|
insertEvent.sessionId as sessionId,
|
||||||
|
|
@ -104,7 +104,7 @@ where insertEvent.driverKey = withdrawEvent.driverKey
|
||||||
and withdrawEvent.occurredAtEpochSecond > insertEvent.occurredAtEpochSecond;
|
and withdrawEvent.occurredAtEpochSecond > insertEvent.occurredAtEpochSecond;
|
||||||
|
|
||||||
@Priority(10)
|
@Priority(10)
|
||||||
on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
on DriverWorkingTimeVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent
|
||||||
delete from OpenVehicleUsagePoint as openEvent
|
delete from OpenVehicleUsagePoint as openEvent
|
||||||
where openEvent.driverKey = withdrawEvent.driverKey
|
where openEvent.driverKey = withdrawEvent.driverKey
|
||||||
and openEvent.intervalId = withdrawEvent.intervalId;
|
and openEvent.intervalId = withdrawEvent.intervalId;
|
||||||
|
|
@ -192,7 +192,7 @@ where current.driverKey = next.driverKey
|
||||||
*/
|
*/
|
||||||
@Priority(70)
|
@Priority(70)
|
||||||
on RawVehicleUsageInterval as next
|
on RawVehicleUsageInterval as next
|
||||||
insert into TachographVehicleUsageIntervalInputEvent
|
insert into DriverWorkingTimeVehicleUsageIntervalInputEvent
|
||||||
select
|
select
|
||||||
current.sessionId as sessionId,
|
current.sessionId as sessionId,
|
||||||
current.driverKey as driverKey,
|
current.driverKey as driverKey,
|
||||||
|
|
@ -316,12 +316,12 @@ where candidate.driverKey = next.driverKey;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The last accumulated interval cannot be emitted by looking at the next interval, so Java
|
* 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
|
* sends one DriverWorkingTimeProjectionFinalizeEvent per driver after all vehicle-usage point
|
||||||
* events and before activity point events.
|
* events and before activity point events.
|
||||||
*/
|
*/
|
||||||
@Priority(20)
|
@Priority(20)
|
||||||
on TachographProjectionFinalizeEvent as finalizeEvent
|
on DriverWorkingTimeProjectionFinalizeEvent as finalizeEvent
|
||||||
insert into TachographVehicleUsageIntervalInputEvent
|
insert into DriverWorkingTimeVehicleUsageIntervalInputEvent
|
||||||
select
|
select
|
||||||
current.sessionId as sessionId,
|
current.sessionId as sessionId,
|
||||||
current.driverKey as driverKey,
|
current.driverKey as driverKey,
|
||||||
|
|
@ -343,6 +343,6 @@ from MergedVehicleUsageAccumulator as current
|
||||||
where current.driverKey = finalizeEvent.driverKey;
|
where current.driverKey = finalizeEvent.driverKey;
|
||||||
|
|
||||||
@Priority(10)
|
@Priority(10)
|
||||||
on TachographProjectionFinalizeEvent as finalizeEvent
|
on DriverWorkingTimeProjectionFinalizeEvent as finalizeEvent
|
||||||
delete from MergedVehicleUsageAccumulator as current
|
delete from MergedVehicleUsageAccumulator as current
|
||||||
where current.driverKey = finalizeEvent.driverKey;
|
where current.driverKey = finalizeEvent.driverKey;
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,12 @@ create schema DailyWeeklyRestCandidateCoverageEmittedKey(
|
||||||
|
|
||||||
create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent;
|
create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
|
@public create window TachographVehicleUsageIntervalInputWindow#keepall as TachographVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
|
insert into TachographVehicleUsageIntervalInputWindow
|
||||||
|
select *
|
||||||
|
from TachographVehicleUsageIntervalInputEvent;
|
||||||
|
|
||||||
create schema VuCardAbsentInterval(
|
create schema VuCardAbsentInterval(
|
||||||
sessionId java.util.UUID,
|
sessionId java.util.UUID,
|
||||||
driverKey string,
|
driverKey string,
|
||||||
|
|
@ -296,6 +302,8 @@ create schema VuCardAbsentInterval(
|
||||||
nextVehicleKey string
|
nextVehicleKey string
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@public create window VuCardAbsentIntervalWindow#keepall as VuCardAbsentInterval;
|
||||||
|
|
||||||
create schema PotentialHomeOvernightStayInterval(
|
create schema PotentialHomeOvernightStayInterval(
|
||||||
sessionId java.util.UUID,
|
sessionId java.util.UUID,
|
||||||
driverKey string,
|
driverKey string,
|
||||||
|
|
@ -554,7 +562,7 @@ select
|
||||||
c.previousVehicleKey as previousVehicleKey,
|
c.previousVehicleKey as previousVehicleKey,
|
||||||
c.nextVehicleKey as nextVehicleKey
|
c.nextVehicleKey as nextVehicleKey
|
||||||
from DailyWeeklyRestCandidateInterval as c unidirectional,
|
from DailyWeeklyRestCandidateInterval as c unidirectional,
|
||||||
VuCardAbsentInterval#keepall as u
|
VuCardAbsentIntervalWindow as u
|
||||||
where u.driverKey = c.driverKey
|
where u.driverKey = c.driverKey
|
||||||
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
||||||
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
||||||
|
|
@ -587,7 +595,7 @@ select
|
||||||
c.nextVehicleKey as nextVehicleKey
|
c.nextVehicleKey as nextVehicleKey
|
||||||
from DailyWeeklyRestCandidateInterval as c
|
from DailyWeeklyRestCandidateInterval as c
|
||||||
where not exists (
|
where not exists (
|
||||||
select * from VuCardAbsentInterval#keepall as u
|
select * from VuCardAbsentIntervalWindow as u
|
||||||
where u.driverKey = c.driverKey
|
where u.driverKey = c.driverKey
|
||||||
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
and u.startedAtEpochSecond < c.endedAtEpochSecond
|
||||||
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
and u.endedAtEpochSecond > c.startedAtEpochSecond
|
||||||
|
|
@ -641,7 +649,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
TachographVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.odometerBeginKm is not null
|
and v.odometerBeginKm is not null
|
||||||
and v.startedAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
and v.startedAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
||||||
|
|
@ -688,7 +696,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
TachographVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.endedAtEpochSecond is not null
|
and v.endedAtEpochSecond is not null
|
||||||
and v.odometerEndKm is not null
|
and v.odometerEndKm is not null
|
||||||
|
|
@ -764,7 +772,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
TachographVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.odometerBeginKm is not null
|
and v.odometerBeginKm is not null
|
||||||
and v.startedAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
and v.startedAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS}
|
||||||
|
|
@ -811,7 +819,7 @@ select
|
||||||
end
|
end
|
||||||
) as rankScore
|
) as rankScore
|
||||||
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional,
|
||||||
TachographVehicleUsageIntervalInputEvent#keepall as v
|
TachographVehicleUsageIntervalInputWindow as v
|
||||||
where v.driverKey = c.driverKey
|
where v.driverKey = c.driverKey
|
||||||
and v.endedAtEpochSecond is not null
|
and v.endedAtEpochSecond is not null
|
||||||
and v.odometerEndKm is not null
|
and v.odometerEndKm is not null
|
||||||
|
|
@ -1768,6 +1776,9 @@ select * from DailyWeeklyRestCandidateCoverageInterval;
|
||||||
@name('drivingInterruptionVehicleChangeIntervals')
|
@name('drivingInterruptionVehicleChangeIntervals')
|
||||||
select * from DrivingInterruptionVehicleChangeInterval;
|
select * from DrivingInterruptionVehicleChangeInterval;
|
||||||
|
|
||||||
|
insert into VuCardAbsentIntervalWindow
|
||||||
|
select * from VuCardAbsentInterval;
|
||||||
|
|
||||||
@name('vuCardAbsentIntervals')
|
@name('vuCardAbsentIntervals')
|
||||||
select * from VuCardAbsentInterval;
|
select * from VuCardAbsentInterval;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ select
|
||||||
base.country,
|
base.country,
|
||||||
base.region,
|
base.region,
|
||||||
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
||||||
case when base.entry_type in (0, 2, 4) then 'START' else 'END' end as lifecycle,
|
case when base.entry_type in (0, 2, 4, 6) then 'START' else 'END' end as lifecycle,
|
||||||
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
||||||
|
|
||||||
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ select
|
||||||
base.country,
|
base.country,
|
||||||
base.region,
|
base.region,
|
||||||
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
||||||
case when base.entry_type in (0, 2, 4) then 'START' else 'END' end as lifecycle,
|
case when base.entry_type in (0, 2, 4, 6) then 'START' else 'END' end as lifecycle,
|
||||||
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
||||||
|
|
||||||
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,62 @@ package at.procon.eventhub.processing.driverworkingtime.service;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
import at.procon.eventhub.config.EventHubProperties;
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.processing.driverworkingtime.esper.DriverWorkingTimeEsperContractNames;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeActivityInterval;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeDerivedProjectionBundle;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeProcessingInput;
|
||||||
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
|
import at.procon.eventhub.processing.driverworkingtime.model.DriverWorkingTimeVehicleUsageInterval;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
class DriverWorkingTimeReusableProjectionBuilderTest {
|
class DriverWorkingTimeReusableProjectionBuilderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void commonEplUsesSourceNeutralDriverWorkingTimeInputContracts() throws IOException {
|
||||||
|
String projectionEpl = StreamUtils.copyToString(
|
||||||
|
new ClassPathResource("esper/driver-working-time-derived-projections.epl").getInputStream(),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
String preprocessorEpl = StreamUtils.copyToString(
|
||||||
|
new ClassPathResource("esper/runtime-driver-event-interval-preprocessor.epl").getInputStream(),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(projectionEpl)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.ACTIVITY_INTERVAL_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.SUPPORT_EVIDENCE_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_WINDOW)
|
||||||
|
.doesNotContain("TachographActivityIntervalInputEvent")
|
||||||
|
.doesNotContain("TachographVehicleUsageIntervalInputEvent")
|
||||||
|
.doesNotContain("TachographSupportGeoEvidenceInputEvent")
|
||||||
|
.doesNotContain("TachographVehicleUsageIntervalInputWindow");
|
||||||
|
|
||||||
|
assertThat(preprocessorEpl)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.ACTIVITY_POINT_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_POINT_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.ACTIVITY_INTERVAL_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_EVENT_TYPE)
|
||||||
|
.contains(DriverWorkingTimeEsperContractNames.PROJECTION_FINALIZE_EVENT_TYPE)
|
||||||
|
.doesNotContain("TachographActivityPointInputEvent")
|
||||||
|
.doesNotContain("TachographVehicleUsagePointInputEvent")
|
||||||
|
.doesNotContain("TachographProjectionFinalizeEvent");
|
||||||
|
|
||||||
|
assertThat(DriverWorkingTimeReusableProjectionBuilder.REUSABLE_RUNTIME_STATE_CLEANUP_QUERIES)
|
||||||
|
.contains("delete from "
|
||||||
|
+ DriverWorkingTimeEsperContractNames.VEHICLE_USAGE_INTERVAL_INPUT_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void reusesWarmRuntimeWithoutLeakingPreviousState() {
|
void reusesWarmRuntimeWithoutLeakingPreviousState() {
|
||||||
DriverWorkingTimeReusableProjectionBuilder builder =
|
DriverWorkingTimeReusableProjectionBuilder builder =
|
||||||
|
|
@ -112,4 +157,270 @@ class DriverWorkingTimeReusableProjectionBuilderTest {
|
||||||
assertThat(second).isEqualTo(first);
|
assertThat(second).isEqualTo(first);
|
||||||
assertThat(second.drivingInterruptionIntervals()).hasSize(1);
|
assertThat(second.drivingInterruptionIntervals()).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clearsVuCardAbsentWindowWhenRuntimeIsReused() {
|
||||||
|
DriverWorkingTimeReusableProjectionBuilder builder =
|
||||||
|
new DriverWorkingTimeReusableProjectionBuilder(new EventHubProperties());
|
||||||
|
UUID sessionId = UUID.randomUUID();
|
||||||
|
OffsetDateTime from = OffsetDateTime.parse("2026-05-01T08:00:00Z");
|
||||||
|
OffsetDateTime firstDriveEnd = OffsetDateTime.parse("2026-05-01T09:00:00Z");
|
||||||
|
OffsetDateTime secondDriveStart = OffsetDateTime.parse("2026-05-01T12:00:00Z");
|
||||||
|
OffsetDateTime to = OffsetDateTime.parse("2026-05-01T13:00:00Z");
|
||||||
|
|
||||||
|
DriverWorkingTimeProcessingInput input = new DriverWorkingTimeProcessingInput(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
15,
|
||||||
|
30,
|
||||||
|
List.of(
|
||||||
|
new DriverWorkingTimeActivityInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"ACT-1",
|
||||||
|
"DRIVE",
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"SINGLE",
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"ACT-1",
|
||||||
|
"ACT-1",
|
||||||
|
from,
|
||||||
|
firstDriveEnd,
|
||||||
|
from.toEpochSecond(),
|
||||||
|
firstDriveEnd.toEpochSecond(),
|
||||||
|
firstDriveEnd.toEpochSecond() - from.toEpochSecond(),
|
||||||
|
List.of("ACT-1"),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"RAW_INTERVAL"
|
||||||
|
),
|
||||||
|
new DriverWorkingTimeActivityInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"ACT-2",
|
||||||
|
"DRIVE",
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"SINGLE",
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"ACT-2",
|
||||||
|
"ACT-2",
|
||||||
|
secondDriveStart,
|
||||||
|
to,
|
||||||
|
secondDriveStart.toEpochSecond(),
|
||||||
|
to.toEpochSecond(),
|
||||||
|
to.toEpochSecond() - secondDriveStart.toEpochSecond(),
|
||||||
|
List.of("ACT-2"),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"RAW_INTERVAL"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
List.of(
|
||||||
|
new DriverWorkingTimeVehicleUsageInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"VU-1",
|
||||||
|
"VU-1",
|
||||||
|
"VU-1",
|
||||||
|
from,
|
||||||
|
firstDriveEnd,
|
||||||
|
from.toEpochSecond(),
|
||||||
|
firstDriveEnd.toEpochSecond(),
|
||||||
|
firstDriveEnd.toEpochSecond() - from.toEpochSecond(),
|
||||||
|
100L,
|
||||||
|
150L,
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
List.of("VU-1")
|
||||||
|
),
|
||||||
|
new DriverWorkingTimeVehicleUsageInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"VU-2",
|
||||||
|
"VU-2",
|
||||||
|
"VU-2",
|
||||||
|
secondDriveStart,
|
||||||
|
to,
|
||||||
|
secondDriveStart.toEpochSecond(),
|
||||||
|
to.toEpochSecond(),
|
||||||
|
to.toEpochSecond() - secondDriveStart.toEpochSecond(),
|
||||||
|
150L,
|
||||||
|
200L,
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
List.of("VU-2")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
List.of(),
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
DriverWorkingTimeDerivedProjectionBundle first = builder.buildDerivedProjectionBundle(input);
|
||||||
|
DriverWorkingTimeDerivedProjectionBundle second = builder.buildDerivedProjectionBundle(input);
|
||||||
|
|
||||||
|
assertThat(first.vuCardAbsentIntervals()).hasSize(1);
|
||||||
|
assertThat(second.vuCardAbsentIntervals()).hasSize(1);
|
||||||
|
assertThat(first.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
|
||||||
|
assertThat(second.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
|
||||||
|
assertThat(second.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentDurationSeconds())
|
||||||
|
.isEqualTo(first.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentDurationSeconds());
|
||||||
|
assertThat(second.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentCoveragePercent())
|
||||||
|
.isEqualTo(first.dailyWeeklyRestCandidateCoverageIntervals().get(0).cardAbsentCoveragePercent())
|
||||||
|
.isLessThanOrEqualTo(100.0d);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cleanupContractCoversEveryPublicNamedWindow() throws IOException {
|
||||||
|
String epl = StreamUtils.copyToString(
|
||||||
|
new ClassPathResource("esper/driver-working-time-derived-projections.epl").getInputStream(),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
);
|
||||||
|
Matcher matcher = Pattern.compile(
|
||||||
|
"(?m)@public(?:\\s+context\\s+[A-Za-z0-9_]+)?\\s*create\\s+window\\s+([A-Za-z0-9_]+)"
|
||||||
|
).matcher(epl);
|
||||||
|
Set<String> publicNamedWindows = new LinkedHashSet<>();
|
||||||
|
while (matcher.find()) {
|
||||||
|
publicNamedWindows.add(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(publicNamedWindows).isNotEmpty();
|
||||||
|
for (String windowName : publicNamedWindows) {
|
||||||
|
assertThat(DriverWorkingTimeReusableProjectionBuilder.REUSABLE_RUNTIME_STATE_CLEANUP_QUERIES)
|
||||||
|
.anyMatch(query -> query.contains("delete from " + windowName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clearsRetainedVehicleUsageInputBetweenExecutions() {
|
||||||
|
DriverWorkingTimeReusableProjectionBuilder builder =
|
||||||
|
new DriverWorkingTimeReusableProjectionBuilder(new EventHubProperties());
|
||||||
|
UUID sessionId = UUID.randomUUID();
|
||||||
|
OffsetDateTime from = OffsetDateTime.parse("2026-05-01T08:00:00Z");
|
||||||
|
OffsetDateTime firstDriveEnd = OffsetDateTime.parse("2026-05-01T09:00:00Z");
|
||||||
|
OffsetDateTime secondDriveStart = OffsetDateTime.parse("2026-05-01T12:00:00Z");
|
||||||
|
OffsetDateTime to = OffsetDateTime.parse("2026-05-01T13:00:00Z");
|
||||||
|
|
||||||
|
List<DriverWorkingTimeActivityInterval> activities = List.of(
|
||||||
|
new DriverWorkingTimeActivityInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"ACT-1",
|
||||||
|
"DRIVE",
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"SINGLE",
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"ACT-1",
|
||||||
|
"ACT-1",
|
||||||
|
from,
|
||||||
|
firstDriveEnd,
|
||||||
|
from.toEpochSecond(),
|
||||||
|
firstDriveEnd.toEpochSecond(),
|
||||||
|
firstDriveEnd.toEpochSecond() - from.toEpochSecond(),
|
||||||
|
List.of("ACT-1"),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"RAW_INTERVAL"
|
||||||
|
),
|
||||||
|
new DriverWorkingTimeActivityInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"ACT-2",
|
||||||
|
"DRIVE",
|
||||||
|
"DRIVER",
|
||||||
|
"INSERTED",
|
||||||
|
"SINGLE",
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"ACT-2",
|
||||||
|
"ACT-2",
|
||||||
|
secondDriveStart,
|
||||||
|
to,
|
||||||
|
secondDriveStart.toEpochSecond(),
|
||||||
|
to.toEpochSecond(),
|
||||||
|
to.toEpochSecond() - secondDriveStart.toEpochSecond(),
|
||||||
|
List.of("ACT-2"),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"RAW_INTERVAL"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DriverWorkingTimeProcessingInput withVehicleUsage = new DriverWorkingTimeProcessingInput(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
15,
|
||||||
|
30,
|
||||||
|
activities,
|
||||||
|
List.of(new DriverWorkingTimeVehicleUsageInterval(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"VU-1",
|
||||||
|
"VU-1",
|
||||||
|
"VU-1",
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
from.toEpochSecond(),
|
||||||
|
to.toEpochSecond(),
|
||||||
|
to.toEpochSecond() - from.toEpochSecond(),
|
||||||
|
100L,
|
||||||
|
200L,
|
||||||
|
"12:REG-1",
|
||||||
|
"VIN-1",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
List.of("VU-1")
|
||||||
|
)),
|
||||||
|
List.of(),
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
DriverWorkingTimeProcessingInput withoutVehicleUsage = new DriverWorkingTimeProcessingInput(
|
||||||
|
sessionId,
|
||||||
|
"12:123",
|
||||||
|
"DRIVER_CARD",
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
15,
|
||||||
|
30,
|
||||||
|
activities,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
DriverWorkingTimeDerivedProjectionBundle first = builder.buildDerivedProjectionBundle(withVehicleUsage);
|
||||||
|
DriverWorkingTimeDerivedProjectionBundle second = builder.buildDerivedProjectionBundle(withoutVehicleUsage);
|
||||||
|
|
||||||
|
assertThat(first.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
|
||||||
|
assertThat(first.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginBoundaryOdometerKm())
|
||||||
|
.isNotNull();
|
||||||
|
assertThat(second.dailyWeeklyRestCandidateCoverageIntervals()).hasSize(1);
|
||||||
|
assertThat(second.dailyWeeklyRestCandidateCoverageIntervals().get(0).beginBoundaryOdometerKm())
|
||||||
|
.isNull();
|
||||||
|
assertThat(second.dailyWeeklyRestCandidateCoverageIntervals().get(0).endBoundaryOdometerKm())
|
||||||
|
.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import at.procon.eventhub.dto.VehicleRefDto;
|
||||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -423,6 +424,388 @@ class RuntimeEventMixingServiceTest {
|
||||||
.containsExactlyInAnyOrder("VU_LOAD_UNLOAD", "VU_SPECIFIC_CONDITION");
|
.containsExactlyInAnyOrder("VU_LOAD_UNLOAD", "VU_SPECIFIC_CONDITION");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mixesFileSessionCardPlaceWithDatabaseVuPlaceAcrossRepresentationDifferences() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
|
||||||
|
EventHubEventDto fileCard = crossRepresentationPlace(
|
||||||
|
true,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:card-place-28",
|
||||||
|
occurredAt,
|
||||||
|
"13",
|
||||||
|
"0",
|
||||||
|
new BigDecimal("50.6400000000000000"),
|
||||||
|
new BigDecimal("9.3883333333333336")
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseVu = crossRepresentationPlace(
|
||||||
|
false,
|
||||||
|
"4693459",
|
||||||
|
occurredAt,
|
||||||
|
"D",
|
||||||
|
null,
|
||||||
|
new BigDecimal("50.64"),
|
||||||
|
new BigDecimal("9.388333333333334")
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileCard, databaseVu),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.supportEvidenceEvents()).hasSize(1);
|
||||||
|
assertThat(mixed.supportEvidenceEvents().getFirst().externalSourceEventId())
|
||||||
|
.isEqualTo(fileCard.externalSourceEventId());
|
||||||
|
assertThat(mixed.supportEvidenceEvents().getFirst().vehicleRef().vin())
|
||||||
|
.isEqualTo("XLRTEH4300G376073");
|
||||||
|
assertThat(mixed.suppressedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(databaseVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.eventMixingDecisions()).hasSize(1);
|
||||||
|
assertThat(mixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mixesFileSessionCardActivityWithDatabaseVuActivity() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T05:22:00Z");
|
||||||
|
EventHubEventDto fileCard = crossRepresentationActivity(
|
||||||
|
true,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:card-activity-1",
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseVu = crossRepresentationActivity(
|
||||||
|
false,
|
||||||
|
"116710708",
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileCard, databaseVu),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.activityTimelineEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(fileCard.externalSourceEventId());
|
||||||
|
assertThat(mixed.suppressedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(databaseVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.eventMixingDecisions()).hasSize(1);
|
||||||
|
assertThat(mixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_COMPATIBLE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suppressesFileSessionVuPlaceWhenSameVuFactIsLoadedFromDatabase() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
|
||||||
|
EventHubEventDto fileVu = sameRoleCrossRepresentationPlace(
|
||||||
|
true,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:vu-place-28",
|
||||||
|
occurredAt,
|
||||||
|
"13",
|
||||||
|
"0",
|
||||||
|
new BigDecimal("50.6400000000000000"),
|
||||||
|
new BigDecimal("9.3883333333333336")
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseVu = sameRoleCrossRepresentationPlace(
|
||||||
|
false,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"4693459",
|
||||||
|
occurredAt,
|
||||||
|
"D",
|
||||||
|
null,
|
||||||
|
new BigDecimal("50.64"),
|
||||||
|
new BigDecimal("9.388333333333334")
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileVu, databaseVu),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.supportEvidenceEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(databaseVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.suppressedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(fileVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.eventMixingDecisions()).hasSize(1);
|
||||||
|
assertThat(mixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_SUPPORT_SAME_ROLE);
|
||||||
|
assertThat(mixed.diagnostics().vehicleUnitSourceRoleCount()).isEqualTo(2);
|
||||||
|
assertThat(mixed.diagnostics().databaseRepresentationCount()).isEqualTo(1);
|
||||||
|
assertThat(mixed.diagnostics().fileSessionRepresentationCount()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suppressesFileSessionCardActivityWhenSameCardFactIsLoadedFromDatabase() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T05:22:00Z");
|
||||||
|
EventHubEventDto fileCard = sameRoleCrossRepresentationActivity(
|
||||||
|
true,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:card-activity-1",
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseCard = sameRoleCrossRepresentationActivity(
|
||||||
|
false,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"116710708",
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileCard, databaseCard),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.activityTimelineEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(databaseCard.externalSourceEventId());
|
||||||
|
assertThat(mixed.suppressedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(fileCard.externalSourceEventId());
|
||||||
|
assertThat(mixed.eventMixingDecisions()).hasSize(1);
|
||||||
|
assertThat(mixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_ACTIVITY_SAME_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsCrossRepresentationSupportEventsWhenMeaningfulCoordinatesConflict() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
|
||||||
|
EventHubEventDto fileCard = crossRepresentationPlace(
|
||||||
|
true,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:card-place-conflict",
|
||||||
|
occurredAt,
|
||||||
|
"13",
|
||||||
|
"0",
|
||||||
|
new BigDecimal("50.64"),
|
||||||
|
new BigDecimal("9.3883333333333336")
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseVu = crossRepresentationPlace(
|
||||||
|
false,
|
||||||
|
"4693460",
|
||||||
|
occurredAt,
|
||||||
|
"D",
|
||||||
|
null,
|
||||||
|
new BigDecimal("50.64"),
|
||||||
|
new BigDecimal("10.0")
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileCard, databaseVu),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.supportEvidenceEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactlyInAnyOrder(fileCard.externalSourceEventId(), databaseVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.suppressedEvents()).isEmpty();
|
||||||
|
assertThat(mixed.eventMixingDecisions()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto sameRoleCrossRepresentationPlace(
|
||||||
|
boolean fileSession,
|
||||||
|
String sourceKind,
|
||||||
|
String externalId,
|
||||||
|
OffsetDateTime occurredAt,
|
||||||
|
String country,
|
||||||
|
String region,
|
||||||
|
BigDecimal latitude,
|
||||||
|
BigDecimal longitude
|
||||||
|
) {
|
||||||
|
ObjectNode raw = fileSession
|
||||||
|
? baseRawWithoutExtraction(sourceKind)
|
||||||
|
: baseRaw(sourceKind.equals("DRIVER_CARD") ? "CARD_PLACE" : "VU_PLACE", sourceKind);
|
||||||
|
raw.put("latitude", latitude.toPlainString());
|
||||||
|
raw.put("longitude", longitude.toPlainString());
|
||||||
|
raw.put("odometerM", "172945000");
|
||||||
|
raw.put("country", country);
|
||||||
|
if (region != null) {
|
||||||
|
raw.put("region", region);
|
||||||
|
}
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
ObjectNode attributes = JsonNodeFactory.instance.objectNode();
|
||||||
|
attributes.put("country", country);
|
||||||
|
if (region != null) {
|
||||||
|
attributes.put("region", region);
|
||||||
|
}
|
||||||
|
VehicleRefDto vehicleRef = fileSession
|
||||||
|
? new VehicleRefDto(null, null, null, new VehicleRegistrationRefDto("13", 13, "RO BS 2219"))
|
||||||
|
: new VehicleRefDto("3342", "XLRTEH4300G376073", null, new VehicleRegistrationRefDto("D", 13, "RO BS 2219"));
|
||||||
|
return new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("13:DF000358328840", new DriverCardRefDto("13", 13, "DF000358328840")),
|
||||||
|
vehicleRef,
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
occurredAt,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
fileSession ? EventLifecycle.BEGIN : EventLifecycle.START,
|
||||||
|
172945000L,
|
||||||
|
new at.procon.eventhub.dto.GeoPointDto(latitude, longitude),
|
||||||
|
new EventDetailsDto("PLACE", attributes),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo(
|
||||||
|
fileSession ? "default" : "TENANT_A",
|
||||||
|
fileSession ? "TACHOGRAPH_FILE_SESSION" : "TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
occurredAt.toLocalDate()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto sameRoleCrossRepresentationActivity(
|
||||||
|
boolean fileSession,
|
||||||
|
String sourceKind,
|
||||||
|
String externalId,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
String extractionCode = sourceKind.equals("DRIVER_CARD") ? "CARD_ACTIVITY" : "VU_ACTIVITY";
|
||||||
|
ObjectNode raw = fileSession
|
||||||
|
? baseRawWithoutExtraction(sourceKind)
|
||||||
|
: baseRaw(extractionCode, sourceKind);
|
||||||
|
raw.put("activityType", "DRIVE");
|
||||||
|
raw.put("cardSlot", "DRIVER");
|
||||||
|
raw.put("startedAt", occurredAt.toString());
|
||||||
|
raw.put("endedAt", occurredAt.plusMinutes(30).toString());
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
ObjectNode attributes = JsonNodeFactory.instance.objectNode();
|
||||||
|
attributes.put("cardSlot", "DRIVER");
|
||||||
|
return new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("13:DF000358328840", new DriverCardRefDto("13", 13, "DF000358328840")),
|
||||||
|
new VehicleRefDto(null, null, null, new VehicleRegistrationRefDto("13", 13, "RO BS 2219")),
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
occurredAt,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventLifecycle.START,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new EventDetailsDto("DRIVER_ACTIVITY", attributes),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo(
|
||||||
|
fileSession ? "default" : "TENANT_A",
|
||||||
|
fileSession ? "TACHOGRAPH_FILE_SESSION" : "TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
occurredAt.toLocalDate()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto crossRepresentationPlace(
|
||||||
|
boolean fileSessionCard,
|
||||||
|
String externalId,
|
||||||
|
OffsetDateTime occurredAt,
|
||||||
|
String country,
|
||||||
|
String region,
|
||||||
|
BigDecimal latitude,
|
||||||
|
BigDecimal longitude
|
||||||
|
) {
|
||||||
|
String sourceKind = fileSessionCard ? "DRIVER_CARD" : "VEHICLE_UNIT";
|
||||||
|
ObjectNode raw = fileSessionCard
|
||||||
|
? baseRawWithoutExtraction(sourceKind)
|
||||||
|
: baseRaw("VU_PLACE", sourceKind);
|
||||||
|
raw.put("latitude", latitude.toPlainString());
|
||||||
|
raw.put("longitude", longitude.toPlainString());
|
||||||
|
raw.put("odometerM", "172945000");
|
||||||
|
raw.put("country", country);
|
||||||
|
if (region != null) {
|
||||||
|
raw.put("region", region);
|
||||||
|
}
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
ObjectNode attributes = JsonNodeFactory.instance.objectNode();
|
||||||
|
attributes.put("country", country);
|
||||||
|
if (region != null) {
|
||||||
|
attributes.put("region", region);
|
||||||
|
}
|
||||||
|
VehicleRefDto vehicleRef = fileSessionCard
|
||||||
|
? new VehicleRefDto(null, null, null, new VehicleRegistrationRefDto("13", 13, "RO BS 2219"))
|
||||||
|
: new VehicleRefDto("3342", "XLRTEH4300G376073", null, new VehicleRegistrationRefDto("D", 13, "RO BS 2219"));
|
||||||
|
EventHubEventDto event = new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("13:DF000358328840", new DriverCardRefDto("13", 13, "DF000358328840")),
|
||||||
|
vehicleRef,
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
occurredAt,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
fileSessionCard ? EventLifecycle.BEGIN : EventLifecycle.START,
|
||||||
|
172945000L,
|
||||||
|
new at.procon.eventhub.dto.GeoPointDto(latitude, longitude),
|
||||||
|
new EventDetailsDto("PLACE", attributes),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo(
|
||||||
|
fileSessionCard ? "default" : "TENANT_A",
|
||||||
|
fileSessionCard ? "TACHOGRAPH_FILE_SESSION" : "TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
occurredAt.toLocalDate()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto crossRepresentationActivity(
|
||||||
|
boolean fileSessionCard,
|
||||||
|
String externalId,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
String sourceKind = fileSessionCard ? "DRIVER_CARD" : "VEHICLE_UNIT";
|
||||||
|
ObjectNode raw = fileSessionCard
|
||||||
|
? baseRawWithoutExtraction(sourceKind)
|
||||||
|
: baseRaw("VU_ACTIVITY", sourceKind);
|
||||||
|
raw.put("activityType", "DRIVE");
|
||||||
|
raw.put("cardSlot", "DRIVER");
|
||||||
|
raw.put("startedAt", "2026-04-01T05:22:00Z");
|
||||||
|
raw.put("endedAt", "2026-04-01T05:52:00Z");
|
||||||
|
if (fileSessionCard) {
|
||||||
|
raw.put("cardStatus", "INSERTED");
|
||||||
|
raw.put("drivingStatus", "SINGLE");
|
||||||
|
}
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
ObjectNode attributes = JsonNodeFactory.instance.objectNode();
|
||||||
|
attributes.put("cardSlot", "DRIVER");
|
||||||
|
VehicleRefDto vehicleRef = fileSessionCard
|
||||||
|
? new VehicleRefDto(null, null, null, new VehicleRegistrationRefDto("13", 13, "RO BS 2219"))
|
||||||
|
: new VehicleRefDto("3342", "XLRTEH4300G376073", null, new VehicleRegistrationRefDto("D", 13, "RO BS 2219"));
|
||||||
|
return new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("13:DF000358328840", new DriverCardRefDto("13", 13, "DF000358328840")),
|
||||||
|
vehicleRef,
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
occurredAt,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventLifecycle.START,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new EventDetailsDto("DRIVER_ACTIVITY", attributes),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo(
|
||||||
|
fileSessionCard ? "default" : "TENANT_A",
|
||||||
|
fileSessionCard ? "TACHOGRAPH_FILE_SESSION" : "TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
occurredAt.toLocalDate()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private EventHubEventDto activity(String extractionCode, String sourceKind, String externalId) {
|
private EventHubEventDto activity(String extractionCode, String sourceKind, String externalId) {
|
||||||
ObjectNode raw = baseRaw(extractionCode, sourceKind);
|
ObjectNode raw = baseRaw(extractionCode, sourceKind);
|
||||||
raw.put("activityType", "BREAK_REST");
|
raw.put("activityType", "BREAK_REST");
|
||||||
|
|
@ -665,6 +1048,27 @@ class RuntimeEventMixingServiceTest {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EventHubPackageRequest packageInfo(
|
||||||
|
String tenantKey,
|
||||||
|
String providerKey,
|
||||||
|
String sourceKind,
|
||||||
|
EventDomain domain,
|
||||||
|
LocalDate businessDate
|
||||||
|
) {
|
||||||
|
EventSourceDto source = new EventSourceDto(providerKey, sourceKind, providerKey + "_" + sourceKind, null, null, null);
|
||||||
|
OffsetDateTime from = businessDate.atStartOfDay().atOffset(java.time.ZoneOffset.UTC);
|
||||||
|
OffsetDateTime to = businessDate.plusDays(1).atStartOfDay().atOffset(java.time.ZoneOffset.UTC);
|
||||||
|
return new EventHubPackageRequest(
|
||||||
|
tenantKey,
|
||||||
|
source,
|
||||||
|
null,
|
||||||
|
ImportScopeDto.tenantAll(from, to),
|
||||||
|
domain.name(),
|
||||||
|
businessDate,
|
||||||
|
source.stableKey() + ":" + domain.name() + ":" + businessDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private EventHubPackageRequest packageInfo(String sourceKind, EventDomain domain, LocalDate businessDate) {
|
private EventHubPackageRequest packageInfo(String sourceKind, EventDomain domain, LocalDate businessDate) {
|
||||||
EventSourceDto source = new EventSourceDto("TACHOGRAPH", sourceKind, "TACHOGRAPH_" + sourceKind, null, null, null);
|
EventSourceDto source = new EventSourceDto("TACHOGRAPH", sourceKind, "TACHOGRAPH_" + sourceKind, null, null, null);
|
||||||
OffsetDateTime from = businessDate.atStartOfDay().atOffset(java.time.ZoneOffset.UTC);
|
OffsetDateTime from = businessDate.atStartOfDay().atOffset(java.time.ZoneOffset.UTC);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.DriverCardRefDto;
|
||||||
|
import at.procon.eventhub.dto.DriverRefDto;
|
||||||
|
import at.procon.eventhub.dto.EventDetailsDto;
|
||||||
|
import at.procon.eventhub.dto.EventDomain;
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.dto.EventHubPackageRequest;
|
||||||
|
import at.procon.eventhub.dto.EventLifecycle;
|
||||||
|
import at.procon.eventhub.dto.EventSourceDto;
|
||||||
|
import at.procon.eventhub.dto.EventType;
|
||||||
|
import at.procon.eventhub.dto.ImportScopeDto;
|
||||||
|
import at.procon.eventhub.dto.SourcePackageRefDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRefDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||||
|
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RuntimeTachographRepresentationParityTest {
|
||||||
|
|
||||||
|
private static final OffsetDateTime OCCURRED_AT = OffsetDateTime.parse("2026-04-01T08:00:00Z");
|
||||||
|
|
||||||
|
private final RuntimeEventDescriptorFactory descriptorFactory = new RuntimeEventDescriptorFactory();
|
||||||
|
private final RuntimeEventMixingService mixingService = new RuntimeEventMixingService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void producesSameCanonicalSourceProfileForDbAndFileSessionPlaceRepresentations() {
|
||||||
|
EventHubEventDto dbEvent = event(
|
||||||
|
Representation.DB,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"TACHOGRAPH:CARD_PLACE:10",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.START
|
||||||
|
);
|
||||||
|
EventHubEventDto fileSessionEvent = event(
|
||||||
|
Representation.FILE_SESSION,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:11111111-1111-1111-1111-111111111111:SUPPORT:place-10:BEGIN:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeEventDescriptor dbDescriptor = descriptorFactory.describe(dbEvent);
|
||||||
|
RuntimeEventDescriptor fileDescriptor = descriptorFactory.describe(fileSessionEvent);
|
||||||
|
|
||||||
|
assertThat(dbDescriptor.sourceProfile().sourceSystem()).isEqualTo("TACHOGRAPH");
|
||||||
|
assertThat(fileDescriptor.sourceProfile().sourceSystem()).isEqualTo("TACHOGRAPH");
|
||||||
|
assertThat(dbDescriptor.sourceProfile().sourceKind()).isEqualTo("DRIVER_CARD");
|
||||||
|
assertThat(fileDescriptor.sourceProfile().sourceKind()).isEqualTo("DRIVER_CARD");
|
||||||
|
assertThat(dbDescriptor.sourceProfile().extractionCode()).isEqualTo("CARD_PLACE");
|
||||||
|
assertThat(fileDescriptor.sourceProfile().extractionCode()).isEqualTo("CARD_PLACE");
|
||||||
|
assertThat(dbDescriptor.evidenceSourceRole())
|
||||||
|
.isEqualTo(RuntimeTachographEvidenceSourceRole.DRIVER_CARD);
|
||||||
|
assertThat(fileDescriptor.evidenceSourceRole())
|
||||||
|
.isEqualTo(RuntimeTachographEvidenceSourceRole.DRIVER_CARD);
|
||||||
|
assertThat(dbDescriptor.representation())
|
||||||
|
.isEqualTo(RuntimeTachographRepresentation.DATABASE);
|
||||||
|
assertThat(fileDescriptor.representation())
|
||||||
|
.isEqualTo(RuntimeTachographRepresentation.FILE_SESSION);
|
||||||
|
assertThat(fileDescriptor.compatibleSupportEvidenceKey())
|
||||||
|
.isEqualTo(dbDescriptor.compatibleSupportEvidenceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void infersSameVehicleUsageExtractionProfilesForDbAndFileSessionRepresentations() {
|
||||||
|
EventHubEventDto dbCvu = event(
|
||||||
|
Representation.DB,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_VEHICLES_USED",
|
||||||
|
"TACHOGRAPH:CARD_VEHICLES_USED:10:INSERT",
|
||||||
|
EventDomain.DRIVER_CARD,
|
||||||
|
EventType.CARD_INSERTED,
|
||||||
|
EventLifecycle.INSERT
|
||||||
|
);
|
||||||
|
EventHubEventDto fileCvu = event(
|
||||||
|
Representation.FILE_SESSION,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:11111111-1111-1111-1111-111111111111:CARD_USAGE:cvu-10:INSERT:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_CARD,
|
||||||
|
EventType.CARD_INSERTED,
|
||||||
|
EventLifecycle.INSERT
|
||||||
|
);
|
||||||
|
EventHubEventDto dbIw = event(
|
||||||
|
Representation.DB,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"IW_CYCLE",
|
||||||
|
"TACHOGRAPH:IW_CYCLE:20:INSERT",
|
||||||
|
EventDomain.DRIVER_CARD,
|
||||||
|
EventType.CARD_INSERTED,
|
||||||
|
EventLifecycle.INSERT
|
||||||
|
);
|
||||||
|
EventHubEventDto fileIw = event(
|
||||||
|
Representation.FILE_SESSION,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:22222222-2222-2222-2222-222222222222:CARD_USAGE:iw-20:INSERT:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_CARD,
|
||||||
|
EventType.CARD_INSERTED,
|
||||||
|
EventLifecycle.INSERT
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeEventSourceProfile dbCvuProfile = descriptorFactory.sourceProfile(dbCvu);
|
||||||
|
RuntimeEventSourceProfile fileCvuProfile = descriptorFactory.sourceProfile(fileCvu);
|
||||||
|
RuntimeEventSourceProfile dbIwProfile = descriptorFactory.sourceProfile(dbIw);
|
||||||
|
RuntimeEventSourceProfile fileIwProfile = descriptorFactory.sourceProfile(fileIw);
|
||||||
|
|
||||||
|
assertThat(fileCvuProfile.sourceKind()).isEqualTo(dbCvuProfile.sourceKind());
|
||||||
|
assertThat(fileCvuProfile.extractionCode()).isEqualTo(dbCvuProfile.extractionCode());
|
||||||
|
assertThat(fileCvuProfile.evidenceSourceRole()).isEqualTo(dbCvuProfile.evidenceSourceRole());
|
||||||
|
assertThat(fileIwProfile.sourceKind()).isEqualTo(dbIwProfile.sourceKind());
|
||||||
|
assertThat(fileIwProfile.extractionCode()).isEqualTo(dbIwProfile.extractionCode());
|
||||||
|
assertThat(fileIwProfile.evidenceSourceRole()).isEqualTo(dbIwProfile.evidenceSourceRole());
|
||||||
|
assertThat(dbCvuProfile.extractionCode()).isEqualTo("CARD_VEHICLES_USED");
|
||||||
|
assertThat(dbIwProfile.extractionCode()).isEqualTo("IW_CYCLE");
|
||||||
|
assertThat(fileCvuProfile.representation()).isEqualTo(RuntimeTachographRepresentation.FILE_SESSION);
|
||||||
|
assertThat(dbCvuProfile.representation()).isEqualTo(RuntimeTachographRepresentation.DATABASE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void producesEquivalentMixingOutcomeForDbAndFileSessionSupportPairs() {
|
||||||
|
RuntimeMixedEventBundle dbMixed = mixingService.mix(
|
||||||
|
List.of(
|
||||||
|
event(
|
||||||
|
Representation.DB,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"TACHOGRAPH:CARD_PLACE:10",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.START
|
||||||
|
),
|
||||||
|
event(
|
||||||
|
Representation.DB,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"VU_PLACE",
|
||||||
|
"TACHOGRAPH:VU_PLACE:20",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.START
|
||||||
|
)
|
||||||
|
),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
RuntimeMixedEventBundle fileMixed = mixingService.mix(
|
||||||
|
List.of(
|
||||||
|
event(
|
||||||
|
Representation.FILE_SESSION,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:11111111-1111-1111-1111-111111111111:SUPPORT:card-place-10:BEGIN:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN
|
||||||
|
),
|
||||||
|
event(
|
||||||
|
Representation.FILE_SESSION,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:22222222-2222-2222-2222-222222222222:SUPPORT:vu-place-20:BEGIN:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN
|
||||||
|
)
|
||||||
|
),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(fileMixed.supportEvidenceEvents()).hasSameSizeAs(dbMixed.supportEvidenceEvents());
|
||||||
|
assertThat(fileMixed.suppressedEvents()).hasSameSizeAs(dbMixed.suppressedEvents());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions()).hasSameSizeAs(dbMixed.eventMixingDecisions());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions().getFirst().primaryExtractionCode())
|
||||||
|
.isEqualTo(dbMixed.eventMixingDecisions().getFirst().primaryExtractionCode());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions().getFirst().secondaryExtractionCodes())
|
||||||
|
.isEqualTo(dbMixed.eventMixingDecisions().getFirst().secondaryExtractionCodes());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(dbMixed.eventMixingDecisions().getFirst().ruleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void producesEquivalentMixingOutcomeForDbAndFileSessionActivityPairs() {
|
||||||
|
RuntimeMixedEventBundle dbMixed = mixingService.mix(
|
||||||
|
List.of(
|
||||||
|
event(Representation.DB, "DRIVER_CARD", "CARD_ACTIVITY",
|
||||||
|
"TACHOGRAPH:CARD_ACTIVITY:10:START",
|
||||||
|
EventDomain.DRIVER_ACTIVITY, EventType.DRIVE, EventLifecycle.START),
|
||||||
|
event(Representation.DB, "VEHICLE_UNIT", "VU_ACTIVITY",
|
||||||
|
"TACHOGRAPH:VU_ACTIVITY:20:START",
|
||||||
|
EventDomain.DRIVER_ACTIVITY, EventType.DRIVE, EventLifecycle.START)
|
||||||
|
),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
RuntimeMixedEventBundle fileMixed = mixingService.mix(
|
||||||
|
List.of(
|
||||||
|
event(Representation.FILE_SESSION, "DRIVER_CARD", null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:11111111-1111-1111-1111-111111111111:ACTIVITY:card-10:START:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_ACTIVITY, EventType.DRIVE, EventLifecycle.START),
|
||||||
|
event(Representation.FILE_SESSION, "VEHICLE_UNIT", null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:22222222-2222-2222-2222-222222222222:ACTIVITY:vu-20:START:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_ACTIVITY, EventType.DRIVE, EventLifecycle.START)
|
||||||
|
),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(fileMixed.activityTimelineEvents()).hasSameSizeAs(dbMixed.activityTimelineEvents());
|
||||||
|
assertThat(fileMixed.suppressedEvents()).hasSameSizeAs(dbMixed.suppressedEvents());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions()).hasSameSizeAs(dbMixed.eventMixingDecisions());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions().getFirst().primaryExtractionCode())
|
||||||
|
.isEqualTo(dbMixed.eventMixingDecisions().getFirst().primaryExtractionCode());
|
||||||
|
assertThat(fileMixed.eventMixingDecisions().getFirst().secondaryExtractionCodes())
|
||||||
|
.isEqualTo(dbMixed.eventMixingDecisions().getFirst().secondaryExtractionCodes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preservesEquivalentCvuAndIwCycleInputsForBothRepresentations() {
|
||||||
|
List<EventHubEventDto> dbEvents = List.of(
|
||||||
|
event(Representation.DB, "DRIVER_CARD", "CARD_VEHICLES_USED",
|
||||||
|
"TACHOGRAPH:CARD_VEHICLES_USED:10:INSERT",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_INSERTED, EventLifecycle.INSERT),
|
||||||
|
event(Representation.DB, "DRIVER_CARD", "CARD_VEHICLES_USED",
|
||||||
|
"TACHOGRAPH:CARD_VEHICLES_USED:10:WITHDRAW",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_WITHDRAWN, EventLifecycle.WITHDRAW),
|
||||||
|
event(Representation.DB, "VEHICLE_UNIT", "IW_CYCLE",
|
||||||
|
"TACHOGRAPH:IW_CYCLE:20:INSERT",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_INSERTED, EventLifecycle.INSERT),
|
||||||
|
event(Representation.DB, "VEHICLE_UNIT", "IW_CYCLE",
|
||||||
|
"TACHOGRAPH:IW_CYCLE:20:WITHDRAW",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_WITHDRAWN, EventLifecycle.WITHDRAW)
|
||||||
|
);
|
||||||
|
List<EventHubEventDto> fileEvents = List.of(
|
||||||
|
event(Representation.FILE_SESSION, "DRIVER_CARD", null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:11111111-1111-1111-1111-111111111111:CARD_USAGE:cvu-10:INSERT:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_INSERTED, EventLifecycle.INSERT),
|
||||||
|
event(Representation.FILE_SESSION, "DRIVER_CARD", null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:11111111-1111-1111-1111-111111111111:CARD_USAGE:cvu-10:WITHDRAW:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_WITHDRAWN, EventLifecycle.WITHDRAW),
|
||||||
|
event(Representation.FILE_SESSION, "VEHICLE_UNIT", null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:22222222-2222-2222-2222-222222222222:CARD_USAGE:iw-20:INSERT:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_INSERTED, EventLifecycle.INSERT),
|
||||||
|
event(Representation.FILE_SESSION, "VEHICLE_UNIT", null,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:22222222-2222-2222-2222-222222222222:CARD_USAGE:iw-20:WITHDRAW:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.DRIVER_CARD, EventType.CARD_WITHDRAWN, EventLifecycle.WITHDRAW)
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle dbMixed = mixingService.mix(
|
||||||
|
dbEvents, RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE);
|
||||||
|
RuntimeMixedEventBundle fileMixed = mixingService.mix(
|
||||||
|
fileEvents, RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE);
|
||||||
|
|
||||||
|
assertThat(dbMixed.vehicleUsageEvents()).hasSize(4);
|
||||||
|
assertThat(fileMixed.vehicleUsageEvents()).hasSize(4);
|
||||||
|
assertThat(dbMixed.suppressedEvents()).isEmpty();
|
||||||
|
assertThat(fileMixed.suppressedEvents()).isEmpty();
|
||||||
|
assertThat(fileEvents).extracting(event -> descriptorFactory.sourceProfile(event).extractionCode())
|
||||||
|
.containsExactly("CARD_VEHICLES_USED", "CARD_VEHICLES_USED", "IW_CYCLE", "IW_CYCLE");
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto event(
|
||||||
|
Representation representation,
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
String externalSourceEventId,
|
||||||
|
EventDomain domain,
|
||||||
|
EventType type,
|
||||||
|
EventLifecycle lifecycle
|
||||||
|
) {
|
||||||
|
String provider = representation == Representation.DB ? "TACHOGRAPH" : "TACHOGRAPH_FILE_SESSION";
|
||||||
|
EventSourceDto source = new EventSourceDto(
|
||||||
|
provider,
|
||||||
|
sourceKind,
|
||||||
|
provider + "_" + sourceKind,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
EventHubPackageRequest packageInfo = new EventHubPackageRequest(
|
||||||
|
"default",
|
||||||
|
source,
|
||||||
|
null,
|
||||||
|
ImportScopeDto.tenantAll(null, null),
|
||||||
|
domain.name(),
|
||||||
|
null,
|
||||||
|
representation == Representation.DB
|
||||||
|
? "RUNTIME:TACHOGRAPH:" + (extractionCode == null ? "UNKNOWN" : extractionCode) + ":test"
|
||||||
|
: provider + ":package"
|
||||||
|
);
|
||||||
|
ObjectNode raw = JsonNodeFactory.instance.objectNode();
|
||||||
|
raw.put("sourceKind", sourceKind);
|
||||||
|
raw.put("driverKey", "12:123");
|
||||||
|
raw.put("registrationKey", "12:REG-1");
|
||||||
|
if (extractionCode != null) {
|
||||||
|
raw.put("extractionCode", extractionCode);
|
||||||
|
}
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
return new EventHubEventDto(
|
||||||
|
null,
|
||||||
|
externalSourceEventId,
|
||||||
|
new DriverRefDto("driver-1", new DriverCardRefDto("12", "123")),
|
||||||
|
new VehicleRefDto(
|
||||||
|
sourceKind.equals("VEHICLE_UNIT") ? "vehicle-1" : null,
|
||||||
|
sourceKind.equals("VEHICLE_UNIT") ? "VIN-1" : null,
|
||||||
|
new VehicleRegistrationRefDto("12", "REG-1")
|
||||||
|
),
|
||||||
|
OCCURRED_AT,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
domain,
|
||||||
|
type,
|
||||||
|
lifecycle,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new EventDetailsDto(domain.name(), JsonNodeFactory.instance.objectNode()),
|
||||||
|
new SourcePackageRefDto(
|
||||||
|
"TACHOGRAPH_FILE_SESSION",
|
||||||
|
representation == Representation.DB ? "package-1" : "session-1",
|
||||||
|
sourceKind.equals("VEHICLE_UNIT") ? "vehicle-1" : "card-1",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Representation {
|
||||||
|
DB,
|
||||||
|
FILE_SESSION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.module;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.DriverRefDto;
|
||||||
|
import at.procon.eventhub.dto.EventDomain;
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.dto.EventLifecycle;
|
||||||
|
import at.procon.eventhub.dto.EventType;
|
||||||
|
import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest;
|
||||||
|
import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest;
|
||||||
|
import at.procon.eventhub.processing.model.UnifiedEventSourceFamily;
|
||||||
|
import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle;
|
||||||
|
import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RuntimeEventAssemblyModuleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesAggregatedTerminologyAndKeepsMergedCountCompatibilityAlias() {
|
||||||
|
UnifiedRuntimeEventAssemblyService service =
|
||||||
|
org.mockito.Mockito.mock(UnifiedRuntimeEventAssemblyService.class);
|
||||||
|
RuntimeEventAssemblyModule module = new RuntimeEventAssemblyModule(service);
|
||||||
|
UnifiedRuntimeProcessingApiRequest scope = scopeRequest();
|
||||||
|
EventHubEventDto first = event("EVENT-1", "2026-05-01T08:00:00Z");
|
||||||
|
EventHubEventDto second = event("EVENT-2", "2026-05-01T09:00:00Z");
|
||||||
|
UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle(
|
||||||
|
scope.toRuntimeRequest(),
|
||||||
|
List.of(first),
|
||||||
|
List.of(),
|
||||||
|
List.of(second),
|
||||||
|
List.of(first, second),
|
||||||
|
List.of("assembled")
|
||||||
|
);
|
||||||
|
when(service.assembleDriverScopedEvents(any())).thenReturn(bundle);
|
||||||
|
|
||||||
|
RuntimeProcessingModuleResult result = module.execute(new RuntimeProcessingModuleContext(
|
||||||
|
new RuntimeProcessingExecutionApiRequest(
|
||||||
|
"driver-working-time-v1",
|
||||||
|
scope,
|
||||||
|
null,
|
||||||
|
List.of(),
|
||||||
|
Map.of()
|
||||||
|
),
|
||||||
|
List.of(),
|
||||||
|
Map.of("runtimeScopeApiRequest", scope),
|
||||||
|
Map.of()
|
||||||
|
));
|
||||||
|
|
||||||
|
assertThat(module.descriptor().description())
|
||||||
|
.contains("aggregates additional events")
|
||||||
|
.contains("mixing and reconciliation");
|
||||||
|
assertThat(result.output()).isSameAs(bundle);
|
||||||
|
assertThat(result.metadata())
|
||||||
|
.containsEntry("aggregatedEventCount", 2)
|
||||||
|
.containsEntry("mergedEventCount", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UnifiedRuntimeProcessingApiRequest scopeRequest() {
|
||||||
|
return new UnifiedRuntimeProcessingApiRequest(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
List.of(),
|
||||||
|
null,
|
||||||
|
"default",
|
||||||
|
Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"12:123",
|
||||||
|
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,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto event(String externalId, String occurredAt) {
|
||||||
|
OffsetDateTime timestamp = OffsetDateTime.parse(occurredAt);
|
||||||
|
return new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("12:123", null),
|
||||||
|
null,
|
||||||
|
timestamp,
|
||||||
|
null,
|
||||||
|
timestamp,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventLifecycle.START,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
package at.procon.eventhub.processing.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.DriverCardRefDto;
|
||||||
|
import at.procon.eventhub.dto.DriverRefDto;
|
||||||
|
import at.procon.eventhub.dto.EventDetailsDto;
|
||||||
|
import at.procon.eventhub.dto.EventDomain;
|
||||||
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
|
import at.procon.eventhub.dto.EventHubPackageRequest;
|
||||||
|
import at.procon.eventhub.dto.EventLifecycle;
|
||||||
|
import at.procon.eventhub.dto.EventSourceDto;
|
||||||
|
import at.procon.eventhub.dto.EventType;
|
||||||
|
import at.procon.eventhub.dto.ImportScopeDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRefDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||||
|
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RuntimeEventAggregationServiceTest {
|
||||||
|
|
||||||
|
private final RuntimeEventAggregationService service = new RuntimeEventAggregationService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removesOnlyRepeatedReadsOfSameSourceRecord() {
|
||||||
|
EventHubEventDto cardPosition = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_POSITION",
|
||||||
|
"TACHOGRAPH:CARD_POSITION:10",
|
||||||
|
EventDomain.POSITION,
|
||||||
|
EventType.POSITION_RECORDED,
|
||||||
|
EventLifecycle.SNAPSHOT,
|
||||||
|
"10",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto vuPosition = event(
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"VU_POSITION",
|
||||||
|
"TACHOGRAPH:VU_POSITION:20",
|
||||||
|
EventDomain.POSITION,
|
||||||
|
EventType.POSITION_RECORDED,
|
||||||
|
EventLifecycle.SNAPSHOT,
|
||||||
|
"20",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto cvu = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_VEHICLES_USED",
|
||||||
|
"TACHOGRAPH:CARD_VEHICLES_USED:30:INSERT",
|
||||||
|
EventDomain.DRIVER_CARD,
|
||||||
|
EventType.CARD_INSERTED,
|
||||||
|
EventLifecycle.INSERT,
|
||||||
|
"30",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto iwCycle = event(
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"IW_CYCLE",
|
||||||
|
"TACHOGRAPH:IW_CYCLE:40:INSERT",
|
||||||
|
EventDomain.DRIVER_CARD,
|
||||||
|
EventType.CARD_INSERTED,
|
||||||
|
EventLifecycle.INSERT,
|
||||||
|
"40",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
|
||||||
|
List<EventHubEventDto> aggregated = service.aggregateRuntimeEvents(
|
||||||
|
List.of(cardPosition, vuPosition, cvu, iwCycle),
|
||||||
|
List.of(cardPosition)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(aggregated).hasSize(4);
|
||||||
|
assertThat(aggregated).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactlyInAnyOrder(
|
||||||
|
cardPosition.externalSourceEventId(),
|
||||||
|
vuPosition.externalSourceEventId(),
|
||||||
|
cvu.externalSourceEventId(),
|
||||||
|
iwCycle.externalSourceEventId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deduplicatesRepresentationsSharingTheSameSourceRowIdentity() {
|
||||||
|
EventHubEventDto first = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_POSITION",
|
||||||
|
"TACHOGRAPH:CARD_POSITION:10",
|
||||||
|
EventDomain.POSITION,
|
||||||
|
EventType.POSITION_RECORDED,
|
||||||
|
EventLifecycle.SNAPSHOT,
|
||||||
|
"10",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto serializedCopy = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_POSITION",
|
||||||
|
"TACHOGRAPH:CARD_POSITION:COPY-10",
|
||||||
|
EventDomain.POSITION,
|
||||||
|
EventType.POSITION_RECORDED,
|
||||||
|
EventLifecycle.SNAPSHOT,
|
||||||
|
"10",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(service.aggregateRuntimeEvents(List.of(first, serializedCopy)))
|
||||||
|
.containsExactly(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collapsesPlaceStartAndBeginRepresentationsForTheSamePhysicalRecord() {
|
||||||
|
String rawRecordPath = "/DriverCard/Places[1]/cardPlaceDailyWorkPeriod/placeRecords[1]";
|
||||||
|
EventHubEventDto dbStyle = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"TACHOGRAPH:CARD_PLACE:10",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.START,
|
||||||
|
"CARDPLACE-1",
|
||||||
|
rawRecordPath,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto fileStyle = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:session-1:SUPPORT:CARDPLACE-1:BEGIN:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN,
|
||||||
|
"CARDPLACE-1",
|
||||||
|
rawRecordPath,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(service.aggregateRuntimeEvents(List.of(dbStyle, fileStyle)))
|
||||||
|
.containsExactly(dbStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsDistinctCardPlaceRecordsWhenGeneratedIdsRepeatAcrossSections() {
|
||||||
|
String repeatedExternalId = "TACHOGRAPH_FILE_SESSION:session-1:SUPPORT:CARDPLACE-1:BEGIN:2026-04-01T08:00:00Z";
|
||||||
|
EventHubEventDto firstSection = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
repeatedExternalId,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN,
|
||||||
|
"CARDPLACE-1",
|
||||||
|
"/DriverCard/Places[1]/cardPlaceDailyWorkPeriod/placeRecords[1]",
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto secondSection = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
repeatedExternalId,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN,
|
||||||
|
"CARDPLACE-1",
|
||||||
|
"/DriverCard/Places[2]/cardPlaceDailyWorkPeriod/placeRecords[1]",
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(service.aggregateRuntimeEvents(List.of(firstSection, secondSection)))
|
||||||
|
.containsExactly(firstSection, secondSection);
|
||||||
|
assertThat(service.exactSourceRecordKey(firstSection))
|
||||||
|
.isNotEqualTo(service.exactSourceRecordKey(secondSection));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsSemanticallyEqualRecordsWhenTheirPhysicalSourceIdentityDiffers() {
|
||||||
|
EventHubEventDto first = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:session-1:SUPPORT:CARDPLACE-1:BEGIN:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN,
|
||||||
|
"CARDPLACE-1",
|
||||||
|
"/DriverCard/Places[1]/cardPlaceDailyWorkPeriod/placeRecords[1]",
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto second = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_PLACE",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:session-1:SUPPORT:CARDPLACE-2:BEGIN:2026-04-01T08:00:00Z",
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
EventLifecycle.BEGIN,
|
||||||
|
"CARDPLACE-2",
|
||||||
|
"/DriverCard/Places[1]/cardPlaceDailyWorkPeriod/placeRecords[2]",
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(service.aggregateRuntimeEvents(List.of(first, second)))
|
||||||
|
.containsExactly(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsDifferentExtractionSourcesEvenWhenSemanticEventDataIsEqual() {
|
||||||
|
EventHubEventDto card = event(
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"CARD_ACTIVITY",
|
||||||
|
"TACHOGRAPH:CARD_ACTIVITY:10:START",
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventLifecycle.START,
|
||||||
|
"10",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
EventHubEventDto vu = event(
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"VU_ACTIVITY",
|
||||||
|
"TACHOGRAPH:VU_ACTIVITY:20:START",
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventLifecycle.START,
|
||||||
|
"20",
|
||||||
|
null,
|
||||||
|
OffsetDateTime.parse("2026-04-01T08:00:00Z")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(service.aggregateRuntimeEvents(List.of(card, vu)))
|
||||||
|
.containsExactly(card, vu);
|
||||||
|
assertThat(service.exactSourceRecordKey(card))
|
||||||
|
.isNotEqualTo(service.exactSourceRecordKey(vu));
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto event(
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
String externalSourceEventId,
|
||||||
|
EventDomain domain,
|
||||||
|
EventType eventType,
|
||||||
|
EventLifecycle lifecycle,
|
||||||
|
String sourceRowId,
|
||||||
|
String rawRecordPath,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
EventSourceDto source = new EventSourceDto(
|
||||||
|
"TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
"TACHOGRAPH_" + sourceKind,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
EventHubPackageRequest packageInfo = new EventHubPackageRequest(
|
||||||
|
"default",
|
||||||
|
source,
|
||||||
|
null,
|
||||||
|
ImportScopeDto.tenantAll(null, null),
|
||||||
|
domain.name(),
|
||||||
|
null,
|
||||||
|
"TACHOGRAPH:package"
|
||||||
|
);
|
||||||
|
ObjectNode raw = JsonNodeFactory.instance.objectNode();
|
||||||
|
raw.put("sourceKind", sourceKind);
|
||||||
|
raw.put("extractionCode", extractionCode);
|
||||||
|
raw.put("driverKey", "12:123");
|
||||||
|
raw.put("registrationKey", "12:REG-1");
|
||||||
|
if (sourceRowId != null) {
|
||||||
|
raw.put("sourceRowId", sourceRowId);
|
||||||
|
}
|
||||||
|
if (rawRecordPath != null) {
|
||||||
|
raw.put("rawRecordPath", rawRecordPath);
|
||||||
|
}
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
return new EventHubEventDto(
|
||||||
|
null,
|
||||||
|
externalSourceEventId,
|
||||||
|
new DriverRefDto("driver-1", new DriverCardRefDto("12", "123")),
|
||||||
|
new VehicleRefDto("vehicle-1", "VIN-1", new VehicleRegistrationRefDto("12", "REG-1")),
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
domain,
|
||||||
|
eventType,
|
||||||
|
lifecycle,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new EventDetailsDto(domain.name(), JsonNodeFactory.instance.objectNode()),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,8 +64,14 @@ class UnifiedRuntimeEventAssemblyServiceTest {
|
||||||
assertThat(bundle.discoveredVehicles()).extracting(UnifiedDiscoveredVehicleRef::stableKey)
|
assertThat(bundle.discoveredVehicles()).extracting(UnifiedDiscoveredVehicleRef::stableKey)
|
||||||
.containsExactly("SOURCE_VEHICLE:VEH-1", "VIN:VIN-2");
|
.containsExactly("SOURCE_VEHICLE:VEH-1", "VIN:VIN-2");
|
||||||
assertThat(bundle.expandedVehicleEvents()).hasSize(2);
|
assertThat(bundle.expandedVehicleEvents()).hasSize(2);
|
||||||
assertThat(bundle.mergedEvents()).hasSize(3);
|
assertThat(bundle.aggregatedEvents()).hasSize(3);
|
||||||
assertThat(bundle.mergedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
assertThat(bundle.aggregatedEventCount()).isEqualTo(3);
|
||||||
|
assertThat(bundle.mergedEvents()).isSameAs(bundle.aggregatedEvents());
|
||||||
|
assertThat(bundle.notes()).anySatisfy(note -> assertThat(note)
|
||||||
|
.contains("broad aggregated runtime scope")
|
||||||
|
.contains("mixing")
|
||||||
|
.contains("reconciliation"));
|
||||||
|
assertThat(bundle.aggregatedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
.containsExactly("SEED-1", "VEHICLE-EXPANDED", "SEED-2");
|
.containsExactly("SEED-1", "VEHICLE-EXPANDED", "SEED-2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +99,7 @@ class UnifiedRuntimeEventAssemblyServiceTest {
|
||||||
assertThat(bundle.driverSeedEvents()).hasSize(2);
|
assertThat(bundle.driverSeedEvents()).hasSize(2);
|
||||||
assertThat(bundle.discoveredVehicles()).hasSize(2);
|
assertThat(bundle.discoveredVehicles()).hasSize(2);
|
||||||
assertThat(bundle.expandedVehicleEvents()).isEmpty();
|
assertThat(bundle.expandedVehicleEvents()).isEmpty();
|
||||||
assertThat(bundle.mergedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
assertThat(bundle.aggregatedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
.containsExactly("SEED-1", "SEED-2");
|
.containsExactly("SEED-1", "SEED-2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,10 +125,10 @@ class UnifiedRuntimeEventAssemblyServiceTest {
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(bundle.driverSeedEvents()).hasSize(1);
|
assertThat(bundle.driverSeedEvents()).hasSize(1);
|
||||||
assertThat(bundle.mergedEvents()).hasSize(1);
|
assertThat(bundle.aggregatedEvents()).hasSize(1);
|
||||||
assertThat(bundle.mergedEvents().get(0).occurredAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:00:00Z"));
|
assertThat(bundle.aggregatedEvents().get(0).occurredAt()).isEqualTo(OffsetDateTime.parse("2026-05-01T08:00:00Z"));
|
||||||
assertThat(bundle.mergedEvents().get(0).eventType()).isEqualTo(EventType.DRIVE);
|
assertThat(bundle.aggregatedEvents().get(0).eventType()).isEqualTo(EventType.DRIVE);
|
||||||
assertThat(bundle.mergedEvents().get(0).lifecycle()).isEqualTo(EventLifecycle.START);
|
assertThat(bundle.aggregatedEvents().get(0).lifecycle()).isEqualTo(EventLifecycle.START);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -166,8 +172,8 @@ class UnifiedRuntimeEventAssemblyServiceTest {
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(bundle.driverSeedEvents()).hasSize(3);
|
assertThat(bundle.driverSeedEvents()).hasSize(3);
|
||||||
assertThat(bundle.mergedEvents()).hasSize(3);
|
assertThat(bundle.aggregatedEvents()).hasSize(3);
|
||||||
assertThat(bundle.mergedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
assertThat(bundle.aggregatedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
.containsExactly("FILE-SESSION-1", "TACHO-DB-1", "YF-DB-1");
|
.containsExactly("FILE-SESSION-1", "TACHO-DB-1", "YF-DB-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,8 +237,8 @@ class UnifiedRuntimeEventAssemblyServiceTest {
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(bundle.driverSeedEvents()).hasSize(3);
|
assertThat(bundle.driverSeedEvents()).hasSize(3);
|
||||||
assertThat(bundle.mergedEvents()).hasSize(3);
|
assertThat(bundle.aggregatedEvents()).hasSize(3);
|
||||||
assertThat(bundle.mergedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
assertThat(bundle.aggregatedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
.containsExactly("FILE-SESSION-1", "TACHO-ACT-1", "YF-DB-1");
|
.containsExactly("FILE-SESSION-1", "TACHO-ACT-1", "YF-DB-1");
|
||||||
assertThat(bundle.notes()).anySatisfy(note -> assertThat(note).contains("mixed runtime scope"));
|
assertThat(bundle.notes()).anySatisfy(note -> assertThat(note).contains("mixed runtime scope"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue