Restore tachographfilesession changes from 2f4ffc2
This commit is contained in:
parent
91c2da02fc
commit
8a106c02c5
|
|
@ -11,6 +11,8 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionListDri
|
|||
import at.procon.eventhub.tachographfilesession.dto.TachographFileSessionSummaryDto;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionProcessingService;
|
||||
import at.procon.eventhub.tachographfilesession.service.TachographFileSessionService;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographUploadWorkflowResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.util.UUID;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
|
@ -44,10 +46,21 @@ public class TachographFileSessionController {
|
|||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(required = false) String tenantKey,
|
||||
@RequestParam(required = false) String sourceInstanceKey,
|
||||
@RequestParam(required = false) String sessionLabel
|
||||
@RequestParam(required = false) String sessionLabel,
|
||||
HttpSession webSession
|
||||
) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(service.createSession(file, tenantKey, sourceInstanceKey, sessionLabel));
|
||||
.body(service.createSession(file, tenantKey, sourceInstanceKey, sessionLabel, webSession));
|
||||
}
|
||||
|
||||
@PostMapping("/workflow/reset")
|
||||
public ResponseEntity<TachographUploadWorkflowResponse> resetUploadWorkflow(HttpSession webSession) {
|
||||
return ResponseEntity.ok(service.resetUploadWorkflow(webSession));
|
||||
}
|
||||
|
||||
@GetMapping("/workflow")
|
||||
public ResponseEntity<TachographUploadWorkflowResponse> getUploadWorkflow(HttpSession webSession) {
|
||||
return ResponseEntity.ok(service.getUploadWorkflow(webSession));
|
||||
}
|
||||
|
||||
@GetMapping("/{sessionId}")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
package at.procon.eventhub.tachographfilesession.dto;
|
||||
|
||||
public record CreateTachographFileSessionResponse(
|
||||
TachographFileSessionSummaryDto session
|
||||
TachographFileSessionSummaryDto session,
|
||||
TachographUploadWorkflowDto workflow,
|
||||
TachographCompositeSessionSummaryDto compositeSession
|
||||
) {
|
||||
|
||||
public CreateTachographFileSessionResponse(TachographFileSessionSummaryDto session) {
|
||||
this(session, null, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package at.procon.eventhub.tachographfilesession.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record TachographUploadWorkflowDto(
|
||||
boolean driverCardUploaded,
|
||||
UUID driverCardSessionId,
|
||||
List<UUID> uploadedSessionIds,
|
||||
List<UUID> vehicleUnitSessionIds,
|
||||
UUID compositeSessionId
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package at.procon.eventhub.tachographfilesession.dto;
|
||||
|
||||
public record TachographUploadWorkflowResponse(
|
||||
TachographUploadWorkflowDto workflow
|
||||
) {
|
||||
}
|
||||
|
|
@ -28,10 +28,8 @@ public class LegalRequirementsClient {
|
|||
|
||||
public LegalRequirementsUploadResult uploadTachographFile(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();
|
||||
CookieManager cookieManager = newCookieManager();
|
||||
HttpClient client = httpClient(config, cookieManager);
|
||||
try {
|
||||
String dataPackageId = uploadDataPackage(client, config, fileBytes, fileName);
|
||||
String xml = downloadXml(client, config, dataPackageId);
|
||||
|
|
@ -43,6 +41,23 @@ public class LegalRequirementsClient {
|
|||
}
|
||||
}
|
||||
|
||||
public LegalRequirementsUploadResult uploadTachographFile(CookieManager cookieManager, byte[] fileBytes, String fileName) {
|
||||
EventHubProperties.LegalRequirements config = properties.getTachographFileSession().getLegalRequirements();
|
||||
HttpClient client = httpClient(config, cookieManager);
|
||||
String dataPackageId = uploadDataPackage(client, config, fileBytes, fileName);
|
||||
String xml = downloadXml(client, config, dataPackageId);
|
||||
return new LegalRequirementsUploadResult(dataPackageId, xml);
|
||||
}
|
||||
|
||||
public void resetSession(CookieManager cookieManager) {
|
||||
EventHubProperties.LegalRequirements config = properties.getTachographFileSession().getLegalRequirements();
|
||||
resetSessionQuietly(httpClient(config, cookieManager), config);
|
||||
}
|
||||
|
||||
public CookieManager newCookieManager() {
|
||||
return new CookieManager(null, CookiePolicy.ACCEPT_ALL);
|
||||
}
|
||||
|
||||
private String uploadDataPackage(HttpClient client, EventHubProperties.LegalRequirements config, byte[] fileBytes, String fileName) {
|
||||
try {
|
||||
String payload = objectMapper.createObjectNode()
|
||||
|
|
@ -115,6 +130,13 @@ public class LegalRequirementsClient {
|
|||
return HttpRequest.newBuilder(URI.create(url)).timeout(timeout);
|
||||
}
|
||||
|
||||
private HttpClient httpClient(EventHubProperties.LegalRequirements config, CookieManager cookieManager) {
|
||||
return HttpClient.newBuilder()
|
||||
.connectTimeout(config.getConnectTimeout())
|
||||
.cookieHandler(cookieManager == null ? newCookieManager() : cookieManager)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String basicAuth(EventHubProperties.LegalRequirements config) {
|
||||
String user = config.getUsername() == null ? "" : config.getUsername();
|
||||
String password = config.getPassword() == null ? "" : config.getPassword();
|
||||
|
|
|
|||
|
|
@ -56,10 +56,20 @@ public class TachographCompositeSessionService {
|
|||
}
|
||||
|
||||
public CreateTachographCompositeSessionResponse createCompositeSession(CreateTachographCompositeSessionRequest request) {
|
||||
if (request == null || request.sessionIds() == null || request.sessionIds().isEmpty()) {
|
||||
return new CreateTachographCompositeSessionResponse(
|
||||
upsertCompositeSession(null, request == null ? null : request.sessionIds(), request == null ? null : request.label())
|
||||
);
|
||||
}
|
||||
|
||||
public TachographCompositeSessionSummaryDto upsertCompositeSession(
|
||||
UUID compositeSessionId,
|
||||
List<UUID> requestedSessionIds,
|
||||
String label
|
||||
) {
|
||||
if (requestedSessionIds == null || requestedSessionIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("sessionIds must not be empty.");
|
||||
}
|
||||
List<UUID> memberSessionIds = request.sessionIds().stream()
|
||||
List<UUID> memberSessionIds = requestedSessionIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
|
@ -72,14 +82,20 @@ public class TachographCompositeSessionService {
|
|||
.filter(value -> value != null && !value.isBlank())
|
||||
.findFirst()
|
||||
.orElse("default");
|
||||
UUID id = compositeSessionId == null ? UUID.randomUUID() : compositeSessionId;
|
||||
Instant createdAt = compositeSessionId == null
|
||||
? Instant.now()
|
||||
: compositeRepository.find(compositeSessionId)
|
||||
.map(TachographCompositeSession::createdAt)
|
||||
.orElseGet(Instant::now);
|
||||
TachographCompositeSession compositeSession = compositeRepository.save(new TachographCompositeSession(
|
||||
UUID.randomUUID(),
|
||||
id,
|
||||
tenantKey,
|
||||
blankToNull(request.label()),
|
||||
blankToNull(label),
|
||||
memberSessionIds,
|
||||
Instant.now()
|
||||
createdAt
|
||||
));
|
||||
return new CreateTachographCompositeSessionResponse(toSummary(compositeSession, sessions));
|
||||
return toSummary(compositeSession, sessions);
|
||||
}
|
||||
|
||||
public TachographCompositeSessionSummaryDto getCompositeSession(UUID compositeSessionId) {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,18 @@ 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.TachographCompositeSessionSummaryDto;
|
||||
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.dto.TachographUploadWorkflowResponse;
|
||||
import at.procon.eventhub.tachographfilesession.model.DriverExtractionSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSession;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographFileSessionMetadata;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.net.CookieManager;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
|
@ -27,9 +31,14 @@ import org.springframework.web.multipart.MultipartFile;
|
|||
public class TachographFileSessionService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TachographFileSessionService.class);
|
||||
private static final String LEGAL_REQUIREMENTS_COOKIE_MANAGER_ATTRIBUTE =
|
||||
TachographFileSessionService.class.getName() + ".legalRequirementsCookieManager";
|
||||
private static final String WORKFLOW_STATE_ATTRIBUTE =
|
||||
TachographFileSessionService.class.getName() + ".uploadWorkflowState";
|
||||
|
||||
private final EventHubProperties properties;
|
||||
private final TachographFileSessionRepository repository;
|
||||
private final TachographCompositeSessionService compositeSessionService;
|
||||
private final LegalRequirementsClient legalRequirementsClient;
|
||||
private final TachographXmlParser tachographXmlParser;
|
||||
private final DriverCardXmlExtractionService driverCardExtractionService;
|
||||
|
|
@ -38,6 +47,7 @@ public class TachographFileSessionService {
|
|||
public TachographFileSessionService(
|
||||
EventHubProperties properties,
|
||||
TachographFileSessionRepository repository,
|
||||
TachographCompositeSessionService compositeSessionService,
|
||||
LegalRequirementsClient legalRequirementsClient,
|
||||
TachographXmlParser tachographXmlParser,
|
||||
DriverCardXmlExtractionService driverCardExtractionService,
|
||||
|
|
@ -45,6 +55,7 @@ public class TachographFileSessionService {
|
|||
) {
|
||||
this.properties = properties;
|
||||
this.repository = repository;
|
||||
this.compositeSessionService = compositeSessionService;
|
||||
this.legalRequirementsClient = legalRequirementsClient;
|
||||
this.tachographXmlParser = tachographXmlParser;
|
||||
this.driverCardExtractionService = driverCardExtractionService;
|
||||
|
|
@ -55,14 +66,21 @@ public class TachographFileSessionService {
|
|||
MultipartFile file,
|
||||
String tenantKey,
|
||||
String sourceInstanceKey,
|
||||
String sessionLabel
|
||||
String sessionLabel,
|
||||
HttpSession webSession
|
||||
) {
|
||||
try {
|
||||
if (webSession == null) {
|
||||
throw new IllegalArgumentException("HTTP session is required for tachograph upload workflow.");
|
||||
}
|
||||
validateFile(file);
|
||||
byte[] fileBytes = file.getBytes();
|
||||
validateFileBytes(fileBytes);
|
||||
CookieManager cookieManager = cookieManager(webSession);
|
||||
TachographUploadWorkflowState workflowState = workflowState(webSession);
|
||||
long uploadStartedAt = System.nanoTime();
|
||||
LegalRequirementsUploadResult uploadResult = legalRequirementsClient.uploadTachographFile(fileBytes, file.getOriginalFilename());
|
||||
LegalRequirementsUploadResult uploadResult =
|
||||
legalRequirementsClient.uploadTachographFile(cookieManager, fileBytes, file.getOriginalFilename());
|
||||
long uploadDurationMs = elapsedMillis(uploadStartedAt);
|
||||
|
||||
long parseStartedAt = System.nanoTime();
|
||||
|
|
@ -72,6 +90,7 @@ public class TachographFileSessionService {
|
|||
Instant createdAt = Instant.now();
|
||||
Instant expiresAt = createdAt.plus(properties.getTachographFileSession().getTtl());
|
||||
boolean driverCardFile = "DriverCard".equals(parsedXml.rootElementName());
|
||||
validateWorkflowSequence(webSession, workflowState, cookieManager, driverCardFile);
|
||||
TachographFileSessionMetadata metadata = new TachographFileSessionMetadata(
|
||||
tenant(tenantKey),
|
||||
sourceInstance(sourceInstanceKey, driverCardFile),
|
||||
|
|
@ -100,7 +119,10 @@ public class TachographFileSessionService {
|
|||
fileBytes.length
|
||||
);
|
||||
TachographFileSession saved = repository.save(session);
|
||||
return new CreateTachographFileSessionResponse(toSummary(saved));
|
||||
workflowState.recordUploadedSession(saved.sessionId(), driverCardFile);
|
||||
TachographCompositeSessionSummaryDto compositeSession = updateCompositeSession(workflowState, sessionLabel);
|
||||
webSession.setAttribute(WORKFLOW_STATE_ATTRIBUTE, workflowState);
|
||||
return new CreateTachographFileSessionResponse(toSummary(saved), workflowState.toDto(), compositeSession);
|
||||
} catch (Exception e) {
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
|
|
@ -109,6 +131,27 @@ public class TachographFileSessionService {
|
|||
}
|
||||
}
|
||||
|
||||
public TachographUploadWorkflowResponse resetUploadWorkflow(HttpSession webSession) {
|
||||
if (webSession == null) {
|
||||
throw new IllegalArgumentException("HTTP session is required for tachograph upload workflow.");
|
||||
}
|
||||
CookieManager existingCookieManager = existingCookieManager(webSession);
|
||||
if (existingCookieManager != null) {
|
||||
legalRequirementsClient.resetSession(existingCookieManager);
|
||||
}
|
||||
webSession.setAttribute(LEGAL_REQUIREMENTS_COOKIE_MANAGER_ATTRIBUTE, legalRequirementsClient.newCookieManager());
|
||||
TachographUploadWorkflowState state = new TachographUploadWorkflowState();
|
||||
webSession.setAttribute(WORKFLOW_STATE_ATTRIBUTE, state);
|
||||
return new TachographUploadWorkflowResponse(state.toDto());
|
||||
}
|
||||
|
||||
public TachographUploadWorkflowResponse getUploadWorkflow(HttpSession webSession) {
|
||||
if (webSession == null) {
|
||||
throw new IllegalArgumentException("HTTP session is required for tachograph upload workflow.");
|
||||
}
|
||||
return new TachographUploadWorkflowResponse(workflowState(webSession).toDto());
|
||||
}
|
||||
|
||||
public TachographFileSessionSummaryDto getSession(UUID sessionId) {
|
||||
return toSummary(requireSession(sessionId));
|
||||
}
|
||||
|
|
@ -146,6 +189,27 @@ public class TachographFileSessionService {
|
|||
return repository.find(sessionId).orElseThrow(() -> new TachographFileSessionNotFoundException(sessionId));
|
||||
}
|
||||
|
||||
private TachographCompositeSessionSummaryDto updateCompositeSession(
|
||||
TachographUploadWorkflowState workflowState,
|
||||
String sessionLabel
|
||||
) {
|
||||
if (workflowState.uploadedSessionIds().size() < 2) {
|
||||
return null;
|
||||
}
|
||||
TachographCompositeSessionSummaryDto compositeSession = compositeSessionService.upsertCompositeSession(
|
||||
workflowState.compositeSessionId(),
|
||||
workflowState.uploadedSessionIds(),
|
||||
compositeSessionLabel(sessionLabel)
|
||||
);
|
||||
workflowState.compositeSessionId(compositeSession.compositeSessionId());
|
||||
return compositeSession;
|
||||
}
|
||||
|
||||
private String compositeSessionLabel(String sessionLabel) {
|
||||
String normalizedLabel = blankToNull(sessionLabel);
|
||||
return normalizedLabel == null ? "tachograph-upload-workflow" : normalizedLabel;
|
||||
}
|
||||
|
||||
private TachographFileSessionSummaryDto toSummary(TachographFileSession session) {
|
||||
return new TachographFileSessionSummaryDto(
|
||||
session.sessionId(),
|
||||
|
|
@ -207,6 +271,63 @@ public class TachographFileSessionService {
|
|||
return driverCardFile ? "legalrequirements-drivercard" : "legalrequirements-vehicleunit";
|
||||
}
|
||||
|
||||
private void validateWorkflowSequence(
|
||||
HttpSession webSession,
|
||||
TachographUploadWorkflowState workflowState,
|
||||
CookieManager cookieManager,
|
||||
boolean driverCardFile
|
||||
) {
|
||||
try {
|
||||
validateWorkflowSequenceInternal(workflowState, driverCardFile);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
resetWorkflowState(webSession, cookieManager);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateWorkflowSequenceInternal(TachographUploadWorkflowState workflowState, boolean driverCardFile) {
|
||||
if (!workflowState.driverCardUploaded() && !workflowState.uploadedSessionIds().isEmpty() && driverCardFile) {
|
||||
throw new IllegalArgumentException("Workflow already contains uploaded files. Reset the workflow before starting a new driver card import.");
|
||||
}
|
||||
if (!workflowState.driverCardUploaded() && !driverCardFile) {
|
||||
throw new IllegalArgumentException("Driver card file must be uploaded first after workflow reset.");
|
||||
}
|
||||
if (workflowState.driverCardUploaded() && driverCardFile) {
|
||||
throw new IllegalArgumentException("Workflow already contains a driver card file. Upload vehicle-unit files next or reset the workflow.");
|
||||
}
|
||||
}
|
||||
|
||||
private CookieManager cookieManager(HttpSession webSession) {
|
||||
CookieManager existing = existingCookieManager(webSession);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
CookieManager created = legalRequirementsClient.newCookieManager();
|
||||
webSession.setAttribute(LEGAL_REQUIREMENTS_COOKIE_MANAGER_ATTRIBUTE, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private CookieManager existingCookieManager(HttpSession webSession) {
|
||||
Object value = webSession.getAttribute(LEGAL_REQUIREMENTS_COOKIE_MANAGER_ATTRIBUTE);
|
||||
return value instanceof CookieManager cookieManager ? cookieManager : null;
|
||||
}
|
||||
|
||||
private TachographUploadWorkflowState workflowState(HttpSession webSession) {
|
||||
Object value = webSession.getAttribute(WORKFLOW_STATE_ATTRIBUTE);
|
||||
if (value instanceof TachographUploadWorkflowState state) {
|
||||
return state;
|
||||
}
|
||||
TachographUploadWorkflowState state = new TachographUploadWorkflowState();
|
||||
webSession.setAttribute(WORKFLOW_STATE_ATTRIBUTE, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
private void resetWorkflowState(HttpSession webSession, CookieManager cookieManager) {
|
||||
legalRequirementsClient.resetSession(cookieManager);
|
||||
webSession.setAttribute(LEGAL_REQUIREMENTS_COOKIE_MANAGER_ATTRIBUTE, legalRequirementsClient.newCookieManager());
|
||||
webSession.setAttribute(WORKFLOW_STATE_ATTRIBUTE, new TachographUploadWorkflowState());
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
package at.procon.eventhub.tachographfilesession.service;
|
||||
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographUploadWorkflowDto;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
final class TachographUploadWorkflowState {
|
||||
|
||||
private UUID driverCardSessionId;
|
||||
private final List<UUID> uploadedSessionIds = new ArrayList<>();
|
||||
private final List<UUID> vehicleUnitSessionIds = new ArrayList<>();
|
||||
private UUID compositeSessionId;
|
||||
|
||||
boolean driverCardUploaded() {
|
||||
return driverCardSessionId != null;
|
||||
}
|
||||
|
||||
UUID driverCardSessionId() {
|
||||
return driverCardSessionId;
|
||||
}
|
||||
|
||||
List<UUID> uploadedSessionIds() {
|
||||
return List.copyOf(uploadedSessionIds);
|
||||
}
|
||||
|
||||
List<UUID> vehicleUnitSessionIds() {
|
||||
return List.copyOf(vehicleUnitSessionIds);
|
||||
}
|
||||
|
||||
UUID compositeSessionId() {
|
||||
return compositeSessionId;
|
||||
}
|
||||
|
||||
void recordUploadedSession(UUID sessionId, boolean driverCardFile) {
|
||||
uploadedSessionIds.add(sessionId);
|
||||
if (driverCardFile) {
|
||||
driverCardSessionId = sessionId;
|
||||
return;
|
||||
}
|
||||
vehicleUnitSessionIds.add(sessionId);
|
||||
}
|
||||
|
||||
void compositeSessionId(UUID compositeSessionId) {
|
||||
this.compositeSessionId = compositeSessionId;
|
||||
}
|
||||
|
||||
TachographUploadWorkflowDto toDto() {
|
||||
return new TachographUploadWorkflowDto(
|
||||
driverCardUploaded(),
|
||||
driverCardSessionId,
|
||||
uploadedSessionIds(),
|
||||
vehicleUnitSessionIds(),
|
||||
compositeSessionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,10 @@ import java.io.ByteArrayInputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
|
@ -21,6 +25,9 @@ import org.xml.sax.SAXException;
|
|||
public class TachographXmlParser {
|
||||
|
||||
private static final String JAXP_MAX_OCCUR_LIMIT = "http://www.oracle.com/xml/jaxp/properties/maxOccurLimit";
|
||||
private static final Pattern MANUFACTURER_SPECIFIC_ERROR_CODE_PATTERN = Pattern.compile(
|
||||
"(<manufacturerSpecificErrorCode>)([^<\\s]+)(</manufacturerSpecificErrorCode>)"
|
||||
);
|
||||
|
||||
private final Schema schema;
|
||||
|
||||
|
|
@ -62,11 +69,45 @@ public class TachographXmlParser {
|
|||
}
|
||||
}
|
||||
|
||||
private String normalizeXmlContent(String xmlContent) {
|
||||
String normalizeXmlContent(String xmlContent) {
|
||||
if (xmlContent == null || xmlContent.isEmpty()) {
|
||||
return xmlContent;
|
||||
}
|
||||
return xmlContent.charAt(0) == '\uFEFF' ? xmlContent.substring(1) : xmlContent;
|
||||
String normalized = xmlContent.charAt(0) == '\uFEFF' ? xmlContent.substring(1) : xmlContent;
|
||||
return normalizeManufacturerSpecificErrorCodes(normalized);
|
||||
}
|
||||
|
||||
private String normalizeManufacturerSpecificErrorCodes(String xmlContent) {
|
||||
Matcher matcher = MANUFACTURER_SPECIFIC_ERROR_CODE_PATTERN.matcher(xmlContent);
|
||||
StringBuffer normalized = new StringBuffer(xmlContent.length());
|
||||
while (matcher.find()) {
|
||||
String rawValue = matcher.group(2);
|
||||
String replacementValue = normalizeThreeByteOctetString(rawValue);
|
||||
matcher.appendReplacement(
|
||||
normalized,
|
||||
Matcher.quoteReplacement(matcher.group(1) + replacementValue + matcher.group(3))
|
||||
);
|
||||
}
|
||||
matcher.appendTail(normalized);
|
||||
return normalized.toString();
|
||||
}
|
||||
|
||||
private String normalizeThreeByteOctetString(String rawValue) {
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return rawValue;
|
||||
}
|
||||
if (rawValue.matches("(?i)[0-9a-f]{6}")) {
|
||||
return rawValue.toUpperCase();
|
||||
}
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(rawValue);
|
||||
if (decoded.length == 3) {
|
||||
return HexFormat.of().formatHex(decoded).toUpperCase();
|
||||
}
|
||||
return rawValue;
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return rawValue;
|
||||
}
|
||||
}
|
||||
|
||||
private Schema loadSchema() {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryD
|
|||
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.dto.TachographUploadWorkflowDto;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographUploadWorkflowResponse;
|
||||
import at.procon.eventhub.tachographfilesession.model.ExtractionStats;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent;
|
||||
import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent;
|
||||
|
|
@ -34,6 +36,7 @@ import java.time.Instant;
|
|||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
|
@ -49,6 +52,7 @@ class TachographFileSessionControllerTest {
|
|||
.setControllerAdvice(new TachographFileSessionExceptionHandler())
|
||||
.build();
|
||||
UUID sessionId = UUID.randomUUID();
|
||||
MockHttpSession webSession = new MockHttpSession();
|
||||
TachographFileDriverSummaryDto driver = new TachographFileDriverSummaryDto("12:123", "Muster", "Max", "12", "123", 3, 2);
|
||||
TachographFileSessionSummaryDto summary = new TachographFileSessionSummaryDto(
|
||||
sessionId,
|
||||
|
|
@ -64,8 +68,26 @@ class TachographFileSessionControllerTest {
|
|||
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.createSession(
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
eq("default"),
|
||||
eq("src"),
|
||||
eq("sample"),
|
||||
org.mockito.ArgumentMatchers.any(jakarta.servlet.http.HttpSession.class)
|
||||
))
|
||||
.thenReturn(new CreateTachographFileSessionResponse(
|
||||
summary,
|
||||
new TachographUploadWorkflowDto(true, sessionId, List.of(sessionId), List.of(), null),
|
||||
null
|
||||
));
|
||||
when(service.resetUploadWorkflow(org.mockito.ArgumentMatchers.any(jakarta.servlet.http.HttpSession.class)))
|
||||
.thenReturn(new TachographUploadWorkflowResponse(
|
||||
new TachographUploadWorkflowDto(false, null, List.of(), List.of(), null)
|
||||
));
|
||||
when(service.getUploadWorkflow(org.mockito.ArgumentMatchers.any(jakarta.servlet.http.HttpSession.class)))
|
||||
.thenReturn(new TachographUploadWorkflowResponse(
|
||||
new TachographUploadWorkflowDto(true, sessionId, List.of(sessionId), List.of(), null)
|
||||
));
|
||||
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(), List.of()));
|
||||
|
|
@ -319,13 +341,23 @@ class TachographFileSessionControllerTest {
|
|||
));
|
||||
when(service.deleteSession(sessionId)).thenReturn(new TachographFileSessionDeleteResponse(sessionId, true));
|
||||
|
||||
mockMvc.perform(post("/api/eventhub/tachograph-file-sessions/workflow/reset").session(webSession))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.workflow.driverCardUploaded").value(false));
|
||||
|
||||
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"))
|
||||
.param("sessionLabel", "sample")
|
||||
.session(webSession))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.session.sessionId").value(sessionId.toString()));
|
||||
.andExpect(jsonPath("$.session.sessionId").value(sessionId.toString()))
|
||||
.andExpect(jsonPath("$.workflow.driverCardUploaded").value(true));
|
||||
|
||||
mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/workflow").session(webSession))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.workflow.driverCardSessionId").value(sessionId.toString()));
|
||||
|
||||
mockMvc.perform(get("/api/eventhub/tachograph-file-sessions/{sessionId}", sessionId))
|
||||
.andExpect(status().isOk())
|
||||
|
|
|
|||
|
|
@ -3,22 +3,27 @@ package at.procon.eventhub.tachographfilesession.service;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import at.procon.eventhub.config.EventHubProperties;
|
||||
import at.procon.eventhub.tachographfilesession.dto.CreateTachographFileSessionResponse;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographCompositeSessionSummaryDto;
|
||||
import at.procon.eventhub.tachographfilesession.dto.TachographFileDriverSummaryDto;
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
|
||||
class TachographFileSessionServiceTest {
|
||||
|
||||
|
|
@ -26,6 +31,7 @@ class TachographFileSessionServiceTest {
|
|||
void createsAndLoadsSession() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
TachographCompositeSessionService compositeSessionService = Mockito.mock(TachographCompositeSessionService.class);
|
||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
|
|
@ -33,14 +39,17 @@ class TachographFileSessionServiceTest {
|
|||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
compositeSessionService,
|
||||
client,
|
||||
parser,
|
||||
driverCardExtractor,
|
||||
vehicleUnitExtractor
|
||||
);
|
||||
when(client.newCookieManager()).thenReturn(new java.net.CookieManager());
|
||||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", "abc".getBytes(StandardCharsets.UTF_8));
|
||||
when(client.uploadTachographFile(any(), eq("sample.ddd"))).thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
||||
when(client.uploadTachographFile(any(java.net.CookieManager.class), any(), eq("sample.ddd")))
|
||||
.thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
||||
TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||
when(parsed.rootElementName()).thenReturn("DriverCard");
|
||||
when(parser.parse("<DriverCard/>")).thenReturn(parsed);
|
||||
|
|
@ -55,9 +64,12 @@ class TachographFileSessionServiceTest {
|
|||
);
|
||||
when(driverCardExtractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||
|
||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "sample");
|
||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "sample", new MockHttpSession());
|
||||
|
||||
assertThat(response.session().sessionId()).isEqualTo(extracted.sessionId());
|
||||
assertThat(response.workflow()).isNotNull();
|
||||
assertThat(response.workflow().driverCardUploaded()).isTrue();
|
||||
assertThat(response.compositeSession()).isNull();
|
||||
assertThat(service.getSession(extracted.sessionId()).sessionId()).isEqualTo(extracted.sessionId());
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +78,7 @@ class TachographFileSessionServiceTest {
|
|||
EventHubProperties properties = new EventHubProperties();
|
||||
properties.getTachographFileSession().setMaxFileSizeBytes(1024);
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
TachographCompositeSessionService compositeSessionService = Mockito.mock(TachographCompositeSessionService.class);
|
||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
|
|
@ -73,6 +86,7 @@ class TachographFileSessionServiceTest {
|
|||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
compositeSessionService,
|
||||
client,
|
||||
parser,
|
||||
driverCardExtractor,
|
||||
|
|
@ -81,17 +95,18 @@ class TachographFileSessionServiceTest {
|
|||
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sample.ddd", "application/octet-stream", new byte[1025]);
|
||||
|
||||
assertThatThrownBy(() -> service.createSession(file, null, null, "sample"))
|
||||
assertThatThrownBy(() -> service.createSession(file, null, null, "sample", new MockHttpSession()))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("size limit");
|
||||
|
||||
verifyNoInteractions(client, parser, driverCardExtractor, vehicleUnitExtractor);
|
||||
verifyNoInteractions(compositeSessionService, client, parser, driverCardExtractor, vehicleUnitExtractor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesVehicleUnitDefaultsAndExtractorForVehicleUnitXml() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
TachographCompositeSessionService compositeSessionService = Mockito.mock(TachographCompositeSessionService.class);
|
||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||
DriverCardXmlExtractionService driverCardExtractor = Mockito.mock(DriverCardXmlExtractionService.class);
|
||||
|
|
@ -99,14 +114,17 @@ class TachographFileSessionServiceTest {
|
|||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
compositeSessionService,
|
||||
client,
|
||||
parser,
|
||||
driverCardExtractor,
|
||||
vehicleUnitExtractor
|
||||
);
|
||||
when(client.newCookieManager()).thenReturn(new java.net.CookieManager());
|
||||
|
||||
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/>"));
|
||||
when(client.uploadTachographFile(any(java.net.CookieManager.class), 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);
|
||||
|
|
@ -121,9 +139,74 @@ class TachographFileSessionServiceTest {
|
|||
);
|
||||
when(vehicleUnitExtractor.extract(eq(parsed), any(), any(), any())).thenReturn(extracted);
|
||||
|
||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "vu");
|
||||
MockHttpSession webSession = new MockHttpSession();
|
||||
service.resetUploadWorkflow(webSession);
|
||||
MockMultipartFile driverCardFile = new MockMultipartFile("file", "driver.ddd", "application/octet-stream", "dc".getBytes(StandardCharsets.UTF_8));
|
||||
when(client.uploadTachographFile(any(java.net.CookieManager.class), any(), eq("driver.ddd")))
|
||||
.thenReturn(new LegalRequirementsUploadResult("42", "<DriverCard/>"));
|
||||
TachographXmlParser.ParsedTachographXml driverParsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class);
|
||||
when(driverParsed.rootElementName()).thenReturn("DriverCard");
|
||||
when(parser.parse("<DriverCard/>")).thenReturn(driverParsed);
|
||||
TachographFileSession driverExtracted = new TachographFileSession(
|
||||
UUID.randomUUID(),
|
||||
new TachographFileSessionMetadata("default", "legalrequirements-drivercard", "sample", "driver.ddd", "a", 2, "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(driverCardExtractor.extract(eq(driverParsed), any(), any(), any())).thenReturn(driverExtracted);
|
||||
when(compositeSessionService.upsertCompositeSession(any(), anyList(), any()))
|
||||
.thenReturn(new TachographCompositeSessionSummaryDto(
|
||||
UUID.randomUUID(),
|
||||
"default",
|
||||
"vu",
|
||||
List.of(driverExtracted.sessionId(), extracted.sessionId()),
|
||||
List.<TachographFileDriverSummaryDto>of(),
|
||||
Instant.now()
|
||||
));
|
||||
|
||||
service.createSession(driverCardFile, null, null, "sample", webSession);
|
||||
CreateTachographFileSessionResponse response = service.createSession(file, null, null, "vu", webSession);
|
||||
|
||||
assertThat(response.session().driverCardFile()).isFalse();
|
||||
assertThat(response.session().sourceInstanceKey()).isEqualTo("legalrequirements-vehicleunit");
|
||||
assertThat(response.compositeSession()).isNotNull();
|
||||
assertThat(response.workflow().uploadedSessionIds()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsVehicleUnitBeforeDriverCardInWorkflow() {
|
||||
EventHubProperties properties = new EventHubProperties();
|
||||
TachographFileSessionRepository repository = new InMemoryTachographFileSessionRepository(properties);
|
||||
TachographCompositeSessionService compositeSessionService = Mockito.mock(TachographCompositeSessionService.class);
|
||||
LegalRequirementsClient client = Mockito.mock(LegalRequirementsClient.class);
|
||||
TachographXmlParser parser = Mockito.mock(TachographXmlParser.class);
|
||||
TachographFileSessionService service = new TachographFileSessionService(
|
||||
properties,
|
||||
repository,
|
||||
compositeSessionService,
|
||||
client,
|
||||
parser,
|
||||
Mockito.mock(DriverCardXmlExtractionService.class),
|
||||
Mockito.mock(VehicleUnitXmlExtractionService.class)
|
||||
);
|
||||
when(client.newCookieManager()).thenReturn(new java.net.CookieManager());
|
||||
when(client.uploadTachographFile(any(java.net.CookieManager.class), 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);
|
||||
|
||||
assertThatThrownBy(() -> service.createSession(
|
||||
new MockMultipartFile("file", "vu.ddd", "application/octet-stream", "vu".getBytes(StandardCharsets.UTF_8)),
|
||||
null,
|
||||
null,
|
||||
"vu",
|
||||
new MockHttpSession()
|
||||
))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Driver card file must be uploaded first");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,26 @@ class TachographXmlParserTest {
|
|||
.isInstanceOf(TachographXmlValidationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesManufacturerSpecificErrorCodeFromBase64ToHex() {
|
||||
String normalized = parser.normalizeXmlContent("""
|
||||
<VehicleUnit>
|
||||
<EventsFaults>
|
||||
<vuFaultData>
|
||||
<vuFaultRecords>
|
||||
<manufacturerSpecificEventFaultData>
|
||||
<manufacturerCode>0</manufacturerCode>
|
||||
<manufacturerSpecificErrorCode>////</manufacturerSpecificErrorCode>
|
||||
</manufacturerSpecificEventFaultData>
|
||||
</vuFaultRecords>
|
||||
</vuFaultData>
|
||||
</EventsFaults>
|
||||
</VehicleUnit>
|
||||
""");
|
||||
|
||||
assertThat(normalized).contains("<manufacturerSpecificErrorCode>FFFFFF</manufacturerSpecificErrorCode>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsXmlWithDoctype() {
|
||||
String xmlWithDoctype = """
|
||||
|
|
|
|||
Loading…
Reference in New Issue