diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/RuntimeConfiguration.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/RuntimeConfiguration.java new file mode 100644 index 0000000..e6b907f --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/RuntimeConfiguration.java @@ -0,0 +1,20 @@ +package de.gecheckt.pdf.umbenenner.application.config; + +/** + * Minimal runtime configuration for the application layer. + *

+ * Contains only the application-level runtime parameters that are needed during + * batch document processing. Technical infrastructure configuration (paths, API keys, + * persistence parameters, etc.) is kept in the bootstrap layer. + *

+ * This intentionally small contract ensures the application layer depends only on + * the configuration values it actually uses, following hexagonal architecture principles. + */ +public record RuntimeConfiguration( + /** + * Maximum number of pages a document can have to be processed. + * Documents exceeding this limit are rejected during pre-checks. + */ + int maxPages +) +{ } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingService.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingService.java index dcb25bc..2395a4d 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingService.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingService.java @@ -1,6 +1,6 @@ package de.gecheckt.pdf.umbenenner.application.service; -import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason; @@ -48,23 +48,23 @@ public class DocumentProcessingService { * * @param candidate the document candidate to process * @param extractionResult the result from PDF extraction (from {@link de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort}) - * @param configuration the startup configuration (used for pre-check validation) + * @param runtimeConfig the runtime configuration (used for pre-check validation) * @return the complete processing outcome (implementing {@link DocumentProcessingOutcome}) * @throws NullPointerException if any parameter is null */ public static DocumentProcessingOutcome processDocument( SourceDocumentCandidate candidate, PdfExtractionResult extractionResult, - StartConfiguration configuration) { + RuntimeConfiguration runtimeConfig) { Objects.requireNonNull(candidate, "candidate must not be null"); Objects.requireNonNull(extractionResult, "extractionResult must not be null"); - Objects.requireNonNull(configuration, "configuration must not be null"); + Objects.requireNonNull(runtimeConfig, "runtimeConfig must not be null"); return switch (extractionResult) { case PdfExtractionSuccess success -> // Extraction succeeded: evaluate pre-checks - PreCheckEvaluator.evaluate(candidate, success, configuration); + PreCheckEvaluator.evaluate(candidate, success, runtimeConfig); case PdfExtractionContentError contentError -> // PDF content not extractable: classify as pre-check failed (deterministic content error) diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluator.java index cd2fa04..757ecd9 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluator.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluator.java @@ -1,6 +1,6 @@ package de.gecheckt.pdf.umbenenner.application.service; -import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed; @@ -44,18 +44,18 @@ public class PreCheckEvaluator { * * @param candidate the source document metadata * @param extraction the successfully extracted PDF content - * @param configuration the startup configuration (used for maxPages limit) + * @param runtimeConfig the runtime configuration (used for maxPages limit) * @return the pre-check outcome: passed or failed with reason (both implement {@link DocumentProcessingOutcome}) * @throws NullPointerException if any parameter is null */ public static DocumentProcessingOutcome evaluate( SourceDocumentCandidate candidate, PdfExtractionSuccess extraction, - StartConfiguration configuration) { + RuntimeConfiguration runtimeConfig) { Objects.requireNonNull(candidate, "candidate must not be null"); Objects.requireNonNull(extraction, "extraction must not be null"); - Objects.requireNonNull(configuration, "configuration must not be null"); + Objects.requireNonNull(runtimeConfig, "runtimeConfig must not be null"); // Pre-check 1: Verify document has usable text if (!hasUsableText(extraction.extractedText())) { @@ -66,7 +66,7 @@ public class PreCheckEvaluator { } // Pre-check 2: Verify document page count does not exceed configured limit - if (extraction.pageCount().exceedsLimit(configuration.maxPages())) { + if (extraction.pageCount().exceedsLimit(runtimeConfig.maxPages())) { return new PreCheckFailed( candidate, PreCheckFailureReason.PAGE_LIMIT_EXCEEDED diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java index 893e3dd..9d64450 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java @@ -1,6 +1,6 @@ package de.gecheckt.pdf.umbenenner.application.usecase; -import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort; @@ -77,7 +77,7 @@ import java.util.Objects; */ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCase { - private final StartConfiguration configuration; + private final RuntimeConfiguration runtimeConfiguration; private final RunLockPort runLockPort; private final SourceDocumentCandidatesPort sourceDocumentCandidatesPort; private final PdfTextExtractionPort pdfTextExtractionPort; @@ -86,13 +86,13 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa private final ProcessingLogger logger; /** - * Creates the batch use case with the already-loaded startup configuration and all - * required ports for the flow. + * Creates the batch use case with the runtime configuration and all required ports for the flow. *

- * The configuration is loaded and validated by Bootstrap before use case creation; - * the use case receives the result directly and does not re-read the properties file. + * The runtime configuration contains only the application-level settings needed + * during batch processing. Technical infrastructure configuration (paths, persistence, + * API details, etc.) is managed by Bootstrap. * - * @param configuration the validated startup configuration; must not be null + * @param runtimeConfiguration the runtime configuration containing application-level settings; must not be null * @param runLockPort for exclusive run locking; must not be null * @param sourceDocumentCandidatesPort for loading PDF candidates from the source folder; * must not be null @@ -106,14 +106,14 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa * @throws NullPointerException if any parameter is null */ public DefaultBatchRunProcessingUseCase( - StartConfiguration configuration, + RuntimeConfiguration runtimeConfiguration, RunLockPort runLockPort, SourceDocumentCandidatesPort sourceDocumentCandidatesPort, PdfTextExtractionPort pdfTextExtractionPort, FingerprintPort fingerprintPort, DocumentProcessingCoordinator documentProcessingCoordinator, ProcessingLogger logger) { - this.configuration = Objects.requireNonNull(configuration, "configuration must not be null"); + this.runtimeConfiguration = Objects.requireNonNull(runtimeConfiguration, "runtimeConfiguration must not be null"); this.runLockPort = Objects.requireNonNull(runLockPort, "runLockPort must not be null"); this.sourceDocumentCandidatesPort = Objects.requireNonNull( sourceDocumentCandidatesPort, "sourceDocumentCandidatesPort must not be null"); @@ -142,8 +142,6 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa return BatchRunOutcome.LOCK_UNAVAILABLE; } - logger.debug("Configuration in use: source={}, target={}", - configuration.sourceFolder(), configuration.targetFolder()); logger.info("Batch run started. RunId: {}, Start: {}", context.runId(), context.startInstant()); @@ -301,7 +299,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa logExtractionResult(candidate, extractionResult); DocumentProcessingOutcome outcome = - DocumentProcessingService.processDocument(candidate, extractionResult, configuration); + DocumentProcessingService.processDocument(candidate, extractionResult, runtimeConfiguration); logProcessingOutcome(candidate, outcome); diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingServiceTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingServiceTest.java index 52a4718..3ae55fb 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingServiceTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingServiceTest.java @@ -1,6 +1,6 @@ package de.gecheckt.pdf.umbenenner.application.service; -import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason; @@ -34,7 +34,7 @@ class DocumentProcessingServiceTest { Path tempDir; private SourceDocumentCandidate candidate; - private StartConfiguration configuration; + private RuntimeConfiguration runtimeConfig; @BeforeEach void setUp() throws Exception { @@ -44,30 +44,8 @@ class DocumentProcessingServiceTest { SourceDocumentLocator locator = new SourceDocumentLocator(pdfFile.toString()); candidate = new SourceDocumentCandidate("document.pdf", 2048L, locator); - // Create directories and files for configuration - Path sourceDir = Files.createDirectories(tempDir.resolve("source")); - Path targetDir = Files.createDirectories(tempDir.resolve("target")); - Path dbFile = tempDir.resolve("db.sqlite"); - Files.createFile(dbFile); - Path promptFile = tempDir.resolve("prompt.txt"); - Files.createFile(promptFile); - - configuration = new StartConfiguration( - sourceDir, - targetDir, - dbFile, - URI.create("http://localhost:8000"), - "gpt-4", - 30, - 3, - 10, - 5000, - promptFile, - tempDir.resolve("lock"), - tempDir.resolve("logs"), - "INFO", - "test-key" - ); + // Create runtime configuration with maxPages limit + runtimeConfig = new RuntimeConfiguration(10); } @Test @@ -77,7 +55,7 @@ class DocumentProcessingServiceTest { // Act DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument( - candidate, extraction, configuration); + candidate, extraction, runtimeConfig); // Assert: Should produce PreCheckPassed assertInstanceOf(PreCheckPassed.class, outcome); @@ -93,7 +71,7 @@ class DocumentProcessingServiceTest { // Act DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument( - candidate, extraction, configuration); + candidate, extraction, runtimeConfig); // Assert: Should produce PreCheckFailed with appropriate reason assertInstanceOf(PreCheckFailed.class, outcome); @@ -109,7 +87,7 @@ class DocumentProcessingServiceTest { // Act DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument( - candidate, extraction, configuration); + candidate, extraction, runtimeConfig); // Assert: Should produce PreCheckFailed with page limit reason assertInstanceOf(PreCheckFailed.class, outcome); @@ -125,7 +103,7 @@ class DocumentProcessingServiceTest { // Act DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument( - candidate, contentError, configuration); + candidate, contentError, runtimeConfig); // Assert: Should produce PreCheckFailed assertInstanceOf(PreCheckFailed.class, outcome); @@ -142,7 +120,7 @@ class DocumentProcessingServiceTest { // Act DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument( - candidate, technicalError, configuration); + candidate, technicalError, runtimeConfig); // Assert: Should produce TechnicalDocumentError assertInstanceOf(TechnicalDocumentError.class, outcome); @@ -159,7 +137,7 @@ class DocumentProcessingServiceTest { // Act DocumentProcessingOutcome outcome = DocumentProcessingService.processDocument( - candidate, technicalError, configuration); + candidate, technicalError, runtimeConfig); // Assert assertInstanceOf(TechnicalDocumentError.class, outcome); @@ -174,14 +152,14 @@ class DocumentProcessingServiceTest { // Act & Assert assertThrows(NullPointerException.class, - () -> DocumentProcessingService.processDocument(null, extraction, configuration)); + () -> DocumentProcessingService.processDocument(null, extraction, runtimeConfig)); } @Test void testProcessDocument_WithNullExtractionResult_ThrowsException() { // Act & Assert assertThrows(NullPointerException.class, - () -> DocumentProcessingService.processDocument(candidate, null, configuration)); + () -> DocumentProcessingService.processDocument(candidate, null, runtimeConfig)); } @Test diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluatorTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluatorTest.java index 8ba1820..aeaf86e 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluatorTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/PreCheckEvaluatorTest.java @@ -1,6 +1,6 @@ package de.gecheckt.pdf.umbenenner.application.service; -import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed; import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason; @@ -31,7 +31,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWhenDocumentHasUsableTextAndValidPageCount() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Some meaningful text", new PdfPageCount(5)); @@ -45,7 +45,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_failsWithNoUsableTextWhenExtractedTextIsEmpty() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("", new PdfPageCount(1)); @@ -58,7 +58,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_failsWithNoUsableTextWhenTextIsOnlyWhitespace() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess(" \n\t \r\n ", new PdfPageCount(1)); @@ -71,7 +71,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_failsWithNoUsableTextWhenTextContainsOnlySpecialCharacters() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("!@#$%^&*()_+-=[]{}|;:',.<>?/", new PdfPageCount(1)); @@ -84,7 +84,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWithTextContainingSingleLetter() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("a", new PdfPageCount(1)); @@ -95,7 +95,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWithTextContainingSingleDigit() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("5", new PdfPageCount(1)); @@ -106,7 +106,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWithTextMixedWithSpecialCharactersIfLettersOrDigitsPresent() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("!@#a$%^&*", new PdfPageCount(1)); @@ -117,7 +117,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWithWhitespaceAroundUsableText() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess(" meaningful text ", new PdfPageCount(1)); @@ -128,7 +128,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_failsWithPageLimitExceededWhenPageCountEqualsLimit() throws Exception { - StartConfiguration config = buildConfig(maxPages(5)); + RuntimeConfiguration config = buildConfig(maxPages(5)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(5)); @@ -139,7 +139,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_failsWithPageLimitExceededWhenPageCountExceedsLimit() throws Exception { - StartConfiguration config = buildConfig(maxPages(5)); + RuntimeConfiguration config = buildConfig(maxPages(5)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(6)); @@ -152,7 +152,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_failsWithPageLimitExceededEvenIfTextIsValid() throws Exception { - StartConfiguration config = buildConfig(maxPages(2)); + RuntimeConfiguration config = buildConfig(maxPages(2)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Excellent meaningful text with lots of content", new PdfPageCount(100)); @@ -167,7 +167,7 @@ class PreCheckEvaluatorTest { void evaluate_prefersPageLimitCheckOverTextCheck() throws Exception { // If both checks fail, page limit check should take precedence (not tested for priority, // but we verify that one failure is reported consistently) - StartConfiguration config = buildConfig(maxPages(1)); + RuntimeConfiguration config = buildConfig(maxPages(1)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("", new PdfPageCount(10)); @@ -181,7 +181,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_throwsNullPointerExceptionWhenCandidateIsNull() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Valid text", new PdfPageCount(1)); assertThrows(NullPointerException.class, () -> @@ -191,7 +191,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_throwsNullPointerExceptionWhenExtractionIsNull() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); assertThrows(NullPointerException.class, () -> @@ -211,7 +211,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWithUnicodeGermanUmlauts() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Äußerst äöüß Großes", new PdfPageCount(1)); @@ -222,7 +222,7 @@ class PreCheckEvaluatorTest { @Test void evaluate_passesWithOtherUnicodeCharacters() throws Exception { - StartConfiguration config = buildConfig(maxPages(10)); + RuntimeConfiguration config = buildConfig(maxPages(10)); SourceDocumentCandidate candidate = buildCandidate(); PdfExtractionSuccess extraction = new PdfExtractionSuccess("Αβγδ 中文 καλημέρα", new PdfPageCount(1)); @@ -235,30 +235,8 @@ class PreCheckEvaluatorTest { // Helpers // ========================================================================= - private StartConfiguration buildConfig(int maxPages) throws Exception { - Path sourceDir = Files.createDirectories(tempDir.resolve("source")); - Path targetDir = Files.createDirectories(tempDir.resolve("target")); - Path dbFile = tempDir.resolve("db.sqlite"); - Files.createFile(dbFile); - Path promptFile = tempDir.resolve("prompt.txt"); - Files.createFile(promptFile); - - return new StartConfiguration( - sourceDir, - targetDir, - dbFile, - URI.create("https://api.example.com"), - "gpt-4", - 30, - 3, - maxPages, - 50000, - promptFile, - tempDir.resolve("lock.lock"), - tempDir.resolve("logs"), - "INFO", - "test-key" - ); + private RuntimeConfiguration buildConfig(int maxPages) throws Exception { + return new RuntimeConfiguration(maxPages); } private int maxPages(int limit) { diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java index 405dc2c..092fc42 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java @@ -1,6 +1,6 @@ package de.gecheckt.pdf.umbenenner.application.usecase; -import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult; @@ -71,7 +71,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_successfullyAcquiresAndReleasesLock() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); DefaultBatchRunProcessingUseCase useCase = buildUseCase( config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(), @@ -88,7 +88,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_returnsLockUnavailableWhenLockCannotBeAcquired() throws Exception { CountingRunLockPort lockPort = new CountingRunLockPort(true); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); DefaultBatchRunProcessingUseCase useCase = buildUseCase( config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(), @@ -108,7 +108,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_doesNotReleaseLockWhenAcquireFails() throws Exception { CountingRunLockPort lockPort = new CountingRunLockPort(true); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); DefaultBatchRunProcessingUseCase useCase = buildUseCase( config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(), @@ -125,7 +125,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_releasesLockEvenOnUnexpectedError() throws Exception { ErrorAfterAcquireLockPort lockPort = new ErrorAfterAcquireLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); DefaultBatchRunProcessingUseCase useCase = buildUseCase( config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(), @@ -146,7 +146,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_withNoCandidates_returnsSuccess() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); DefaultBatchRunProcessingUseCase useCase = buildUseCase( config, lockPort, new EmptyCandidatesPort(), new NoOpExtractionPort(), @@ -161,7 +161,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_happyPath_candidatePassesPreChecks_persistenceInvoked() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("document.pdf"); PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice text", new PdfPageCount(1)); @@ -184,7 +184,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_noUsableText_candidateEndsControlled_batchContinues() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("image-only.pdf"); PdfExtractionSuccess emptySuccess = new PdfExtractionSuccess(" ", new PdfPageCount(1)); @@ -206,7 +206,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_pageLimitExceeded_candidateEndsControlled_batchContinues() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("big.pdf"); PdfExtractionSuccess manyPages = new PdfExtractionSuccess("Some text", new PdfPageCount(10)); @@ -228,7 +228,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_extractionContentError_candidateEndsControlled_batchContinues() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("encrypted.pdf"); PdfExtractionContentError contentError = new PdfExtractionContentError("PDF is encrypted"); @@ -250,7 +250,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_extractionTechnicalError_candidateEndsControlled_batchContinues() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("corrupt.pdf"); PdfExtractionTechnicalError technicalError = new PdfExtractionTechnicalError("I/O error reading file", null); @@ -272,7 +272,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_sourceAccessException_returnsFailure() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidatesPort failingPort = () -> { throw new SourceDocumentAccessException("Source folder not readable"); @@ -297,7 +297,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_fingerprintFailure_candidateNotHistorised_batchContinues() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("unreadable.pdf"); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); @@ -322,7 +322,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_fingerprintFailure_extractionNotCalled() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate candidate = makeCandidate("unreadable.pdf"); FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate)); @@ -350,7 +350,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_mixedBatch_allOutcomeTypes_batchOverallSucceeds() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); SourceDocumentCandidate goodCandidate = makeCandidate("good.pdf"); SourceDocumentCandidate noTextCandidate = makeCandidate("notext.pdf"); @@ -400,7 +400,7 @@ class BatchRunProcessingUseCaseTest { @Test void execute_multipleCandidates_allProcessed_batchSucceeds() throws Exception { MockRunLockPort lockPort = new MockRunLockPort(); - StartConfiguration config = buildConfig(tempDir); + RuntimeConfiguration config = buildConfig(tempDir); List candidates = List.of( makeCandidate("a.pdf"), @@ -429,41 +429,20 @@ class BatchRunProcessingUseCaseTest { // ------------------------------------------------------------------------- private static DefaultBatchRunProcessingUseCase buildUseCase( - StartConfiguration config, + RuntimeConfiguration runtimeConfig, RunLockPort lockPort, SourceDocumentCandidatesPort candidatesPort, PdfTextExtractionPort extractionPort, FingerprintPort fingerprintPort, DocumentProcessingCoordinator processor) { return new DefaultBatchRunProcessingUseCase( - config, lockPort, candidatesPort, extractionPort, fingerprintPort, processor, + runtimeConfig, lockPort, candidatesPort, extractionPort, fingerprintPort, processor, new NoOpProcessingLogger()); } - private static StartConfiguration buildConfig(Path tempDir) throws Exception { - Path sourceDir = Files.createDirectories(tempDir.resolve("source")); - Path targetDir = Files.createDirectories(tempDir.resolve("target")); - Path dbFile = tempDir.resolve("db.sqlite"); - if (!Files.exists(dbFile)) Files.createFile(dbFile); - Path promptFile = tempDir.resolve("prompt.txt"); - if (!Files.exists(promptFile)) Files.createFile(promptFile); - - return new StartConfiguration( - sourceDir, - targetDir, - dbFile, - URI.create("https://api.example.com"), - "gpt-4", - 30, - 3, // maxRetries - 3, // maxPages (low limit – useful for page-limit tests) - 50000, - promptFile, - tempDir.resolve("lock.lock"), - tempDir.resolve("logs"), - "INFO", - "test-key" - ); + private static RuntimeConfiguration buildConfig(Path tempDir) throws Exception { + // maxPages set to 3 – useful for page-limit tests + return new RuntimeConfiguration(3); } private static SourceDocumentCandidate makeCandidate(String filename) { 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 eaf04d5..57bc6d9 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 @@ -20,6 +20,7 @@ import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDocumentRecordReposit import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteProcessingAttemptRepositoryAdapter; import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteSchemaInitializationAdapter; import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteUnitOfWorkAdapter; +import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase; @@ -122,14 +123,14 @@ public class BootstrapRunner { /** * Functional interface for creating a BatchRunProcessingUseCase. *

- * Receives the already-loaded and validated {@link StartConfiguration} and run lock port. - * The factory is responsible for creating and wiring any additional outbound ports + * Receives the full startup configuration (for infrastructure adapter wiring) and run lock port. + * The factory extracts the runtime configuration and wires all outbound ports * required by the use case (e.g., source document port, PDF extraction port, * persistence and fingerprint ports). */ @FunctionalInterface public interface UseCaseFactory { - BatchRunProcessingUseCase create(StartConfiguration config, RunLockPort runLockPort); + BatchRunProcessingUseCase create(StartConfiguration startConfig, RunLockPort runLockPort); } /** @@ -164,8 +165,11 @@ public class BootstrapRunner { this.runLockPortFactory = FilesystemRunLockPortAdapter::new; this.validatorFactory = StartConfigurationValidator::new; this.schemaInitPortFactory = SqliteSchemaInitializationAdapter::new; - this.useCaseFactory = (config, lock) -> { - String jdbcUrl = buildJdbcUrl(config); + this.useCaseFactory = (startConfig, lock) -> { + // Extract runtime configuration from startup configuration + RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(startConfig.maxPages()); + + String jdbcUrl = buildJdbcUrl(startConfig); FingerprintPort fingerprintPort = new Sha256FingerprintAdapter(); DocumentRecordRepository documentRecordRepository = new SqliteDocumentRecordRepositoryAdapter(jdbcUrl); @@ -178,9 +182,9 @@ public class BootstrapRunner { new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository, unitOfWorkPort, coordinatorLogger); ProcessingLogger useCaseLogger = new Log4jProcessingLogger(DefaultBatchRunProcessingUseCase.class); return new DefaultBatchRunProcessingUseCase( - config, + runtimeConfig, lock, - new SourceDocumentCandidatesPortAdapter(config.sourceFolder()), + new SourceDocumentCandidatesPortAdapter(startConfig.sourceFolder()), new PdfTextExtractionPortAdapter(), fingerprintPort, documentProcessingCoordinator,