diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java index 0504df4..7a9c714 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionController.java @@ -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 resetUploadWorkflow(HttpSession webSession) { + return ResponseEntity.ok(service.resetUploadWorkflow(webSession)); + } + + @GetMapping("/workflow") + public ResponseEntity getUploadWorkflow(HttpSession webSession) { + return ResponseEntity.ok(service.getUploadWorkflow(webSession)); } @GetMapping("/{sessionId}") diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographFileSessionResponse.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographFileSessionResponse.java index aea1251..7fb086b 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographFileSessionResponse.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/CreateTachographFileSessionResponse.java @@ -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); + } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographUploadWorkflowDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographUploadWorkflowDto.java new file mode 100644 index 0000000..c86fcbe --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographUploadWorkflowDto.java @@ -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 uploadedSessionIds, + List vehicleUnitSessionIds, + UUID compositeSessionId +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographUploadWorkflowResponse.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographUploadWorkflowResponse.java new file mode 100644 index 0000000..4c018ff --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographUploadWorkflowResponse.java @@ -0,0 +1,6 @@ +package at.procon.eventhub.tachographfilesession.dto; + +public record TachographUploadWorkflowResponse( + TachographUploadWorkflowDto workflow +) { +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/LegalRequirementsClient.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/LegalRequirementsClient.java index f21ec89..82cd935 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/LegalRequirementsClient.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/LegalRequirementsClient.java @@ -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(); diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java index 1b3a68b..de60868 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographCompositeSessionService.java @@ -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 requestedSessionIds, + String label + ) { + if (requestedSessionIds == null || requestedSessionIds.isEmpty()) { throw new IllegalArgumentException("sessionIds must not be empty."); } - List memberSessionIds = request.sessionIds().stream() + List 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) { diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java index 0c816f1..a1f6a44 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionService.java @@ -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(); } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographUploadWorkflowState.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographUploadWorkflowState.java new file mode 100644 index 0000000..c7155f6 --- /dev/null +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographUploadWorkflowState.java @@ -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 uploadedSessionIds = new ArrayList<>(); + private final List vehicleUnitSessionIds = new ArrayList<>(); + private UUID compositeSessionId; + + boolean driverCardUploaded() { + return driverCardSessionId != null; + } + + UUID driverCardSessionId() { + return driverCardSessionId; + } + + List uploadedSessionIds() { + return List.copyOf(uploadedSessionIds); + } + + List 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 + ); + } +} diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParser.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParser.java index 4757f8a..2f70d1b 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParser.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParser.java @@ -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( + "()([^<\\s]+)()" + ); 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() { diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java index bf83b93..c63f2ca 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/api/TachographFileSessionControllerTest.java @@ -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()) diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionServiceTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionServiceTest.java index 7cace0d..88dc3b8 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionServiceTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographFileSessionServiceTest.java @@ -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", "")); + when(client.uploadTachographFile(any(java.net.CookieManager.class), any(), eq("sample.ddd"))) + .thenReturn(new LegalRequirementsUploadResult("42", "")); TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class); when(parsed.rootElementName()).thenReturn("DriverCard"); when(parser.parse("")).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", "")); + when(client.uploadTachographFile(any(java.net.CookieManager.class), any(), eq("vu.ddd"))) + .thenReturn(new LegalRequirementsUploadResult("77", "")); TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class); when(parsed.rootElementName()).thenReturn("VehicleUnit"); when(parser.parse("")).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", "")); + TachographXmlParser.ParsedTachographXml driverParsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class); + when(driverParsed.rootElementName()).thenReturn("DriverCard"); + when(parser.parse("")).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.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", "")); + TachographXmlParser.ParsedTachographXml parsed = Mockito.mock(TachographXmlParser.ParsedTachographXml.class); + when(parsed.rootElementName()).thenReturn("VehicleUnit"); + when(parser.parse("")).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"); } } diff --git a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParserTest.java b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParserTest.java index 1d63ed1..da4e479 100644 --- a/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParserTest.java +++ b/src/test/java/at/procon/eventhub/tachographfilesession/service/TachographXmlParserTest.java @@ -43,6 +43,26 @@ class TachographXmlParserTest { .isInstanceOf(TachographXmlValidationException.class); } + @Test + void normalizesManufacturerSpecificErrorCodeFromBase64ToHex() { + String normalized = parser.normalizeXmlContent(""" + + + + + + 0 + //// + + + + + + """); + + assertThat(normalized).contains("FFFFFF"); + } + @Test void rejectsXmlWithDoctype() { String xmlWithDoctype = """