From 1b974db3211bbd360d52bd030a139e99be224c0c Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 1 Apr 2026 22:09:04 +0200 Subject: [PATCH] M3-AP-007 Bootstrap- und CLI-Anpassungen Pre-Version --- .../config/StartConfigurationValidator.java | 2 + .../umbenenner/bootstrap/BootstrapRunner.java | 83 ++++++++----------- .../bootstrap/BootstrapRunnerTest.java | 24 ++++++ 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java index 1144609..6a2270c 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java @@ -70,6 +70,8 @@ public class StartConfigurationValidator { errors.add("- source.folder: path does not exist: " + sourceFolder); } else if (!Files.isDirectory(sourceFolder)) { errors.add("- source.folder: path is not a directory: " + sourceFolder); + } else if (!Files.isReadable(sourceFolder)) { + errors.add("- source.folder: directory is not readable: " + sourceFolder); } } diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index 1ddbaf0..de910da 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -6,15 +6,15 @@ import org.apache.logging.log4j.Logger; import de.gecheckt.pdf.umbenenner.adapter.inbound.cli.SchedulerBatchCommand; import de.gecheckt.pdf.umbenenner.adapter.outbound.configuration.PropertiesConfigurationPortAdapter; import de.gecheckt.pdf.umbenenner.adapter.outbound.lock.FilesystemRunLockPortAdapter; +import de.gecheckt.pdf.umbenenner.adapter.outbound.pdfextraction.PdfTextExtractionPortAdapter; +import de.gecheckt.pdf.umbenenner.adapter.outbound.sourcedocument.SourceDocumentCandidatesPortAdapter; import de.gecheckt.pdf.umbenenner.application.config.InvalidStartConfigurationException; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfigurationValidator; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort; -import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; -import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentCandidatesPort; import de.gecheckt.pdf.umbenenner.application.usecase.M2BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.RunId; @@ -27,12 +27,22 @@ import java.util.UUID; /** * Manual bootstrap runner that constructs the object graph and drives the startup flow. *

- * AP-003 Implementation: Creates all required components using plain Java constructor injection - * and executes the minimal no-op batch processing path. + * Responsibilities: + *

    + *
  1. Load and validate the startup configuration
  2. + *
  3. Resolve the run-lock file path (with default fallback)
  4. + *
  5. Create and wire all ports and adapters
  6. + *
  7. Start the CLI adapter and execute the batch use case
  8. + *
  9. Map the batch outcome to a process exit code
  10. + *
*

- * AP-005: CLI adapter and bootstrap wiring for M2 batch orchestration with run lock integration. - *

- * AP-006: Validates configuration before processing begins, returns exit code 1 on invalid config. + * Exit code semantics (M3): + *

*/ public class BootstrapRunner { @@ -72,10 +82,8 @@ public class BootstrapRunner { * Functional interface for creating a RunBatchProcessingUseCase. *

* Receives the already-loaded and validated {@link StartConfiguration} and run lock port. - *

- * Note: The use case signature may accept additional ports for M3+ functionality, - * but bootstrap provides No-Op implementations for now (AP-005 scope). - * Full M3 adapter wiring will be completed in AP-007 (Bootstrap expansion). + * The factory is responsible for creating and wiring any additional outbound ports + * required by the use case (e.g., source document port, PDF extraction port). */ @FunctionalInterface public interface UseCaseFactory { @@ -93,16 +101,23 @@ public class BootstrapRunner { /** * Creates the BootstrapRunner with default factories for production use. *

- * AP-006: Uses FilesystemRunLockPortAdapter for file-based exclusive run locking. + * Wires the full M3 processing pipeline: + *

*/ public BootstrapRunner() { this.configPortFactory = PropertiesConfigurationPortAdapter::new; this.runLockPortFactory = FilesystemRunLockPortAdapter::new; this.validatorFactory = StartConfigurationValidator::new; - // AP-005: Use case accepts M3 ports, but bootstrap provides No-Op implementations (M2 scope) - // AP-007 will wire real M3 adapters; for now, M2 uses No-Op ports - this.useCaseFactory = (config, lock) -> - new M2BatchRunProcessingUseCase(config, lock, new NoOpSourceCandidatesPort(), new NoOpExtractionPort()); + this.useCaseFactory = (config, lock) -> new M2BatchRunProcessingUseCase( + config, + lock, + new SourceDocumentCandidatesPortAdapter(config.sourceFolder()), + new PdfTextExtractionPortAdapter()); this.commandFactory = SchedulerBatchCommand::new; } @@ -163,10 +178,9 @@ public class BootstrapRunner { BatchRunContext runContext = new BatchRunContext(runId, Instant.now()); LOG.info("Batch run started. RunId: {}", runId); - // Step 6: Create the use case with the validated config and run lock (application layer) + // Step 6: Create the use case with the validated config and run lock (application layer). // Config is passed directly; the use case does not re-read the properties file. - // Note: The use case signature includes M3 ports, but bootstrap (M2 scope) provides No-Op implementations. - // Real M3 adapter wiring will be completed in AP-007. + // M3 adapters (source document port, PDF extraction port) are wired by the factory. RunBatchProcessingUseCase useCase = useCaseFactory.create(config, runLockPort); // Step 7: Create the CLI command adapter with the use case @@ -203,35 +217,4 @@ public class BootstrapRunner { } } - // ========================================================================= - // AP-005 (M2 scope): No-Op port implementations - // (Real M3 adapters will be wired in AP-007) - // ========================================================================= - - /** - * No-Op implementation of {@link SourceDocumentCandidatesPort} for M2 scope. - *

- * M2 batch execution does not scan the source folder, so this returns an empty list. - * AP-007 will replace this with a real filesystem adapter. - */ - private static class NoOpSourceCandidatesPort implements SourceDocumentCandidatesPort { - @Override - public java.util.List loadCandidates() { - return java.util.List.of(); - } - } - - /** - * No-Op implementation of {@link PdfTextExtractionPort} for M2 scope. - *

- * M2 batch execution does not extract PDF content, so this port is never called. - * AP-007 will replace this with a real PDFBox adapter. - */ - private static class NoOpExtractionPort implements PdfTextExtractionPort { - @Override - public de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult extractTextAndPageCount( - de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate) { - throw new UnsupportedOperationException("M2 scope: No-Op port, should not be called"); - } - } } \ No newline at end of file diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java index a8d3afe..4c9f96a 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java @@ -203,6 +203,30 @@ class BootstrapRunnerTest { "Lock path passed to adapter must not be blank (default must be applied)"); } + /** + * M3 exit-code contract: a completed batch run returns exit code 0 even when individual + * documents ended with M3 content errors or technical document errors. + * Only hard infrastructure or startup failures must cause exit code 1. + */ + @Test + void run_returnsZeroWhenBatchRunCompletesWithDocumentLevelFailures() throws Exception { + ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true); + // BatchRunOutcome.SUCCESS is what the M3 use case returns when the run completed, + // regardless of how many individual documents had content or technical errors. + RunBatchProcessingUseCase useCaseWithDocumentFailures = (context) -> BatchRunOutcome.SUCCESS; + + BootstrapRunner runner = new BootstrapRunner( + () -> mockConfigPort, + lockFile -> new MockRunLockPort(), + StartConfigurationValidator::new, + (config, lock) -> useCaseWithDocumentFailures, + SchedulerBatchCommand::new + ); + + assertEquals(0, runner.run(), + "M3 contract: batch completion with document-level failures must return exit code 0"); + } + @Test void run_withDefaultConstructor_usesRealImplementations() { BootstrapRunner runner = new BootstrapRunner();