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 Tachograph tachograph = new Tachograph();
|
||||
private final TachographFileSession tachographFileSession = new TachographFileSession();
|
||||
private final EsperPoc esperPoc = new EsperPoc();
|
||||
private final YellowFox yellowFox = new YellowFox();
|
||||
|
||||
|
|
@ -41,6 +42,10 @@ public class EventHubProperties {
|
|||
return tachograph;
|
||||
}
|
||||
|
||||
public TachographFileSession getTachographFileSession() {
|
||||
return tachographFileSession;
|
||||
}
|
||||
|
||||
public EsperPoc getEsperPoc() {
|
||||
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 {
|
||||
private String jdbcUrl;
|
||||
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"
|
||||
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:
|
||||
activity-merge-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