Add vehicle unit tachograph file sessions
This commit is contained in:
parent
39714b90b3
commit
6b43a4b0e8
|
|
@ -248,6 +248,49 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create tachograph vehicle unit file session",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "file",
|
||||
"type": "file",
|
||||
"src": "{{tachographVuDddFile}}"
|
||||
},
|
||||
{
|
||||
"key": "tenantKey",
|
||||
"type": "text",
|
||||
"value": "{{tenantKey}}"
|
||||
},
|
||||
{
|
||||
"key": "sourceInstanceKey",
|
||||
"type": "text",
|
||||
"value": "legalrequirements-vehicleunit"
|
||||
},
|
||||
{
|
||||
"key": "sessionLabel",
|
||||
"type": "text",
|
||||
"value": "vehicle-unit upload sample"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/eventhub/tachograph-file-sessions",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"eventhub",
|
||||
"tachograph-file-sessions"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get tachograph file session",
|
||||
"request": {
|
||||
|
|
@ -374,6 +417,10 @@
|
|||
{
|
||||
"key": "tachographDddFile",
|
||||
"value": "C:\\\\temp\\\\driver-card.ddd"
|
||||
},
|
||||
{
|
||||
"key": "tachographVuDddFile",
|
||||
"value": "C:\\\\temp\\\\vehicle-unit.ddd"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public class LegalRequirementsClient {
|
|||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public LegalRequirementsUploadResult uploadDriverCard(byte[] fileBytes, String fileName) {
|
||||
public LegalRequirementsUploadResult uploadTachographFile(byte[] fileBytes, String fileName) {
|
||||
EventHubProperties.LegalRequirements config = properties.getTachographFileSession().getLegalRequirements();
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(config.getConnectTimeout())
|
||||
|
|
|
|||
|
|
@ -28,20 +28,23 @@ public class TachographFileSessionService {
|
|||
private final TachographFileSessionRepository repository;
|
||||
private final LegalRequirementsClient legalRequirementsClient;
|
||||
private final TachographXmlParser tachographXmlParser;
|
||||
private final DriverCardXmlExtractionService extractionService;
|
||||
private final DriverCardXmlExtractionService driverCardExtractionService;
|
||||
private final VehicleUnitXmlExtractionService vehicleUnitExtractionService;
|
||||
|
||||
public TachographFileSessionService(
|
||||
EventHubProperties properties,
|
||||
TachographFileSessionRepository repository,
|
||||
LegalRequirementsClient legalRequirementsClient,
|
||||
TachographXmlParser tachographXmlParser,
|
||||
DriverCardXmlExtractionService extractionService
|
||||
DriverCardXmlExtractionService driverCardExtractionService,
|
||||
VehicleUnitXmlExtractionService vehicleUnitExtractionService
|
||||
) {
|
||||
this.properties = properties;
|
||||
this.repository = repository;
|
||||
this.legalRequirementsClient = legalRequirementsClient;
|
||||
this.tachographXmlParser = tachographXmlParser;
|
||||
this.extractionService = extractionService;
|
||||
this.driverCardExtractionService = driverCardExtractionService;
|
||||
this.vehicleUnitExtractionService = vehicleUnitExtractionService;
|
||||
}
|
||||
|
||||
public CreateTachographFileSessionResponse createSession(
|
||||
|
|
@ -54,23 +57,26 @@ public class TachographFileSessionService {
|
|||
validateFile(file);
|
||||
byte[] fileBytes = file.getBytes();
|
||||
validateFileBytes(fileBytes);
|
||||
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadDriverCard(fileBytes, file.getOriginalFilename());
|
||||
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parseDriverCardXml(uploadResult.xmlContent());
|
||||
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadTachographFile(fileBytes, file.getOriginalFilename());
|
||||
TachographXmlParser.ParsedTachographXml parsedXml = tachographXmlParser.parse(uploadResult.xmlContent());
|
||||
Instant createdAt = Instant.now();
|
||||
Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
|
||||
boolean driverCardFile = "DriverCard".equals(parsedXml.rootElementName());
|
||||
TachographFileSessionMetadata metadata = new TachographFileSessionMetadata(
|
||||
tenant(tenantKey),
|
||||
sourceInstance(sourceInstanceKey),
|
||||
sourceInstance(sourceInstanceKey, driverCardFile),
|
||||
blankToNull(sessionLabel),
|
||||
file.getOriginalFilename(),
|
||||
sha256(fileBytes),
|
||||
fileBytes.length,
|
||||
uploadResult.dataPackageId(),
|
||||
sha256(uploadResult.xmlContent().getBytes(StandardCharsets.UTF_8)),
|
||||
true,
|
||||
driverCardFile,
|
||||
null
|
||||
);
|
||||
TachographFileSession session = extractionService.extract(parsedXml, metadata, createdAt, expiresAt);
|
||||
TachographFileSession session = driverCardFile
|
||||
? driverCardExtractionService.extract(parsedXml, metadata, createdAt, expiresAt)
|
||||
: vehicleUnitExtractionService.extract(parsedXml, metadata, createdAt, expiresAt);
|
||||
TachographFileSession saved = repository.save(session);
|
||||
return new CreateTachographFileSessionResponse(toSummary(saved));
|
||||
} catch (Exception e) {
|
||||
|
|
@ -171,8 +177,11 @@ public class TachographFileSessionService {
|
|||
return blankToNull(tenantKey) == null ? "default" : tenantKey.trim();
|
||||
}
|
||||
|
||||
private String sourceInstance(String sourceInstanceKey) {
|
||||
return blankToNull(sourceInstanceKey) == null ? "legalrequirements-drivercard" : sourceInstanceKey.trim();
|
||||
private String sourceInstance(String sourceInstanceKey, boolean driverCardFile) {
|
||||
if (blankToNull(sourceInstanceKey) != null) {
|
||||
return sourceInstanceKey.trim();
|
||||
}
|
||||
return driverCardFile ? "legalrequirements-drivercard" : "legalrequirements-vehicleunit";
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class TachographXmlParser {
|
|||
this.schema = loadSchema();
|
||||
}
|
||||
|
||||
public ParsedTachographXml parseDriverCardXml(String xmlContent) {
|
||||
public ParsedTachographXml parse(String xmlContent) {
|
||||
try {
|
||||
validate(xmlContent);
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
|
|
@ -44,8 +44,8 @@ public class TachographXmlParser {
|
|||
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.");
|
||||
if (!"DriverCard".equals(rootName) && !"VehicleUnit".equals(rootName)) {
|
||||
throw new UnsupportedTachographFileTypeException("Only DriverCard and VehicleUnit XML documents are supported.");
|
||||
}
|
||||
return new ParsedTachographXml(document, rootName);
|
||||
} catch (ParserConfigurationException | IOException | SAXException e) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,364 @@
|
|||
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.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.NodeList;
|
||||
|
||||
@Component
|
||||
public class VehicleUnitXmlExtractionService {
|
||||
|
||||
private final DriverKeyFactory driverKeyFactory;
|
||||
private final VehicleKeyFactory vehicleKeyFactory;
|
||||
|
||||
public VehicleUnitXmlExtractionService(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> sessionWarnings = new ArrayList<>();
|
||||
|
||||
VehicleContext vehicleContext = extractVehicleContext(document, sessionWarnings);
|
||||
Map<String, DriverExtractionBuilder> driversByKey = new LinkedHashMap<>();
|
||||
|
||||
if (nodes(document, "/VehicleUnit/Activities/vuActivityDailyData").getLength() > 0) {
|
||||
sessionWarnings.add(new ExtractionWarning(
|
||||
"VU_ACTIVITY_NOT_EXTRACTED",
|
||||
"Vehicle-unit daily activity data is present but is not yet mapped into card activity intervals in this phase.",
|
||||
"/VehicleUnit/Activities/vuActivityDailyData"
|
||||
));
|
||||
}
|
||||
|
||||
NodeList records = nodes(document, "/VehicleUnit/Activities/vuCardIWData/vuCardIWRecords");
|
||||
for (int i = 0; i < records.getLength(); i++) {
|
||||
Element record = (Element) records.item(i);
|
||||
String path = "/VehicleUnit/Activities/vuCardIWData/vuCardIWRecords[" + (i + 1) + "]";
|
||||
String driverIdentification = text(record, "fullCardNumber/cardNumber/driverIdentification");
|
||||
if (driverIdentification == null) {
|
||||
continue;
|
||||
}
|
||||
String cardNation = text(record, "fullCardNumber/cardIssuingMemberState");
|
||||
String cardNumber = joinCardNumber(record, "fullCardNumber/cardNumber");
|
||||
if (cardNumber == null) {
|
||||
sessionWarnings.add(new ExtractionWarning(
|
||||
"MISSING_VU_DRIVER_CARD",
|
||||
"Vehicle-unit insertion/withdrawal record is missing a usable driver card number.",
|
||||
path
|
||||
));
|
||||
continue;
|
||||
}
|
||||
String driverKey = driverKeyFactory.createDriverKey(cardNation, cardNumber);
|
||||
DriverExtractionBuilder builder = driversByKey.computeIfAbsent(
|
||||
driverKey,
|
||||
ignored -> new DriverExtractionBuilder(driverKey, driverKeyFactory.createSourceDriverId(driverKey))
|
||||
);
|
||||
builder.mergeDriver(
|
||||
text(record, "cardHolderName/holderSurname/name"),
|
||||
text(record, "cardHolderName/holderFirstNames/name")
|
||||
);
|
||||
builder.mergeDriverCard(
|
||||
driverKeyFactory.createSourceDriverCardId(driverKey),
|
||||
cardNation,
|
||||
cardNumber,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
offsetDateTime(text(record, "cardExpiryDate"))
|
||||
);
|
||||
builder.addVehicleContext(vehicleContext.registration(), vehicleContext.vehicle());
|
||||
|
||||
OffsetDateTime from = offsetDateTime(text(record, "cardInsertionTime"));
|
||||
if (from == null) {
|
||||
sessionWarnings.add(new ExtractionWarning(
|
||||
"MISSING_VU_CARD_INSERTION",
|
||||
"Vehicle-unit insertion/withdrawal record is missing cardInsertionTime.",
|
||||
path
|
||||
));
|
||||
continue;
|
||||
}
|
||||
OffsetDateTime to = offsetDateTime(text(record, "cardWithdrawalTime"));
|
||||
if (to == null) {
|
||||
to = vehicleContext.defaultOpenIntervalEnd();
|
||||
sessionWarnings.add(new ExtractionWarning(
|
||||
"OPEN_VU_CARD_INTERVAL",
|
||||
"Vehicle-unit insertion/withdrawal record has no withdrawal time; interval was closed using the VU downloadable-period end.",
|
||||
path
|
||||
));
|
||||
}
|
||||
if (to == null || to.isBefore(from)) {
|
||||
sessionWarnings.add(new ExtractionWarning(
|
||||
"INVALID_VU_CARD_INTERVAL",
|
||||
"Vehicle-unit insertion/withdrawal record has an invalid interval range.",
|
||||
path
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.vehicleUsageIntervals.add(new ExtractedCardVehicleUsageInterval(
|
||||
"VUIW-" + (i + 1),
|
||||
from,
|
||||
to,
|
||||
longValue(text(record, "vehicleOdometerValueAtInsertion")),
|
||||
longValue(text(record, "vehicleOdometerValueAtWithdrawal")),
|
||||
vehicleContext.registration() == null ? null : vehicleContext.registration().registrationKey(),
|
||||
vehicleContext.vehicle() == null ? null : vehicleContext.vehicle().vehicleKey(),
|
||||
path
|
||||
));
|
||||
}
|
||||
|
||||
if (driversByKey.isEmpty()) {
|
||||
sessionWarnings.add(new ExtractionWarning(
|
||||
"NO_DRIVER_CARD_IW_DATA",
|
||||
"Vehicle-unit XML did not contain driver-card insertion/withdrawal records to build driver sessions.",
|
||||
"/VehicleUnit/Activities"
|
||||
));
|
||||
}
|
||||
|
||||
List<ExtractionWarning> allWarnings = new ArrayList<>(sessionWarnings);
|
||||
Map<String, DriverExtractionSession> driverSessions = new LinkedHashMap<>();
|
||||
int activityCount = 0;
|
||||
int vehicleUsageCount = 0;
|
||||
for (DriverExtractionBuilder builder : driversByKey.values()) {
|
||||
builder.vehicleUsageIntervals.sort(Comparator.comparing(ExtractedCardVehicleUsageInterval::from));
|
||||
allWarnings.addAll(builder.warnings);
|
||||
activityCount += builder.cardActivityIntervals.size();
|
||||
vehicleUsageCount += builder.vehicleUsageIntervals.size();
|
||||
driverSessions.put(builder.driverKey, builder.build());
|
||||
}
|
||||
|
||||
int registrationCount = vehicleContext.registration() == null ? 0 : 1;
|
||||
int vehicleCount = vehicleContext.vehicle() == null ? 0 : 1;
|
||||
ExtractionStats stats = new ExtractionStats(
|
||||
driverSessions.size(),
|
||||
activityCount,
|
||||
vehicleUsageCount,
|
||||
registrationCount,
|
||||
vehicleCount,
|
||||
allWarnings.size()
|
||||
);
|
||||
|
||||
return new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
metadata,
|
||||
Map.copyOf(driverSessions),
|
||||
stats,
|
||||
List.copyOf(allWarnings),
|
||||
createdAt,
|
||||
expiresAt
|
||||
);
|
||||
}
|
||||
|
||||
private VehicleContext extractVehicleContext(Document document, List<ExtractionWarning> warnings) {
|
||||
Element overview = firstElement(document, "/VehicleUnit/Overview[1]");
|
||||
if (overview == null) {
|
||||
warnings.add(new ExtractionWarning(
|
||||
"MISSING_VU_OVERVIEW",
|
||||
"Vehicle-unit XML does not contain an Overview block.",
|
||||
"/VehicleUnit"
|
||||
));
|
||||
return new VehicleContext(null, null, null);
|
||||
}
|
||||
|
||||
String registrationNation = text(overview, "vehicleRegistrationIdentification/vehicleRegistrationNation");
|
||||
String registrationNumber = text(overview, "vehicleRegistrationIdentification/vehicleRegistrationNumber/vehicleRegNumber");
|
||||
ExtractedVehicleRegistration registration = null;
|
||||
String registrationKey = registrationNation == null && registrationNumber == null
|
||||
? null
|
||||
: vehicleKeyFactory.createRegistrationKey(registrationNation, registrationNumber);
|
||||
if (registrationKey != null) {
|
||||
registration = new ExtractedVehicleRegistration(
|
||||
registrationKey,
|
||||
vehicleKeyFactory.createSourceVehicleRegistrationId(registrationKey),
|
||||
registrationNation,
|
||||
registrationNumber
|
||||
);
|
||||
}
|
||||
|
||||
String vin = text(overview, "vehicleIdentificationNumber");
|
||||
ExtractedVehicle vehicle = null;
|
||||
String vehicleKey = vehicleKeyFactory.createVehicleKey(vin);
|
||||
if (vehicleKey != null) {
|
||||
vehicle = new ExtractedVehicle(
|
||||
vehicleKey,
|
||||
vehicleKeyFactory.createSourceVehicleId(vehicleKey),
|
||||
vin
|
||||
);
|
||||
}
|
||||
|
||||
OffsetDateTime defaultEnd = offsetDateTime(text(overview, "vuDownloadablePeriod/maxDownloadableTime"));
|
||||
return new VehicleContext(registration, vehicle, defaultEnd);
|
||||
}
|
||||
|
||||
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 Long longValue(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return Long.parseLong(value.trim());
|
||||
}
|
||||
|
||||
private String joinCardNumber(Element node, String basePath) {
|
||||
String driverIdentification = text(node, basePath + "/driverIdentification");
|
||||
if (driverIdentification == null) {
|
||||
return null;
|
||||
}
|
||||
String replacement = text(node, basePath + "/cardReplacementIndex");
|
||||
String renewal = text(node, basePath + "/cardRenewalIndex");
|
||||
StringBuilder builder = new StringBuilder(driverIdentification);
|
||||
if (replacement != null) {
|
||||
builder.append(replacement);
|
||||
}
|
||||
if (renewal != null) {
|
||||
builder.append(renewal);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static final class DriverExtractionBuilder {
|
||||
private final String driverKey;
|
||||
private final String sourceDriverId;
|
||||
private ExtractedDriver driver;
|
||||
private ExtractedDriverCard driverCard;
|
||||
private final Map<String, ExtractedVehicleRegistration> registrationsByKey = new LinkedHashMap<>();
|
||||
private final Map<String, ExtractedVehicle> vehiclesByKey = new LinkedHashMap<>();
|
||||
private final List<ExtractedCardVehicleUsageInterval> vehicleUsageIntervals = new ArrayList<>();
|
||||
private final List<ExtractedCardActivityInterval> cardActivityIntervals = new ArrayList<>();
|
||||
private final List<ExtractionWarning> warnings = new ArrayList<>();
|
||||
|
||||
private DriverExtractionBuilder(String driverKey, String sourceDriverId) {
|
||||
this.driverKey = driverKey;
|
||||
this.sourceDriverId = sourceDriverId;
|
||||
}
|
||||
|
||||
private void mergeDriver(String surname, String firstNames) {
|
||||
if (driver == null) {
|
||||
driver = new ExtractedDriver(
|
||||
driverKey,
|
||||
sourceDriverId,
|
||||
surname,
|
||||
firstNames,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeDriverCard(
|
||||
String sourceDriverCardId,
|
||||
String cardNation,
|
||||
String cardNumber,
|
||||
String issuingAuthorityName,
|
||||
OffsetDateTime issueDate,
|
||||
OffsetDateTime validityBegin,
|
||||
OffsetDateTime expiryDate
|
||||
) {
|
||||
if (driverCard == null) {
|
||||
driverCard = new ExtractedDriverCard(
|
||||
sourceDriverCardId,
|
||||
cardNation,
|
||||
cardNumber,
|
||||
issuingAuthorityName,
|
||||
issueDate,
|
||||
validityBegin,
|
||||
expiryDate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void addVehicleContext(ExtractedVehicleRegistration registration, ExtractedVehicle vehicle) {
|
||||
if (registration != null) {
|
||||
registrationsByKey.putIfAbsent(registration.registrationKey(), registration);
|
||||
}
|
||||
if (vehicle != null) {
|
||||
vehiclesByKey.putIfAbsent(vehicle.vehicleKey(), vehicle);
|
||||
}
|
||||
}
|
||||
|
||||
private DriverExtractionSession build() {
|
||||
return new DriverExtractionSession(
|
||||
driverKey,
|
||||
driver,
|
||||
driverCard,
|
||||
List.copyOf(registrationsByKey.values()),
|
||||
List.copyOf(vehiclesByKey.values()),
|
||||
List.copyOf(vehicleUsageIntervals),
|
||||
List.copyOf(cardActivityIntervals),
|
||||
List.copyOf(warnings)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private record VehicleContext(
|
||||
ExtractedVehicleRegistration registration,
|
||||
ExtractedVehicle vehicle,
|
||||
OffsetDateTime defaultOpenIntervalEnd
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ class DriverCardXmlExtractionServiceTest {
|
|||
@Test
|
||||
void extractsDriverCardVehiclesAndActivities() {
|
||||
TachographFileSession session = service.extract(
|
||||
parser.parseDriverCardXml(DriverCardXmlSamples.validDriverCardXml()),
|
||||
parser.parse(DriverCardXmlSamples.validDriverCardXml()),
|
||||
new TachographFileSessionMetadata(
|
||||
"default",
|
||||
"legalrequirements-drivercard",
|
||||
|
|
|
|||
|
|
@ -28,13 +28,22 @@ class TachographFileSessionServiceTest {
|
|||
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);
|
||||
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
VehicleUnitXmlExtractionService vehicleUnitExtractor = Mockito.mock(VehicleUnitXmlExtractionService.class);
|
||||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
client,
|
||||
parser,
|
||||
driverCardExtractor,
|
||||
vehicleUnitExtractor
|
||||
);
|
||||
|
||||
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/>"));
|
||||
when(client.uploadTachographFile(any(), eq("sample.ddd"))).thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
||||
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||
when(parser.parseDriverCardXml("<DriverCard/>")).thenReturn(parsed);
|
||||
when(parsed.rootElementName()).thenReturn("DriverCard");
|
||||
when(parser.parse("<DriverCard/>")).thenReturn(parsed);
|
||||
TachographFileSession extracted = new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "sample.ddd", "a", 3, "42", "b", true, null),
|
||||
|
|
@ -44,7 +53,7 @@ class TachographFileSessionServiceTest {
|
|||
Instant.now(),
|
||||
Instant.now().plusSeconds(10)
|
||||
);
|
||||
when(extractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||
when(driverCardExtractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||
|
||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "sample");
|
||||
|
||||
|
|
@ -59,8 +68,16 @@ class TachographFileSessionServiceTest {
|
|||
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);
|
||||
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
VehicleUnitXmlExtractionService vehicleUnitExtractor = Mockito.mock(VehicleUnitXmlExtractionService.class);
|
||||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
client,
|
||||
parser,
|
||||
driverCardExtractor,
|
||||
vehicleUnitExtractor
|
||||
);
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", new byte[1025]);
|
||||
|
||||
|
|
@ -68,6 +85,45 @@ class TachographFileSessionServiceTest {
|
|||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("size limit");
|
||||
|
||||
verifyNoInteractions(client, parser, extractor);
|
||||
verifyNoInteractions(client, parser, driverCardExtractor, vehicleUnitExtractor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesVehicleUnitDefaultsAndExtractorForVehicleUnitXml() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
VehicleUnitXmlExtractionService vehicleUnitExtractor = Mockito.mock(VehicleUnitXmlExtractionService.class);
|
||||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
client,
|
||||
parser,
|
||||
driverCardExtractor,
|
||||
vehicleUnitExtractor
|
||||
);
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "vu.ddd", "application/octet-stream", "vu".getBytes(StandardCharsets.UTF_8));
|
||||
when(client.uploadTachographFile(any(), eq("vu.ddd"))).thenReturn(new LegalRequirementsUploadResult("77", "<VehicleUnit/>"));
|
||||
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||
when(parsed.rootElementName()).thenReturn("VehicleUnit");
|
||||
when(parser.parse("<VehicleUnit/>")).thenReturn(parsed);
|
||||
TachographFileSession extracted = new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
new TachographFileSessionMetadata("default", "legalrequirements-vehicleunit", "vu", "vu.ddd", "a", 2, "77", "b", false, null),
|
||||
Map.of(),
|
||||
new ExtractionStats(0, 0, 0, 0, 0, 0),
|
||||
java.util.List.of(),
|
||||
Instant.now(),
|
||||
Instant.now().plusSeconds(10)
|
||||
);
|
||||
when(vehicleUnitExtractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||
|
||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "vu");
|
||||
|
||||
assertThat(response.session().driverCardFile()).isFalse();
|
||||
assertThat(response.session().sourceInstanceKey()).isEqualTo("legalrequirements-vehicleunit");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,25 @@ class TachographXmlParserTest {
|
|||
|
||||
@Test
|
||||
void parsesValidDriverCardXml() {
|
||||
TachographXmlParser.ParsedTachographXml parsed = parser.parseDriverCardXml(DriverCardXmlSamples.validDriverCardXml());
|
||||
TachographXmlParser.ParsedTachographXml parsed = parser.parse(DriverCardXmlSamples.validDriverCardXml());
|
||||
|
||||
assertThat(parsed.rootElementName()).isEqualTo("DriverCard");
|
||||
assertThat(parsed.document().getDocumentElement().getNodeName()).isEqualTo("DriverCard");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesValidVehicleUnitXml() {
|
||||
TachographXmlParser.ParsedTachographXml parsed = parser.parse("<VehicleUnit/>");
|
||||
|
||||
assertThat(parsed.rootElementName()).isEqualTo("VehicleUnit");
|
||||
assertThat(parsed.document().getDocumentElement().getNodeName()).isEqualTo("VehicleUnit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidXmlAgainstSchema() {
|
||||
String invalid = "<DriverCard><Identification></DriverCard>";
|
||||
|
||||
assertThatThrownBy(() -> parser.parseDriverCardXml(invalid))
|
||||
assertThatThrownBy(() -> parser.parse(invalid))
|
||||
.isInstanceOf(TachographXmlValidationException.class);
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +42,7 @@ class TachographXmlParserTest {
|
|||
<DriverCard/>
|
||||
""";
|
||||
|
||||
assertThatThrownBy(() -> parser.parseDriverCardXml(xmlWithDoctype))
|
||||
assertThatThrownBy(() -> parser.parse(xmlWithDoctype))
|
||||
.isInstanceOf(TachographXmlValidationException.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
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.io.StringReader;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Document;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
class VehicleUnitXmlExtractionServiceTest {
|
||||
|
||||
private final VehicleUnitXmlExtractionService service = new VehicleUnitXmlExtractionService(
|
||||
new DriverKeyFactory(),
|
||||
new VehicleKeyFactory()
|
||||
);
|
||||
|
||||
@Test
|
||||
void extractsVehicleUnitDriverSessionsFromCardInsertionWithdrawalData() throws Exception {
|
||||
TachographFileSession session = service.extract(
|
||||
new TachographXmlParser.ParsedTachographXml(document(VehicleUnitXmlSamples.vehicleUnitXml()), "VehicleUnit"),
|
||||
new TachographFileSessionMetadata(
|
||||
"default",
|
||||
"legalrequirements-vehicleunit",
|
||||
"sample-vu",
|
||||
"sample-vu.ddd",
|
||||
"abc",
|
||||
10,
|
||||
"42",
|
||||
"def",
|
||||
false,
|
||||
null
|
||||
),
|
||||
Instant.now(),
|
||||
Instant.now().plus(4, ChronoUnit.HOURS)
|
||||
);
|
||||
|
||||
assertThat(session.driversByKey()).hasSize(2);
|
||||
DriverExtractionSession firstDriver = session.driversByKey().get("12:12345678901200");
|
||||
DriverExtractionSession secondDriver = session.driversByKey().get("12:99999999999911");
|
||||
assertThat(firstDriver).isNotNull();
|
||||
assertThat(secondDriver).isNotNull();
|
||||
|
||||
assertThat(firstDriver.driver().surname()).isEqualTo("Muster");
|
||||
assertThat(firstDriver.vehicleRegistrations()).extracting("registrationKey").containsExactly("12:W-1000V");
|
||||
assertThat(firstDriver.vehicles()).extracting("vehicleKey").containsExactly("VINVU123456789012");
|
||||
assertThat(firstDriver.cardVehicleUsageIntervals()).hasSize(1);
|
||||
assertThat(firstDriver.cardVehicleUsageIntervals().get(0).from().toString()).isEqualTo("2026-04-01T08:00Z");
|
||||
assertThat(firstDriver.cardVehicleUsageIntervals().get(0).to().toString()).isEqualTo("2026-04-01T11:00Z");
|
||||
|
||||
assertThat(secondDriver.cardVehicleUsageIntervals()).hasSize(1);
|
||||
assertThat(secondDriver.cardVehicleUsageIntervals().get(0).to().toString()).isEqualTo("2026-04-02T10:00Z");
|
||||
|
||||
assertThat(session.warnings()).extracting("code")
|
||||
.contains("VU_ACTIVITY_NOT_EXTRACTED", "OPEN_VU_CARD_INTERVAL");
|
||||
}
|
||||
|
||||
private Document document(String xml) throws Exception {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(false);
|
||||
return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
final class VehicleUnitXmlSamples {
|
||||
|
||||
private VehicleUnitXmlSamples() {
|
||||
}
|
||||
|
||||
static String vehicleUnitXml() {
|
||||
return """
|
||||
<VehicleUnit>
|
||||
<Overview>
|
||||
<vehicleIdentificationNumber>VINVU123456789012</vehicleIdentificationNumber>
|
||||
<vehicleRegistrationIdentification>
|
||||
<vehicleRegistrationNation>12</vehicleRegistrationNation>
|
||||
<vehicleRegistrationNumber><vehicleRegNumber>W-1000V</vehicleRegNumber></vehicleRegistrationNumber>
|
||||
</vehicleRegistrationIdentification>
|
||||
<vuDownloadablePeriod>
|
||||
<maxDownloadableTime>2026-04-02T10:00:00Z</maxDownloadableTime>
|
||||
</vuDownloadablePeriod>
|
||||
</Overview>
|
||||
<Activities>
|
||||
<vuCardIWData>
|
||||
<vuCardIWRecords>
|
||||
<cardHolderName>
|
||||
<holderSurname><name>Muster</name></holderSurname>
|
||||
<holderFirstNames><name>Max</name></holderFirstNames>
|
||||
</cardHolderName>
|
||||
<fullCardNumber>
|
||||
<cardType>DRIVER_CARD</cardType>
|
||||
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||
<cardNumber>
|
||||
<driverIdentification>123456789012</driverIdentification>
|
||||
<cardReplacementIndex>0</cardReplacementIndex>
|
||||
<cardRenewalIndex>0</cardRenewalIndex>
|
||||
</cardNumber>
|
||||
<generation>2</generation>
|
||||
</fullCardNumber>
|
||||
<cardExpiryDate>2031-04-01T00:00:00Z</cardExpiryDate>
|
||||
<cardInsertionTime>2026-04-01T08:00:00Z</cardInsertionTime>
|
||||
<vehicleOdometerValueAtInsertion>1000</vehicleOdometerValueAtInsertion>
|
||||
<cardSlotNumber>DRIVER</cardSlotNumber>
|
||||
<cardWithdrawalTime>2026-04-01T11:00:00Z</cardWithdrawalTime>
|
||||
<vehicleOdometerValueAtWithdrawal>1100</vehicleOdometerValueAtWithdrawal>
|
||||
<manualInputFlag>NO_ENTRY</manualInputFlag>
|
||||
</vuCardIWRecords>
|
||||
<vuCardIWRecords>
|
||||
<cardHolderName>
|
||||
<holderSurname><name>Test</name></holderSurname>
|
||||
<holderFirstNames><name>Tina</name></holderFirstNames>
|
||||
</cardHolderName>
|
||||
<fullCardNumber>
|
||||
<cardType>DRIVER_CARD</cardType>
|
||||
<cardIssuingMemberState>12</cardIssuingMemberState>
|
||||
<cardNumber>
|
||||
<driverIdentification>999999999999</driverIdentification>
|
||||
<cardReplacementIndex>1</cardReplacementIndex>
|
||||
<cardRenewalIndex>1</cardRenewalIndex>
|
||||
</cardNumber>
|
||||
<generation>2</generation>
|
||||
</fullCardNumber>
|
||||
<cardInsertionTime>2026-04-02T07:30:00Z</cardInsertionTime>
|
||||
<vehicleOdometerValueAtInsertion>1200</vehicleOdometerValueAtInsertion>
|
||||
<cardSlotNumber>DRIVER</cardSlotNumber>
|
||||
<manualInputFlag>MANUAL_ENTRIES</manualInputFlag>
|
||||
</vuCardIWRecords>
|
||||
</vuCardIWData>
|
||||
<vuActivityDailyData/>
|
||||
</Activities>
|
||||
</VehicleUnit>
|
||||
""";
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue