Add in-memory tachograph file session flow
This commit is contained in:
parent
d5a6254a33
commit
5a1061a314
|
|
@ -30,6 +30,7 @@ public class EventHubProperties {
|
||||||
|
|
||||||
private final Batch batch = new Batch();
|
private final Batch batch = new Batch();
|
||||||
private final Tachograph tachograph = new Tachograph();
|
private final Tachograph tachograph = new Tachograph();
|
||||||
|
private final TachographFileSession tachographFileSession = new TachographFileSession();
|
||||||
private final EsperPoc esperPoc = new EsperPoc();
|
private final EsperPoc esperPoc = new EsperPoc();
|
||||||
private final YellowFox yellowFox = new YellowFox();
|
private final YellowFox yellowFox = new YellowFox();
|
||||||
|
|
||||||
|
|
@ -41,6 +42,10 @@ public class EventHubProperties {
|
||||||
return tachograph;
|
return tachograph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TachographFileSession getTachographFileSession() {
|
||||||
|
return tachographFileSession;
|
||||||
|
}
|
||||||
|
|
||||||
public EsperPoc getEsperPoc() {
|
public EsperPoc getEsperPoc() {
|
||||||
return esperPoc;
|
return esperPoc;
|
||||||
}
|
}
|
||||||
|
|
@ -308,6 +313,104 @@ public class EventHubProperties {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TachographFileSession {
|
||||||
|
private Duration ttl = Duration.ofHours(4);
|
||||||
|
private int maxSessions = 100;
|
||||||
|
private long maxFileSizeBytes = 20L * 1024L * 1024L;
|
||||||
|
private final LegalRequirements legalRequirements = new LegalRequirements();
|
||||||
|
|
||||||
|
public Duration getTtl() {
|
||||||
|
return ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTtl(Duration ttl) {
|
||||||
|
if (ttl != null && !ttl.isNegative() && !ttl.isZero()) {
|
||||||
|
this.ttl = ttl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSessions() {
|
||||||
|
return maxSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxSessions(int maxSessions) {
|
||||||
|
this.maxSessions = Math.max(1, maxSessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxFileSizeBytes() {
|
||||||
|
return maxFileSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxFileSizeBytes(long maxFileSizeBytes) {
|
||||||
|
this.maxFileSizeBytes = Math.max(1024L, maxFileSizeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegalRequirements getLegalRequirements() {
|
||||||
|
return legalRequirements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LegalRequirements {
|
||||||
|
private String baseUrl = "https://legalrequirements.services.bytebar.eu/ODataV4/LR";
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private Duration connectTimeout = Duration.ofSeconds(20);
|
||||||
|
private Duration readTimeout = Duration.ofMinutes(2);
|
||||||
|
private boolean resetSessionAfterUse = true;
|
||||||
|
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBaseUrl(String baseUrl) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getConnectTimeout() {
|
||||||
|
return connectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectTimeout(Duration connectTimeout) {
|
||||||
|
if (connectTimeout != null && !connectTimeout.isNegative() && !connectTimeout.isZero()) {
|
||||||
|
this.connectTimeout = connectTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getReadTimeout() {
|
||||||
|
return readTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadTimeout(Duration readTimeout) {
|
||||||
|
if (readTimeout != null && !readTimeout.isNegative() && !readTimeout.isZero()) {
|
||||||
|
this.readTimeout = readTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isResetSessionAfterUse() {
|
||||||
|
return resetSessionAfterUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResetSessionAfterUse(boolean resetSessionAfterUse) {
|
||||||
|
this.resetSessionAfterUse = resetSessionAfterUse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class TachographDataSource {
|
public static class TachographDataSource {
|
||||||
private String jdbcUrl;
|
private String jdbcUrl;
|
||||||
private String username;
|
private String username;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.api;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionDeleteResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDriversResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/eventhub/tachograph-file-sessions")
|
||||||
|
public class TachographFileSessionController {
|
||||||
|
|
||||||
|
private final TachographFileSessionService service;
|
||||||
|
|
||||||
|
public TachographFileSessionController(TachographFileSessionService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<CreateTachographFileSessionResponse> createSession(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam(required = false) String tenantKey,
|
||||||
|
@RequestParam(required = false) String sourceInstanceKey,
|
||||||
|
@RequestParam(required = false) String sessionLabel
|
||||||
|
) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED)
|
||||||
|
.body(service.createSession(file, tenantKey, sourceInstanceKey, sessionLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{sessionId}")
|
||||||
|
public ResponseEntity<TachographFileSessionSummaryDto> getSession(@PathVariable UUID sessionId) {
|
||||||
|
return ResponseEntity.ok(service.getSession(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{sessionId}/drivers")
|
||||||
|
public ResponseEntity<TachographFileSessionListDriversResponse> listDrivers(@PathVariable UUID sessionId) {
|
||||||
|
return ResponseEntity.ok(service.listDrivers(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{sessionId}/drivers/{driverKey}")
|
||||||
|
public ResponseEntity<TachographFileDriverDetailDto> getDriver(
|
||||||
|
@PathVariable UUID sessionId,
|
||||||
|
@PathVariable String driverKey
|
||||||
|
) {
|
||||||
|
return ResponseEntity.ok(service.getDriver(sessionId, driverKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{sessionId}")
|
||||||
|
public ResponseEntity<TachographFileSessionDeleteResponse> deleteSession(@PathVariable UUID sessionId) {
|
||||||
|
return ResponseEntity.ok(service.deleteSession(sessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.api;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.DriverNotFoundInSessionException;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.LegalRequirementsUploadException;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.LegalRequirementsXmlDownloadException;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionNotFoundException;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.TachographXmlValidationException;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.UnsupportedTachographFileTypeException;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice(basePackageClasses = TachographFileSessionController.class)
|
||||||
|
public class TachographFileSessionExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler({
|
||||||
|
TachographFileSessionNotFoundException.class,
|
||||||
|
DriverNotFoundInSessionException.class
|
||||||
|
})
|
||||||
|
public ResponseEntity<Map<String, Object>> notFound(RuntimeException exception) {
|
||||||
|
return error(HttpStatus.NOT_FOUND, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
TachographXmlValidationException.class,
|
||||||
|
UnsupportedTachographFileTypeException.class
|
||||||
|
})
|
||||||
|
public ResponseEntity<Map<String, Object>> badRequest(RuntimeException exception) {
|
||||||
|
return error(HttpStatus.BAD_REQUEST, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler({
|
||||||
|
LegalRequirementsUploadException.class,
|
||||||
|
LegalRequirementsXmlDownloadException.class
|
||||||
|
})
|
||||||
|
public ResponseEntity<Map<String, Object>> badGateway(RuntimeException exception) {
|
||||||
|
return error(HttpStatus.BAD_GATEWAY, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, Object>> error(HttpStatus status, RuntimeException exception) {
|
||||||
|
return ResponseEntity.status(status).body(Map.of(
|
||||||
|
"timestamp", OffsetDateTime.now().toString(),
|
||||||
|
"status", status.value(),
|
||||||
|
"error", status.getReasonPhrase(),
|
||||||
|
"message", exception.getMessage()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.dto;
|
||||||
|
|
||||||
|
public record CreateTachographFileSessionResponse(
|
||||||
|
TachographFileSessionSummaryDto session
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.dto;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record TachographFileDriverDetailDto(
|
||||||
|
UUID sessionId,
|
||||||
|
String driverKey,
|
||||||
|
ExtractedDriver driver,
|
||||||
|
ExtractedDriverCard driverCard,
|
||||||
|
List<ExtractedVehicleRegistration> vehicleRegistrations,
|
||||||
|
List<ExtractedVehicle> vehicles,
|
||||||
|
List<ExtractedCardVehicleUsageInterval> cardVehicleUsageIntervals,
|
||||||
|
List<ExtractedCardActivityInterval> cardActivityIntervals,
|
||||||
|
List<ExtractionWarning> warnings
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.dto;
|
||||||
|
|
||||||
|
public record TachographFileDriverSummaryDto(
|
||||||
|
String driverKey,
|
||||||
|
String surname,
|
||||||
|
String firstNames,
|
||||||
|
String cardNation,
|
||||||
|
String cardNumber,
|
||||||
|
int activityIntervalCount,
|
||||||
|
int cardVehicleUsageIntervalCount
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.dto;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record TachographFileSessionDeleteResponse(
|
||||||
|
UUID sessionId,
|
||||||
|
boolean deleted
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record TachographFileSessionListDriversResponse(
|
||||||
|
UUID sessionId,
|
||||||
|
List<TachographFileDriverSummaryDto> drivers
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.dto;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record TachographFileSessionSummaryDto(
|
||||||
|
UUID sessionId,
|
||||||
|
String tenantKey,
|
||||||
|
String sourceInstanceKey,
|
||||||
|
String sessionLabel,
|
||||||
|
String originalFileName,
|
||||||
|
boolean driverCardFile,
|
||||||
|
String legalRequirementsDataPackageId,
|
||||||
|
ExtractionStats stats,
|
||||||
|
List<TachographFileDriverSummaryDto> drivers,
|
||||||
|
List<ExtractionWarning> warnings,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant expiresAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record DriverExtractionSession(
|
||||||
|
String driverKey,
|
||||||
|
ExtractedDriver driver,
|
||||||
|
ExtractedDriverCard driverCard,
|
||||||
|
List<ExtractedVehicleRegistration> vehicleRegistrations,
|
||||||
|
List<ExtractedVehicle> vehicles,
|
||||||
|
List<ExtractedCardVehicleUsageInterval> cardVehicleUsageIntervals,
|
||||||
|
List<ExtractedCardActivityInterval> cardActivityIntervals,
|
||||||
|
List<ExtractionWarning> warnings
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
public record ExtractedCardActivityInterval(
|
||||||
|
String intervalId,
|
||||||
|
OffsetDateTime from,
|
||||||
|
OffsetDateTime to,
|
||||||
|
String activityType,
|
||||||
|
String slot,
|
||||||
|
String cardStatus,
|
||||||
|
String drivingStatus,
|
||||||
|
String registrationKey,
|
||||||
|
String vehicleKey,
|
||||||
|
String rawRecordPath
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
public record ExtractedCardVehicleUsageInterval(
|
||||||
|
String intervalId,
|
||||||
|
OffsetDateTime from,
|
||||||
|
OffsetDateTime to,
|
||||||
|
Long odometerBeginKm,
|
||||||
|
Long odometerEndKm,
|
||||||
|
String registrationKey,
|
||||||
|
String vehicleKey,
|
||||||
|
String rawRecordPath
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
public record ExtractedDriver(
|
||||||
|
String driverKey,
|
||||||
|
String sourceDriverId,
|
||||||
|
String surname,
|
||||||
|
String firstNames,
|
||||||
|
LocalDate birthDate,
|
||||||
|
String preferredLanguage,
|
||||||
|
String drivingLicenceNumber,
|
||||||
|
String drivingLicenceNation,
|
||||||
|
String drivingLicenceAuthority
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
public record ExtractedDriverCard(
|
||||||
|
String sourceDriverCardId,
|
||||||
|
String cardNation,
|
||||||
|
String cardNumber,
|
||||||
|
String issuingAuthorityName,
|
||||||
|
OffsetDateTime issueDate,
|
||||||
|
OffsetDateTime validityBegin,
|
||||||
|
OffsetDateTime expiryDate
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
public record ExtractedVehicle(
|
||||||
|
String vehicleKey,
|
||||||
|
String sourceVehicleId,
|
||||||
|
String vin
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
public record ExtractedVehicleRegistration(
|
||||||
|
String registrationKey,
|
||||||
|
String sourceVehicleRegistrationId,
|
||||||
|
String registrationNation,
|
||||||
|
String registrationNumber
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
public record ExtractionStats(
|
||||||
|
int driverCount,
|
||||||
|
int activityIntervalCount,
|
||||||
|
int cardVehicleUsageIntervalCount,
|
||||||
|
int vehicleRegistrationCount,
|
||||||
|
int vehicleCount,
|
||||||
|
int warningCount
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
public record ExtractionWarning(
|
||||||
|
String code,
|
||||||
|
String message,
|
||||||
|
String rawRecordPath
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record TachographFileSession(
|
||||||
|
UUID sessionId,
|
||||||
|
TachographFileSessionMetadata metadata,
|
||||||
|
Map<String, DriverExtractionSession> driversByKey,
|
||||||
|
ExtractionStats extractionStats,
|
||||||
|
java.util.List<ExtractionWarning> warnings,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant expiresAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.model;
|
||||||
|
|
||||||
|
public record TachographFileSessionMetadata(
|
||||||
|
String tenantKey,
|
||||||
|
String sourceInstanceKey,
|
||||||
|
String sessionLabel,
|
||||||
|
String originalFileName,
|
||||||
|
String uploadedFileSha256,
|
||||||
|
long uploadedFileSize,
|
||||||
|
String legalRequirementsDataPackageId,
|
||||||
|
String xmlSha256,
|
||||||
|
boolean driverCardFile,
|
||||||
|
String xmlGeneration
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardActivityInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedCardVehicleUsageInterval;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriver;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedDriverCard;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicle;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractedVehicleRegistration;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionWarning;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.xml.xpath.XPath;
|
||||||
|
import javax.xml.xpath.XPathConstants;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
import javax.xml.xpath.XPathFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DriverCardXmlExtractionService {
|
||||||
|
|
||||||
|
private final DriverKeyFactory driverKeyFactory;
|
||||||
|
private final VehicleKeyFactory vehicleKeyFactory;
|
||||||
|
|
||||||
|
public DriverCardXmlExtractionService(DriverKeyFactory driverKeyFactory, VehicleKeyFactory vehicleKeyFactory) {
|
||||||
|
this.driverKeyFactory = driverKeyFactory;
|
||||||
|
this.vehicleKeyFactory = vehicleKeyFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographFileSession extract(
|
||||||
|
TachographXmlParser.ParsedTachographXml parsedXml,
|
||||||
|
TachographFileSessionMetadata metadata,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant expiresAt
|
||||||
|
) {
|
||||||
|
Document document = parsedXml.document();
|
||||||
|
List<ExtractionWarning> warnings = new ArrayList<>();
|
||||||
|
|
||||||
|
ExtractedDriverCard driverCard = extractDriverCard(document, warnings);
|
||||||
|
if (driverCard == null || driverCard.cardNumber() == null) {
|
||||||
|
throw new TachographXmlValidationException("Driver card identification could not be extracted from XML.");
|
||||||
|
}
|
||||||
|
String driverKey = driverKeyFactory.createDriverKey(driverCard.cardNation(), driverCard.cardNumber());
|
||||||
|
driverCard = new ExtractedDriverCard(
|
||||||
|
driverKeyFactory.createSourceDriverCardId(driverKey),
|
||||||
|
driverCard.cardNation(),
|
||||||
|
driverCard.cardNumber(),
|
||||||
|
driverCard.issuingAuthorityName(),
|
||||||
|
driverCard.issueDate(),
|
||||||
|
driverCard.validityBegin(),
|
||||||
|
driverCard.expiryDate()
|
||||||
|
);
|
||||||
|
ExtractedDriver driver = extractDriver(document, driverKey, warnings);
|
||||||
|
|
||||||
|
LinkedHashMap<String, ExtractedVehicleRegistration> registrationsByKey = new LinkedHashMap<>();
|
||||||
|
LinkedHashMap<String, ExtractedVehicle> vehiclesByKey = new LinkedHashMap<>();
|
||||||
|
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals =
|
||||||
|
extractVehicleUsageIntervals(document, registrationsByKey, vehiclesByKey, warnings);
|
||||||
|
List<ExtractedCardActivityInterval> activityIntervals =
|
||||||
|
assignVehicleCoverage(extractActivityIntervals(document, warnings), vehicleUsageIntervals);
|
||||||
|
|
||||||
|
DriverExtractionSession driverSession = new DriverExtractionSession(
|
||||||
|
driverKey,
|
||||||
|
driver,
|
||||||
|
driverCard,
|
||||||
|
List.copyOf(registrationsByKey.values()),
|
||||||
|
List.copyOf(vehiclesByKey.values()),
|
||||||
|
List.copyOf(vehicleUsageIntervals),
|
||||||
|
List.copyOf(activityIntervals),
|
||||||
|
List.copyOf(warnings)
|
||||||
|
);
|
||||||
|
Map<String, DriverExtractionSession> driversByKey = Map.of(driverKey, driverSession);
|
||||||
|
ExtractionStats stats = new ExtractionStats(
|
||||||
|
1,
|
||||||
|
activityIntervals.size(),
|
||||||
|
vehicleUsageIntervals.size(),
|
||||||
|
registrationsByKey.size(),
|
||||||
|
vehiclesByKey.size(),
|
||||||
|
warnings.size()
|
||||||
|
);
|
||||||
|
return new TachographFileSession(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
metadata,
|
||||||
|
driversByKey,
|
||||||
|
stats,
|
||||||
|
List.copyOf(warnings),
|
||||||
|
createdAt,
|
||||||
|
expiresAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtractedDriverCard extractDriverCard(Document document, List<ExtractionWarning> warnings) {
|
||||||
|
Element identification = firstElement(document, "/DriverCard/Identification[1]");
|
||||||
|
if (identification == null) {
|
||||||
|
warnings.add(new ExtractionWarning("MISSING_IDENTIFICATION", "Driver card identification block is missing.", "/DriverCard"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String cardNation = text(identification, "cardIdentification/cardIssuingMemberState");
|
||||||
|
String cardNumber = joinCardNumber(identification);
|
||||||
|
String authority = text(identification, "cardIdentification/cardIssuingAuthorityName/name");
|
||||||
|
return new ExtractedDriverCard(
|
||||||
|
null,
|
||||||
|
cardNation,
|
||||||
|
cardNumber,
|
||||||
|
authority,
|
||||||
|
offsetDateTime(text(identification, "cardIdentification/cardIssueDate")),
|
||||||
|
offsetDateTime(text(identification, "cardIdentification/cardValidityBegin")),
|
||||||
|
offsetDateTime(text(identification, "cardIdentification/cardExpiryDate"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtractedDriver extractDriver(Document document, String driverKey, List<ExtractionWarning> warnings) {
|
||||||
|
Element identification = firstElement(document, "/DriverCard/Identification[1]");
|
||||||
|
if (identification == null) {
|
||||||
|
warnings.add(new ExtractionWarning("MISSING_DRIVER", "Driver holder identification block is missing.", "/DriverCard"));
|
||||||
|
return new ExtractedDriver(
|
||||||
|
driverKey,
|
||||||
|
driverKeyFactory.createSourceDriverId(driverKey),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Element licenceInfo = firstElement(document, "/DriverCard/DrivingLicenseInfo[1]");
|
||||||
|
return new ExtractedDriver(
|
||||||
|
driverKey,
|
||||||
|
driverKeyFactory.createSourceDriverId(driverKey),
|
||||||
|
text(identification, "driverCardHolderIdentification/cardHolderName/holderSurname/name"),
|
||||||
|
text(identification, "driverCardHolderIdentification/cardHolderName/holderFirstNames/name"),
|
||||||
|
localDate(identification, "driverCardHolderIdentification/cardHolderBirthDate"),
|
||||||
|
text(identification, "driverCardHolderIdentification/cardHolderPreferredLanguage"),
|
||||||
|
text(licenceInfo, "cardDrivingLicenseInformation/drivingLicenceNumber"),
|
||||||
|
text(licenceInfo, "cardDrivingLicenseInformation/drivingLicenceIssuingNation"),
|
||||||
|
text(licenceInfo, "cardDrivingLicenseInformation/drivingLicenceIssuingAuthority/name")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ExtractedCardVehicleUsageInterval> extractVehicleUsageIntervals(
|
||||||
|
Document document,
|
||||||
|
Map<String, ExtractedVehicleRegistration> registrationsByKey,
|
||||||
|
Map<String, ExtractedVehicle> vehiclesByKey,
|
||||||
|
List<ExtractionWarning> warnings
|
||||||
|
) {
|
||||||
|
NodeList records = nodes(document, "/DriverCard/VehiclesUsed/cardVehiclesUsed/cardVehicleRecords");
|
||||||
|
List<ExtractedCardVehicleUsageInterval> intervals = new ArrayList<>();
|
||||||
|
for (int i = 0; i < records.getLength(); i++) {
|
||||||
|
Element record = (Element) records.item(i);
|
||||||
|
String path = "/DriverCard/VehiclesUsed/cardVehiclesUsed/cardVehicleRecords[" + (i + 1) + "]";
|
||||||
|
OffsetDateTime from = offsetDateTime(text(record, "vehicleFirstUse"));
|
||||||
|
OffsetDateTime to = offsetDateTime(text(record, "vehicleLastUse"));
|
||||||
|
String registrationNation = text(record, "vehicleRegistration/vehicleRegistrationNation");
|
||||||
|
String registrationNumber = text(record, "vehicleRegistration/vehicleRegistrationNumber/vehicleRegNumber");
|
||||||
|
String registrationKey = vehicleKeyFactory.createRegistrationKey(registrationNation, registrationNumber);
|
||||||
|
registrationsByKey.putIfAbsent(
|
||||||
|
registrationKey,
|
||||||
|
new ExtractedVehicleRegistration(
|
||||||
|
registrationKey,
|
||||||
|
vehicleKeyFactory.createSourceVehicleRegistrationId(registrationKey),
|
||||||
|
registrationNation,
|
||||||
|
registrationNumber
|
||||||
|
)
|
||||||
|
);
|
||||||
|
String vin = text(record, "vehicleIdentificationNumber");
|
||||||
|
String vehicleKey = vehicleKeyFactory.createVehicleKey(vin);
|
||||||
|
if (vehicleKey != null) {
|
||||||
|
vehiclesByKey.putIfAbsent(
|
||||||
|
vehicleKey,
|
||||||
|
new ExtractedVehicle(vehicleKey, vehicleKeyFactory.createSourceVehicleId(vehicleKey), vin)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (from == null || to == null) {
|
||||||
|
warnings.add(new ExtractionWarning("INCOMPLETE_VEHICLE_USAGE", "Vehicle usage interval is missing start or end timestamp.", path));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
intervals.add(new ExtractedCardVehicleUsageInterval(
|
||||||
|
"CVU-" + (i + 1),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
longValue(text(record, "vehicleOdometerBegin")),
|
||||||
|
longValue(text(record, "vehicleOdometerEnd")),
|
||||||
|
registrationKey,
|
||||||
|
vehicleKey,
|
||||||
|
path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
intervals.sort(Comparator.comparing(ExtractedCardVehicleUsageInterval::from));
|
||||||
|
return intervals;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ExtractedCardActivityInterval> extractActivityIntervals(Document document, List<ExtractionWarning> warnings) {
|
||||||
|
NodeList dayRecords = nodes(document, "/DriverCard/DriverActivityData/cardDriverActivity/cardActivityDailyRecord");
|
||||||
|
List<ExtractedCardActivityInterval> intervals = new ArrayList<>();
|
||||||
|
int intervalNo = 0;
|
||||||
|
for (int dayIndex = 0; dayIndex < dayRecords.getLength(); dayIndex++) {
|
||||||
|
Element dayRecord = (Element) dayRecords.item(dayIndex);
|
||||||
|
String dayPath = "/DriverCard/DriverActivityData/cardDriverActivity/cardActivityDailyRecord[" + (dayIndex + 1) + "]";
|
||||||
|
OffsetDateTime recordDate = offsetDateTime(text(dayRecord, "activityRecordDate"));
|
||||||
|
if (recordDate == null) {
|
||||||
|
warnings.add(new ExtractionWarning("MISSING_ACTIVITY_RECORD_DATE", "Activity daily record has no activityRecordDate.", dayPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LocalDate date = recordDate.withOffsetSameInstant(ZoneOffset.UTC).toLocalDate();
|
||||||
|
NodeList changes = nodes(dayRecord, "activityChangeInfos");
|
||||||
|
List<ActivityChange> parsedChanges = new ArrayList<>();
|
||||||
|
for (int changeIndex = 0; changeIndex < changes.getLength(); changeIndex++) {
|
||||||
|
Element change = (Element) changes.item(changeIndex);
|
||||||
|
OffsetDateTime from = combine(date, text(change, "timeOfChange"));
|
||||||
|
if (from == null) {
|
||||||
|
warnings.add(new ExtractionWarning("INVALID_ACTIVITY_CHANGE_TIME", "Activity change has invalid timeOfChange.", dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parsedChanges.add(new ActivityChange(
|
||||||
|
from,
|
||||||
|
normalizeActivity(text(change, "activity")),
|
||||||
|
normalizeToken(text(change, "slot")),
|
||||||
|
normalizeToken(text(change, "cardStatus")),
|
||||||
|
normalizeToken(text(change, "drivingStatus")),
|
||||||
|
dayPath + "/activityChangeInfos[" + (changeIndex + 1) + "]"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
parsedChanges.sort(Comparator.comparing(ActivityChange::from));
|
||||||
|
for (int i = 0; i < parsedChanges.size(); i++) {
|
||||||
|
ActivityChange current = parsedChanges.get(i);
|
||||||
|
OffsetDateTime to = i + 1 < parsedChanges.size()
|
||||||
|
? parsedChanges.get(i + 1).from()
|
||||||
|
: date.plusDays(1).atStartOfDay().atOffset(ZoneOffset.UTC);
|
||||||
|
if (!current.from().isBefore(to)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
intervalNo++;
|
||||||
|
intervals.add(new ExtractedCardActivityInterval(
|
||||||
|
"ACT-" + intervalNo,
|
||||||
|
current.from(),
|
||||||
|
to,
|
||||||
|
current.activityType(),
|
||||||
|
current.slot(),
|
||||||
|
current.cardStatus(),
|
||||||
|
current.drivingStatus(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
current.rawRecordPath()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intervals.sort(Comparator.comparing(ExtractedCardActivityInterval::from));
|
||||||
|
return intervals;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ExtractedCardActivityInterval> assignVehicleCoverage(
|
||||||
|
List<ExtractedCardActivityInterval> activityIntervals,
|
||||||
|
List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals
|
||||||
|
) {
|
||||||
|
List<ExtractedCardActivityInterval> result = new ArrayList<>(activityIntervals.size());
|
||||||
|
for (ExtractedCardActivityInterval interval : activityIntervals) {
|
||||||
|
ExtractedCardVehicleUsageInterval covering = vehicleUsageIntervals.stream()
|
||||||
|
.filter(usage -> !usage.from().isAfter(interval.from()) && !usage.to().isBefore(interval.from()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
result.add(new ExtractedCardActivityInterval(
|
||||||
|
interval.intervalId(),
|
||||||
|
interval.from(),
|
||||||
|
interval.to(),
|
||||||
|
interval.activityType(),
|
||||||
|
interval.slot(),
|
||||||
|
interval.cardStatus(),
|
||||||
|
interval.drivingStatus(),
|
||||||
|
covering == null ? null : covering.registrationKey(),
|
||||||
|
covering == null ? null : covering.vehicleKey(),
|
||||||
|
interval.rawRecordPath()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Element firstElement(Object node, String expression) {
|
||||||
|
NodeList nodes = nodes(node, expression);
|
||||||
|
if (nodes.getLength() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (Element) nodes.item(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeList nodes(Object node, String expression) {
|
||||||
|
try {
|
||||||
|
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||||
|
return (NodeList) xpath.evaluate(expression, node, XPathConstants.NODESET);
|
||||||
|
} catch (XPathExpressionException e) {
|
||||||
|
throw new IllegalStateException("Invalid XPath expression: " + expression, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(Object node, String expression) {
|
||||||
|
try {
|
||||||
|
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||||
|
String value = xpath.evaluate(expression, node);
|
||||||
|
return value == null || value.isBlank() ? null : value.trim();
|
||||||
|
} catch (XPathExpressionException e) {
|
||||||
|
throw new IllegalStateException("Invalid XPath expression: " + expression, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime offsetDateTime(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return OffsetDateTime.parse(value.trim()).withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate localDate(Element element, String expression) {
|
||||||
|
Element dateElement = firstElement(element, expression);
|
||||||
|
if (dateElement == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String year = text(dateElement, "year");
|
||||||
|
String month = text(dateElement, "month");
|
||||||
|
String day = text(dateElement, "day");
|
||||||
|
if (year == null || month == null || day == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocalDate.of(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day));
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime combine(LocalDate date, String timeText) {
|
||||||
|
if (date == null || timeText == null || timeText.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
LocalTime time = java.time.OffsetTime.parse(timeText.trim()).withOffsetSameInstant(ZoneOffset.UTC).toLocalTime();
|
||||||
|
return date.atTime(time).atOffset(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long longValue(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Long.parseLong(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeActivity(String value) {
|
||||||
|
String normalized = normalizeToken(value);
|
||||||
|
if (normalized == null) {
|
||||||
|
return "UNKNOWN_ACTIVITY";
|
||||||
|
}
|
||||||
|
return switch (normalized) {
|
||||||
|
case "DRIVING", "DRIVE" -> "DRIVE";
|
||||||
|
case "WORK" -> "WORK";
|
||||||
|
case "AVAILABILITY", "AVAILABLE" -> "AVAILABILITY";
|
||||||
|
case "BREAK_REST", "BREAK/REST", "REST" -> "BREAK_REST";
|
||||||
|
default -> "UNKNOWN_ACTIVITY";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeToken(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim().toUpperCase().replace('-', '_').replace(' ', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ActivityChange(
|
||||||
|
OffsetDateTime from,
|
||||||
|
String activityType,
|
||||||
|
String slot,
|
||||||
|
String cardStatus,
|
||||||
|
String drivingStatus,
|
||||||
|
String rawRecordPath
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinCardNumber(Element identification) {
|
||||||
|
String driverIdentification = text(identification, "cardIdentification/cardNumber/driverIdentification");
|
||||||
|
if (driverIdentification == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String replacement = text(identification, "cardIdentification/cardNumber/cardReplacementIndex");
|
||||||
|
String renewal = text(identification, "cardIdentification/cardNumber/cardRenewalIndex");
|
||||||
|
StringBuilder builder = new StringBuilder(driverIdentification);
|
||||||
|
if (replacement != null) {
|
||||||
|
builder.append(replacement);
|
||||||
|
}
|
||||||
|
if (renewal != null) {
|
||||||
|
builder.append(renewal);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DriverKeyFactory {
|
||||||
|
|
||||||
|
public String createDriverKey(String cardNation, String cardNumber) {
|
||||||
|
String normalizedNation = normalize(cardNation, "UNKNOWN");
|
||||||
|
String normalizedCardNumber = normalize(cardNumber, "UNKNOWN");
|
||||||
|
return normalizedNation + ":" + normalizedCardNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createSourceDriverId(String driverKey) {
|
||||||
|
return "DRV:" + driverKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createSourceDriverCardId(String driverKey) {
|
||||||
|
return "CARD:" + driverKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value, String fallback) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class DriverNotFoundInSessionException extends RuntimeException {
|
||||||
|
|
||||||
|
public DriverNotFoundInSessionException(UUID sessionId, String driverKey) {
|
||||||
|
super("Driver not found in session " + sessionId + ": " + driverKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class InMemoryTachographFileSessionRepository implements TachographFileSessionRepository {
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<UUID, TachographFileSession> sessions = new ConcurrentHashMap<>();
|
||||||
|
private final EventHubProperties properties;
|
||||||
|
|
||||||
|
public InMemoryTachographFileSessionRepository(EventHubProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TachographFileSession save(TachographFileSession session) {
|
||||||
|
cleanupExpired();
|
||||||
|
if (!sessions.containsKey(session.sessionId())
|
||||||
|
&& sessions.size() >= properties.getTachographFileSession().getMaxSessions()) {
|
||||||
|
throw new IllegalStateException("Maximum number of tachograph file sessions reached.");
|
||||||
|
}
|
||||||
|
sessions.put(session.sessionId(), session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<TachographFileSession> find(UUID sessionId) {
|
||||||
|
cleanupExpired();
|
||||||
|
return Optional.ofNullable(sessions.get(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean delete(UUID sessionId) {
|
||||||
|
cleanupExpired();
|
||||||
|
return sessions.remove(sessionId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<TachographFileSession> list() {
|
||||||
|
cleanupExpired();
|
||||||
|
return List.copyOf(sessions.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupExpired() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
sessions.entrySet().removeIf(entry -> entry.getValue().expiresAt() != null && entry.getValue().expiresAt().isBefore(now));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.CookiePolicy;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class LegalRequirementsClient {
|
||||||
|
|
||||||
|
private final EventHubProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public LegalRequirementsClient(EventHubProperties properties, ObjectMapper objectMapper) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegalRequirementsUploadResult uploadDriverCard(byte[] fileBytes, String fileName) {
|
||||||
|
EventHubProperties.LegalRequirements config = properties.getTachographFileSession().getLegalRequirements();
|
||||||
|
HttpClient client = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(config.getConnectTimeout())
|
||||||
|
.cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ALL))
|
||||||
|
.build();
|
||||||
|
try {
|
||||||
|
String dataPackageId = uploadDataPackage(client, config, fileBytes, fileName);
|
||||||
|
String xml = downloadXml(client, config, dataPackageId);
|
||||||
|
return new LegalRequirementsUploadResult(dataPackageId, xml);
|
||||||
|
} finally {
|
||||||
|
if (config.isResetSessionAfterUse()) {
|
||||||
|
resetSessionQuietly(client, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String uploadDataPackage(HttpClient client, EventHubProperties.LegalRequirements config, byte[] fileBytes, String fileName) {
|
||||||
|
try {
|
||||||
|
String payload = objectMapper.createObjectNode()
|
||||||
|
.put("FileName", fileName)
|
||||||
|
.putNull("CompressionMediaType")
|
||||||
|
.put("Data", Base64.getEncoder().encodeToString(fileBytes))
|
||||||
|
.toString();
|
||||||
|
HttpRequest request = requestBuilder(config.getBaseUrl() + "/DataPackages/Upload", config.getReadTimeout())
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", basicAuth(config))
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8))
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
if (response.statusCode() / 100 != 2) {
|
||||||
|
throw new LegalRequirementsUploadException("LegalRequirements upload failed with status " + response.statusCode());
|
||||||
|
}
|
||||||
|
JsonNode root = objectMapper.readTree(response.body());
|
||||||
|
String dataPackageId = text(root, "DataPackageID");
|
||||||
|
if (dataPackageId == null) {
|
||||||
|
dataPackageId = text(root, "DataPackageId");
|
||||||
|
}
|
||||||
|
if (dataPackageId == null && root.has("value") && root.get("value").isObject()) {
|
||||||
|
dataPackageId = text(root.get("value"), "DataPackageID");
|
||||||
|
}
|
||||||
|
if (dataPackageId == null) {
|
||||||
|
throw new LegalRequirementsUploadException("LegalRequirements upload response did not contain DataPackageID.");
|
||||||
|
}
|
||||||
|
return dataPackageId;
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new LegalRequirementsUploadException("Failed to upload tachograph file to LegalRequirements.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String downloadXml(HttpClient client, EventHubProperties.LegalRequirements config, String dataPackageId) {
|
||||||
|
try {
|
||||||
|
HttpRequest request = requestBuilder(config.getBaseUrl() + "/DataPackages/" + dataPackageId + "/Xml", config.getReadTimeout())
|
||||||
|
.header("Authorization", basicAuth(config))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
if (response.statusCode() / 100 != 2) {
|
||||||
|
throw new LegalRequirementsXmlDownloadException("LegalRequirements XML download failed with status " + response.statusCode());
|
||||||
|
}
|
||||||
|
return response.body();
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new LegalRequirementsXmlDownloadException("Failed to download processed tachograph XML.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetSessionQuietly(HttpClient client, EventHubProperties.LegalRequirements config) {
|
||||||
|
try {
|
||||||
|
HttpRequest request = requestBuilder(config.getBaseUrl() + "/LegalRequirementsResults/ResetSession", config.getReadTimeout())
|
||||||
|
.header("Authorization", basicAuth(config))
|
||||||
|
.POST(HttpRequest.BodyPublishers.noBody())
|
||||||
|
.build();
|
||||||
|
client.send(request, HttpResponse.BodyHandlers.discarding());
|
||||||
|
} catch (IOException | InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequest.Builder requestBuilder(String url, Duration timeout) {
|
||||||
|
return HttpRequest.newBuilder(URI.create(url)).timeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String basicAuth(EventHubProperties.LegalRequirements config) {
|
||||||
|
String user = config.getUsername() == null ? "" : config.getUsername();
|
||||||
|
String password = config.getPassword() == null ? "" : config.getPassword();
|
||||||
|
String token = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||||
|
return "Basic " + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String field) {
|
||||||
|
if (node == null || !node.has(field) || node.get(field).isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return node.get(field).asText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
public class LegalRequirementsUploadException extends RuntimeException {
|
||||||
|
|
||||||
|
public LegalRequirementsUploadException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegalRequirementsUploadException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
public record LegalRequirementsUploadResult(
|
||||||
|
String dataPackageId,
|
||||||
|
String xmlContent
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
public class LegalRequirementsXmlDownloadException extends RuntimeException {
|
||||||
|
|
||||||
|
public LegalRequirementsXmlDownloadException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegalRequirementsXmlDownloadException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class TachographFileSessionNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public TachographFileSessionNotFoundException(UUID sessionId) {
|
||||||
|
super("Tachograph file session not found: " + sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface TachographFileSessionRepository {
|
||||||
|
|
||||||
|
TachographFileSession save(TachographFileSession session);
|
||||||
|
|
||||||
|
Optional<TachographFileSession> find(UUID sessionId);
|
||||||
|
|
||||||
|
boolean delete(UUID sessionId);
|
||||||
|
|
||||||
|
List<TachographFileSession> list();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionDeleteResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDriversResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TachographFileSessionService {
|
||||||
|
|
||||||
|
private final EventHubProperties properties;
|
||||||
|
private final TachographFileSessionRepository repository;
|
||||||
|
private final LegalRequirementsClient legalRequirementsClient;
|
||||||
|
private final TachographXmlParser tachographXmlParser;
|
||||||
|
private final DriverCardXmlExtractionService extractionService;
|
||||||
|
|
||||||
|
public TachographFileSessionService(
|
||||||
|
EventHubProperties properties,
|
||||||
|
TachographFileSessionRepository repository,
|
||||||
|
LegalRequirementsClient legalRequirementsClient,
|
||||||
|
TachographXmlParser tachographXmlParser,
|
||||||
|
DriverCardXmlExtractionService extractionService
|
||||||
|
) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.repository = repository;
|
||||||
|
this.legalRequirementsClient = legalRequirementsClient;
|
||||||
|
this.tachographXmlParser = tachographXmlParser;
|
||||||
|
this.extractionService = extractionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreateTachographFileSessionResponse createSession(
|
||||||
|
MultipartFile file,
|
||||||
|
String tenantKey,
|
||||||
|
String sourceInstanceKey,
|
||||||
|
String sessionLabel
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
byte[] fileBytes = file.getBytes();
|
||||||
|
validateFile(file, fileBytes);
|
||||||
|
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadDriverCard(fileBytes, file.getOriginalFilename());
|
||||||
|
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parseDriverCardXml(uploadResult.xmlContent());
|
||||||
|
Instant createdAt = Instant.now();
|
||||||
|
Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
|
||||||
|
TachographFileSessionMetadata metadata = new TachographFileSessionMetadata(
|
||||||
|
tenant(tenantKey),
|
||||||
|
sourceInstance(sourceInstanceKey),
|
||||||
|
blankToNull(sessionLabel),
|
||||||
|
file.getOriginalFilename(),
|
||||||
|
sha256(fileBytes),
|
||||||
|
fileBytes.length,
|
||||||
|
uploadResult.dataPackageId(),
|
||||||
|
sha256(uploadResult.xmlContent().getBytes(StandardCharsets.UTF_8)),
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
TachographFileSession session = extractionService.extract(parsedXml, metadata, createdAt, expiresAt);
|
||||||
|
TachographFileSession saved = repository.save(session);
|
||||||
|
return new CreateTachographFileSessionResponse(toSummary(saved));
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Failed to create tachograph file session.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographFileSessionSummaryDto getSession(UUID sessionId) {
|
||||||
|
return toSummary(requireSession(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographFileSessionListDriversResponse listDrivers(UUID sessionId) {
|
||||||
|
TachographFileSession session = requireSession(sessionId);
|
||||||
|
return new TachographFileSessionListDriversResponse(session.sessionId(), driverSummaries(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographFileDriverDetailDto getDriver(UUID sessionId, String driverKey) {
|
||||||
|
TachographFileSession session = requireSession(sessionId);
|
||||||
|
DriverExtractionSession driver = session.driversByKey().get(driverKey);
|
||||||
|
if (driver == null) {
|
||||||
|
throw new DriverNotFoundInSessionException(sessionId, driverKey);
|
||||||
|
}
|
||||||
|
return new TachographFileDriverDetailDto(
|
||||||
|
session.sessionId(),
|
||||||
|
driver.driverKey(),
|
||||||
|
driver.driver(),
|
||||||
|
driver.driverCard(),
|
||||||
|
driver.vehicleRegistrations(),
|
||||||
|
driver.vehicles(),
|
||||||
|
driver.cardVehicleUsageIntervals(),
|
||||||
|
driver.cardActivityIntervals(),
|
||||||
|
driver.warnings()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographFileSessionDeleteResponse deleteSession(UUID sessionId) {
|
||||||
|
return new TachographFileSessionDeleteResponse(sessionId, repository.delete(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TachographFileSession requireSession(UUID sessionId) {
|
||||||
|
return repository.find(sessionId).orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TachographFileSessionSummaryDto toSummary(TachographFileSession session) {
|
||||||
|
return new TachographFileSessionSummaryDto(
|
||||||
|
session.sessionId(),
|
||||||
|
session.metadata().tenantKey(),
|
||||||
|
session.metadata().sourceInstanceKey(),
|
||||||
|
session.metadata().sessionLabel(),
|
||||||
|
session.metadata().originalFileName(),
|
||||||
|
session.metadata().driverCardFile(),
|
||||||
|
session.metadata().legalRequirementsDataPackageId(),
|
||||||
|
session.extractionStats(),
|
||||||
|
driverSummaries(session),
|
||||||
|
session.warnings(),
|
||||||
|
session.createdAt(),
|
||||||
|
session.expiresAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TachographFileDriverSummaryDto> driverSummaries(TachographFileSession session) {
|
||||||
|
return session.driversByKey().values().stream()
|
||||||
|
.sorted(Comparator.comparing(DriverExtractionSession::driverKey))
|
||||||
|
.map(driver -> new TachographFileDriverSummaryDto(
|
||||||
|
driver.driverKey(),
|
||||||
|
driver.driver() == null ? null : driver.driver().surname(),
|
||||||
|
driver.driver() == null ? null : driver.driver().firstNames(),
|
||||||
|
driver.driverCard() == null ? null : driver.driverCard().cardNation(),
|
||||||
|
driver.driverCard() == null ? null : driver.driverCard().cardNumber(),
|
||||||
|
driver.cardActivityIntervals().size(),
|
||||||
|
driver.cardVehicleUsageIntervals().size()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFile(MultipartFile file, byte[] fileBytes) {
|
||||||
|
if (file == null || file.isEmpty() || fileBytes.length == 0) {
|
||||||
|
throw new IllegalArgumentException("Tachograph file must not be empty.");
|
||||||
|
}
|
||||||
|
if (fileBytes.length > properties.getTachographFileSession().getMaxFileSizeBytes()) {
|
||||||
|
throw new IllegalArgumentException("Tachograph file exceeds the configured size limit.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tenant(String tenantKey) {
|
||||||
|
return blankToNull(tenantKey) == null ? "default" : tenantKey.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sourceInstance(String sourceInstanceKey) {
|
||||||
|
return blankToNull(sourceInstanceKey) == null ? "legalrequirements-drivercard" : sourceInstanceKey.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToNull(String value) {
|
||||||
|
return value == null || value.isBlank() ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sha256(byte[] bytes) {
|
||||||
|
try {
|
||||||
|
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("SHA-256 digest is not available.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import javax.xml.XMLConstants;
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.transform.stream.StreamSource;
|
||||||
|
import javax.xml.validation.Schema;
|
||||||
|
import javax.xml.validation.SchemaFactory;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class TachographXmlParser {
|
||||||
|
|
||||||
|
private static final String JAXP_MAX_OCCUR_LIMIT = "http://www.oracle.com/xml/jaxp/properties/maxOccurLimit";
|
||||||
|
|
||||||
|
private final Schema schema;
|
||||||
|
|
||||||
|
public TachographXmlParser() {
|
||||||
|
this.schema = loadSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParsedTachographXml parseDriverCardXml(String xmlContent) {
|
||||||
|
try {
|
||||||
|
validate(xmlContent);
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(false);
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));
|
||||||
|
String rootName = document.getDocumentElement() == null ? null : document.getDocumentElement().getNodeName();
|
||||||
|
if (!"DriverCard".equals(rootName)) {
|
||||||
|
throw new UnsupportedTachographFileTypeException("Only DriverCard XML documents are supported in phase 1.");
|
||||||
|
}
|
||||||
|
return new ParsedTachographXml(document, rootName);
|
||||||
|
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||||
|
throw new TachographXmlValidationException("Failed to parse tachograph XML.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(String xmlContent) {
|
||||||
|
try {
|
||||||
|
schema.newValidator().validate(new StreamSource(new StringReader(xmlContent)));
|
||||||
|
} catch (IOException | SAXException e) {
|
||||||
|
throw new TachographXmlValidationException("Tachograph XML failed XSD validation.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Schema loadSchema() {
|
||||||
|
try {
|
||||||
|
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
|
||||||
|
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
||||||
|
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
||||||
|
factory.setProperty(JAXP_MAX_OCCUR_LIMIT, 100_000);
|
||||||
|
byte[] content = new ClassPathResource("xsd/Tachograph.xsd").getContentAsByteArray();
|
||||||
|
return factory.newSchema(new StreamSource(new ByteArrayInputStream(content)));
|
||||||
|
} catch (IOException | SAXException e) {
|
||||||
|
throw new IllegalStateException("Failed to load Tachograph XSD schema.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ParsedTachographXml(
|
||||||
|
Document document,
|
||||||
|
String rootElementName
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
public class TachographXmlValidationException extends RuntimeException {
|
||||||
|
|
||||||
|
public TachographXmlValidationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TachographXmlValidationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
public class UnsupportedTachographFileTypeException extends RuntimeException {
|
||||||
|
|
||||||
|
public UnsupportedTachographFileTypeException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class VehicleKeyFactory {
|
||||||
|
|
||||||
|
public String createRegistrationKey(String registrationNation, String registrationNumber) {
|
||||||
|
String normalizedNation = normalize(registrationNation, "UNKNOWN");
|
||||||
|
String normalizedNumber = normalize(registrationNumber, "UNKNOWN");
|
||||||
|
return normalizedNation + ":" + normalizedNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createSourceVehicleRegistrationId(String registrationKey) {
|
||||||
|
return "VR:" + registrationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createVehicleKey(String vin) {
|
||||||
|
return normalize(vin, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createSourceVehicleId(String vehicleKey) {
|
||||||
|
return vehicleKey == null ? null : "VIN:" + vehicleKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalize(String value, String fallback) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -121,6 +121,18 @@ eventhub:
|
||||||
initial-occurred-to: "2026-04-10T00:00:00+01:00"
|
initial-occurred-to: "2026-04-10T00:00:00+01:00"
|
||||||
run-initial-on-startup: true
|
run-initial-on-startup: true
|
||||||
|
|
||||||
|
tachograph-file-session:
|
||||||
|
ttl: 4h
|
||||||
|
max-sessions: 100
|
||||||
|
max-file-size-bytes: 20971520
|
||||||
|
legal-requirements:
|
||||||
|
base-url: ${LEGAL_REQUIREMENTS_BASE_URL:https://legalrequirements.services.bytebar.eu/ODataV4/LR}
|
||||||
|
username: ${LEGAL_REQUIREMENTS_USERNAME:}
|
||||||
|
password: ${LEGAL_REQUIREMENTS_PASSWORD:}
|
||||||
|
connect-timeout: 20s
|
||||||
|
read-timeout: 2m
|
||||||
|
reset-session-after-use: true
|
||||||
|
|
||||||
esper-poc:
|
esper-poc:
|
||||||
activity-merge-mode: JAVA
|
activity-merge-mode: JAVA
|
||||||
shift-resolution-mode: JAVA
|
shift-resolution-mode: JAVA
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,82 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.api;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverDetailDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionDeleteResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDriversResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
|
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
class TachographFileSessionControllerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void uploadsSessionListsDriversAndDeletes() throws Exception {
|
||||||
|
TachographFileSessionService service = org.mockito.Mockito.mock(TachographFileSessionService.class);
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TachographFileSessionController(service))
|
||||||
|
.setControllerAdvice(new TachographFileSessionExceptionHandler())
|
||||||
|
.build();
|
||||||
|
UUID sessionId = UUID.randomUUID();
|
||||||
|
TachographFileDriverSummaryDto driver = new TachographFileDriverSummaryDto("12:123", "Muster", "Max", "12", "123", 3, 2);
|
||||||
|
TachographFileSessionSummaryDto summary = new TachographFileSessionSummaryDto(
|
||||||
|
sessionId,
|
||||||
|
"default",
|
||||||
|
"legalrequirements-drivercard",
|
||||||
|
"sample",
|
||||||
|
"sample.ddd",
|
||||||
|
true,
|
||||||
|
"42",
|
||||||
|
new ExtractionStats(1, 3, 2, 2, 2, 0),
|
||||||
|
List.of(driver),
|
||||||
|
List.of(),
|
||||||
|
Instant.parse("2026-05-12T10:00:00Z"),
|
||||||
|
Instant.parse("2026-05-12T14:00:00Z")
|
||||||
|
);
|
||||||
|
when(service.createSession(org.mockito.ArgumentMatchers.any(), eq("default"), eq("src"), eq("sample")))
|
||||||
|
.thenReturn(new CreateTachographFileSessionResponse(summary));
|
||||||
|
when(service.getSession(sessionId)).thenReturn(summary);
|
||||||
|
when(service.listDrivers(sessionId)).thenReturn(new TachographFileSessionListDriversResponse(sessionId, List.of(driver)));
|
||||||
|
when(service.getDriver(sessionId, "12:123")).thenReturn(new TachographFileDriverDetailDto(sessionId, "12:123", null, null, List.of(), List.of(), List.of(), List.of(), List.of()));
|
||||||
|
when(service.deleteSession(sessionId)).thenReturn(new TachographFileSessionDeleteResponse(sessionId, true));
|
||||||
|
|
||||||
|
mockMvc.perform(multipart("/api/eventhub/tachograph-file-sessions")
|
||||||
|
.file(new MockMultipartFile("file", "sample.ddd", "application/octet-stream", "abc".getBytes()))
|
||||||
|
.param("tenantKey", "default")
|
||||||
|
.param("sourceInstanceKey", "src")
|
||||||
|
.param("sessionLabel", "sample"))
|
||||||
|
.andExpect(status().isCreated())
|
||||||
|
.andExpect(jsonPath("$.session.sessionId").value(sessionId.toString()));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/{sessionId}", sessionId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.sessionId").value(sessionId.toString()));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers", sessionId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.drivers[0].driverKey").value("12:123"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}", sessionId, "12:123"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.driverKey").value("12:123"));
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/api/eventhub/tachograph-file-sessions/{sessionId}", sessionId))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.deleted").value(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class DriverCardXmlExtractionServiceTest {
|
||||||
|
|
||||||
|
private final TachographXmlParser parser = new TachographXmlParser();
|
||||||
|
private final DriverCardXmlExtractionService service = new DriverCardXmlExtractionService(
|
||||||
|
new DriverKeyFactory(),
|
||||||
|
new VehicleKeyFactory()
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void extractsDriverCardVehiclesAndActivities() {
|
||||||
|
TachographFileSession session = service.extract(
|
||||||
|
parser.parseDriverCardXml(DriverCardXmlSamples.validDriverCardXml()),
|
||||||
|
new TachographFileSessionMetadata(
|
||||||
|
"default",
|
||||||
|
"legalrequirements-drivercard",
|
||||||
|
"sample",
|
||||||
|
"sample.ddd",
|
||||||
|
"abc",
|
||||||
|
10,
|
||||||
|
"42",
|
||||||
|
"def",
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(session.driversByKey()).hasSize(1);
|
||||||
|
DriverExtractionSession driver = session.driversByKey().values().iterator().next();
|
||||||
|
assertThat(driver.driverKey()).isEqualTo("12:12345678901200");
|
||||||
|
assertThat(driver.driver().surname()).isEqualTo("Muster");
|
||||||
|
assertThat(driver.driverCard().sourceDriverCardId()).isEqualTo("CARD:12:12345678901200");
|
||||||
|
assertThat(driver.vehicleRegistrations()).hasSize(2);
|
||||||
|
assertThat(driver.vehicles()).hasSize(2);
|
||||||
|
assertThat(driver.cardVehicleUsageIntervals()).hasSize(2);
|
||||||
|
assertThat(driver.cardActivityIntervals()).hasSize(3);
|
||||||
|
assertThat(driver.cardActivityIntervals().get(0).registrationKey()).isEqualTo("12:W-12345A");
|
||||||
|
assertThat(driver.cardActivityIntervals().get(2).registrationKey()).isEqualTo("12:W-54321B");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
final class DriverCardXmlSamples {
|
||||||
|
|
||||||
|
private DriverCardXmlSamples() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static String validDriverCardXml() {
|
||||||
|
return """
|
||||||
|
<DriverCard>
|
||||||
|
<Identification>
|
||||||
|
<cardIdentification>
|
||||||
|
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||||
|
<cardNumber>
|
||||||
|
<driverIdentification>123456789012</driverIdentification>
|
||||||
|
<cardReplacementIndex>0</cardReplacementIndex>
|
||||||
|
<cardRenewalIndex>0</cardRenewalIndex>
|
||||||
|
</cardNumber>
|
||||||
|
<cardIssuingAuthorityName>
|
||||||
|
<name>Authority</name>
|
||||||
|
</cardIssuingAuthorityName>
|
||||||
|
<cardIssueDate>2026-04-01T00:00:00Z</cardIssueDate>
|
||||||
|
<cardValidityBegin>2026-04-01T00:00:00Z</cardValidityBegin>
|
||||||
|
<cardExpiryDate>2031-04-01T00:00:00Z</cardExpiryDate>
|
||||||
|
</cardIdentification>
|
||||||
|
<driverCardHolderIdentification>
|
||||||
|
<cardHolderName>
|
||||||
|
<holderSurname><name>Muster</name></holderSurname>
|
||||||
|
<holderFirstNames><name>Max</name></holderFirstNames>
|
||||||
|
</cardHolderName>
|
||||||
|
<cardHolderBirthDate>
|
||||||
|
<year>1985</year>
|
||||||
|
<month>06</month>
|
||||||
|
<day>15</day>
|
||||||
|
</cardHolderBirthDate>
|
||||||
|
<cardHolderPreferredLanguage>de</cardHolderPreferredLanguage>
|
||||||
|
</driverCardHolderIdentification>
|
||||||
|
<signature></signature>
|
||||||
|
</Identification>
|
||||||
|
<DrivingLicenseInfo>
|
||||||
|
<cardDrivingLicenseInformation>
|
||||||
|
<drivingLicenceIssuingAuthority><name>Vienna</name></drivingLicenceIssuingAuthority>
|
||||||
|
<drivingLicenceIssuingNation>12</drivingLicenceIssuingNation>
|
||||||
|
<drivingLicenceNumber>B1234567</drivingLicenceNumber>
|
||||||
|
</cardDrivingLicenseInformation>
|
||||||
|
<signature></signature>
|
||||||
|
</DrivingLicenseInfo>
|
||||||
|
<VehiclesUsed>
|
||||||
|
<cardVehiclesUsed>
|
||||||
|
<vehiclePointerNewestRecord>1</vehiclePointerNewestRecord>
|
||||||
|
<cardVehicleRecords>
|
||||||
|
<vehicleOdometerBegin>1000</vehicleOdometerBegin>
|
||||||
|
<vehicleOdometerEnd>1100</vehicleOdometerEnd>
|
||||||
|
<vehicleFirstUse>2026-04-01T08:00:00Z</vehicleFirstUse>
|
||||||
|
<vehicleLastUse>2026-04-01T11:59:59Z</vehicleLastUse>
|
||||||
|
<vehicleRegistration>
|
||||||
|
<vehicleRegistrationNation>12</vehicleRegistrationNation>
|
||||||
|
<vehicleRegistrationNumber><vehicleRegNumber>W-12345A</vehicleRegNumber></vehicleRegistrationNumber>
|
||||||
|
</vehicleRegistration>
|
||||||
|
<vuDataBlockCounter>0001</vuDataBlockCounter>
|
||||||
|
<vehicleIdentificationNumber>VIN12345678901234</vehicleIdentificationNumber>
|
||||||
|
</cardVehicleRecords>
|
||||||
|
<cardVehicleRecords>
|
||||||
|
<vehicleOdometerBegin>1101</vehicleOdometerBegin>
|
||||||
|
<vehicleOdometerEnd>1200</vehicleOdometerEnd>
|
||||||
|
<vehicleFirstUse>2026-04-01T12:00:00Z</vehicleFirstUse>
|
||||||
|
<vehicleLastUse>2026-04-01T18:00:00Z</vehicleLastUse>
|
||||||
|
<vehicleRegistration>
|
||||||
|
<vehicleRegistrationNation>12</vehicleRegistrationNation>
|
||||||
|
<vehicleRegistrationNumber><vehicleRegNumber>W-54321B</vehicleRegNumber></vehicleRegistrationNumber>
|
||||||
|
</vehicleRegistration>
|
||||||
|
<vuDataBlockCounter>0002</vuDataBlockCounter>
|
||||||
|
<vehicleIdentificationNumber>VIN99999999999999</vehicleIdentificationNumber>
|
||||||
|
</cardVehicleRecords>
|
||||||
|
</cardVehiclesUsed>
|
||||||
|
<signature></signature>
|
||||||
|
</VehiclesUsed>
|
||||||
|
<DriverActivityData>
|
||||||
|
<cardDriverActivity>
|
||||||
|
<activityPointerOldestDayRecord>0</activityPointerOldestDayRecord>
|
||||||
|
<activityPointerNewestRecord>1</activityPointerNewestRecord>
|
||||||
|
<cardActivityDailyRecord>
|
||||||
|
<activityRecordDate>2026-04-01T00:00:00Z</activityRecordDate>
|
||||||
|
<activityDailyPresenceCounter>0001</activityDailyPresenceCounter>
|
||||||
|
<activityDayDistance>200</activityDayDistance>
|
||||||
|
<noOfActivityChanges>3</noOfActivityChanges>
|
||||||
|
<activityChangeInfos>
|
||||||
|
<slot>DRIVER</slot>
|
||||||
|
<drivingStatus>SINGLE</drivingStatus>
|
||||||
|
<cardStatus>INSERTED</cardStatus>
|
||||||
|
<activity>WORK</activity>
|
||||||
|
<timeOfChange>08:00:00Z</timeOfChange>
|
||||||
|
</activityChangeInfos>
|
||||||
|
<activityChangeInfos>
|
||||||
|
<slot>DRIVER</slot>
|
||||||
|
<drivingStatus>SINGLE</drivingStatus>
|
||||||
|
<cardStatus>INSERTED</cardStatus>
|
||||||
|
<activity>DRIVING</activity>
|
||||||
|
<timeOfChange>09:00:00Z</timeOfChange>
|
||||||
|
</activityChangeInfos>
|
||||||
|
<activityChangeInfos>
|
||||||
|
<slot>DRIVER</slot>
|
||||||
|
<drivingStatus>SINGLE</drivingStatus>
|
||||||
|
<cardStatus>INSERTED</cardStatus>
|
||||||
|
<activity>BREAK/REST</activity>
|
||||||
|
<timeOfChange>12:30:00Z</timeOfChange>
|
||||||
|
</activityChangeInfos>
|
||||||
|
</cardActivityDailyRecord>
|
||||||
|
</cardDriverActivity>
|
||||||
|
<signature></signature>
|
||||||
|
</DriverActivityData>
|
||||||
|
</DriverCard>
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||||
|
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
|
class TachographFileSessionServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsAndLoadsSession() {
|
||||||
|
EventHubProperties properties = new EventHubProperties();
|
||||||
|
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||||
|
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||||
|
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||||
|
DriverCardXmlExtractionService extractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||||
|
TachographFileSessionService service = new TachographFileSessionService(properties, repository, client, parser, extractor);
|
||||||
|
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", "abc".getBytes(StandardCharsets.UTF_8));
|
||||||
|
when(client.uploadDriverCard(any(), eq("sample.ddd"))).thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
||||||
|
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||||
|
when(parser.parseDriverCardXml("<DriverCard/>")).thenReturn(parsed);
|
||||||
|
TachographFileSession extracted = new TachographFileSession(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
|
||||||
|
Map.of(),
|
||||||
|
new ExtractionStats(0, 0, 0, 0, 0, 0),
|
||||||
|
java.util.List.of(),
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now().plusSeconds(10)
|
||||||
|
);
|
||||||
|
when(extractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||||
|
|
||||||
|
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "sample");
|
||||||
|
|
||||||
|
assertThat(response.session().sessionId()).isEqualTo(extracted.sessionId());
|
||||||
|
assertThat(service.getSession(extracted.sessionId()).sessionId()).isEqualTo(extracted.sessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package at.procon.eventhub.tachographfilesession.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class TachographXmlParserTest {
|
||||||
|
|
||||||
|
private final TachographXmlParser parser = new TachographXmlParser();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesValidDriverCardXml() {
|
||||||
|
TachographXmlParser.ParsedTachographXml parsed = parser.parseDriverCardXml(DriverCardXmlSamples.validDriverCardXml());
|
||||||
|
|
||||||
|
assertThat(parsed.rootElementName()).isEqualTo("DriverCard");
|
||||||
|
assertThat(parsed.document().getDocumentElement().getNodeName()).isEqualTo("DriverCard");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsInvalidXmlAgainstSchema() {
|
||||||
|
String invalid = "<DriverCard><Identification></DriverCard>";
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> parser.parseDriverCardXml(invalid))
|
||||||
|
.isInstanceOf(TachographXmlValidationException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue