+ * The application delegates all logging to this port to remain decoupled from + * specific logging frameworks. Concrete implementations are provided by adapters. + */ +public interface ProcessingLogger { + + /** + * Logs an information-level message. + * + * @param message the message template (may contain {} placeholders) + * @param args optional message arguments for placeholder substitution + */ + void info(String message, Object... args); + + /** + * Logs a debug-level message. + * + * @param message the message template (may contain {} placeholders) + * @param args optional message arguments for placeholder substitution + */ + void debug(String message, Object... args); + + /** + * Logs a warning-level message. + * + * @param message the message template (may contain {} placeholders) + * @param args optional message arguments for placeholder substitution + */ + void warn(String message, Object... args); + + /** + * Logs an error-level message with optional arguments and exception. + *
+ * If the last argument is a Throwable, it is treated as the exception to log. + * Otherwise, all arguments are treated as message parameters. + * + * @param message the message template (may contain {} placeholders) + * @param args optional message arguments and/or exception + */ + void error(String message, Object... args); + +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java index fcde2b1..2dcced1 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinator.java @@ -12,6 +12,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters; import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; +import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; @@ -22,9 +23,6 @@ import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate; import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator; import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import java.time.Instant; import java.util.Objects; import java.util.function.Consumer; @@ -80,14 +78,13 @@ import java.util.function.Function; */ public class DocumentProcessingCoordinator { - private static final Logger LOG = LogManager.getLogger(DocumentProcessingCoordinator.class); - private final DocumentRecordRepository documentRecordRepository; private final ProcessingAttemptRepository processingAttemptRepository; private final UnitOfWorkPort unitOfWorkPort; + private final ProcessingLogger logger; /** - * Creates the document processor with the required persistence ports. + * Creates the document processor with the required persistence ports and logger. * * @param documentRecordRepository port for reading and writing the document master record; * must not be null @@ -95,18 +92,21 @@ public class DocumentProcessingCoordinator { * must not be null * @param unitOfWorkPort port for executing operations atomically; * must not be null + * @param logger for processing-related logging; must not be null * @throws NullPointerException if any parameter is null */ public DocumentProcessingCoordinator( DocumentRecordRepository documentRecordRepository, ProcessingAttemptRepository processingAttemptRepository, - UnitOfWorkPort unitOfWorkPort) { + UnitOfWorkPort unitOfWorkPort, + ProcessingLogger logger) { this.documentRecordRepository = Objects.requireNonNull(documentRecordRepository, "documentRecordRepository must not be null"); this.processingAttemptRepository = Objects.requireNonNull(processingAttemptRepository, "processingAttemptRepository must not be null"); this.unitOfWorkPort = Objects.requireNonNull(unitOfWorkPort, "unitOfWorkPort must not be null"); + this.logger = Objects.requireNonNull(logger, "logger must not be null"); } /** @@ -192,7 +192,7 @@ public class DocumentProcessingCoordinator { // Step 2: Handle persistence lookup failure – cannot safely proceed if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) { - LOG.error("Cannot process '{}': master record lookup failed: {}", + logger.error("Cannot process '{}': master record lookup failed: {}", candidate.uniqueIdentifier(), failure.errorMessage()); return; } @@ -201,7 +201,7 @@ public class DocumentProcessingCoordinator { switch (lookupResult) { case DocumentTerminalSuccess terminalSuccess -> { // Document already successfully processed → skip - LOG.info("Skipping '{}': already successfully processed (fingerprint: {}).", + logger.info("Skipping '{}': already successfully processed (fingerprint: {}).", candidate.uniqueIdentifier(), fingerprint.sha256Hex()); persistSkipAttempt( candidate, fingerprint, terminalSuccess.record(), @@ -211,7 +211,7 @@ public class DocumentProcessingCoordinator { case DocumentTerminalFinalFailure terminalFailure -> { // Document finally failed → skip - LOG.info("Skipping '{}': already finally failed (fingerprint: {}).", + logger.info("Skipping '{}': already finally failed (fingerprint: {}).", candidate.uniqueIdentifier(), fingerprint.sha256Hex()); persistSkipAttempt( candidate, fingerprint, terminalFailure.record(), @@ -235,7 +235,7 @@ public class DocumentProcessingCoordinator { default -> // Exhaustive sealed hierarchy; this branch is unreachable - LOG.error("Unexpected lookup result type for '{}': {}", + logger.error("Unexpected lookup result type for '{}': {}", candidate.uniqueIdentifier(), lookupResult.getClass().getSimpleName()); } } @@ -291,11 +291,11 @@ public class DocumentProcessingCoordinator { txOps.updateDocumentRecord(skipRecord); }); - LOG.debug("Skip attempt #{} persisted for '{}' with status {}.", + logger.debug("Skip attempt #{} persisted for '{}' with status {}.", attemptNumber, candidate.uniqueIdentifier(), skipStatus); } catch (DocumentPersistenceException e) { - LOG.error("Failed to persist skip attempt for '{}': {}", + logger.error("Failed to persist skip attempt for '{}': {}", candidate.uniqueIdentifier(), e.getMessage(), e); } } @@ -514,14 +514,14 @@ public class DocumentProcessingCoordinator { recordWriter.accept(txOps); }); - LOG.info("Document '{}' processed: status={}, contentErrors={}, transientErrors={}.", + logger.info("Document '{}' processed: status={}, contentErrors={}, transientErrors={}.", candidate.uniqueIdentifier(), outcome.overallStatus(), outcome.counters().contentErrorCount(), outcome.counters().transientErrorCount()); } catch (DocumentPersistenceException e) { - LOG.error("Failed to persist processing result for '{}': {}", + logger.error("Failed to persist processing result for '{}': {}", candidate.uniqueIdentifier(), e.getMessage(), e); } } 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 95b275e..893e3dd 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 @@ -8,6 +8,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintResult; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError; import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; +import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException; import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException; @@ -20,9 +21,6 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome; import de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult; import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import java.time.Instant; import java.util.List; import java.util.Objects; @@ -79,14 +77,13 @@ import java.util.Objects; */ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCase { - private static final Logger LOG = LogManager.getLogger(DefaultBatchRunProcessingUseCase.class); - private final StartConfiguration configuration; private final RunLockPort runLockPort; private final SourceDocumentCandidatesPort sourceDocumentCandidatesPort; private final PdfTextExtractionPort pdfTextExtractionPort; private final FingerprintPort fingerprintPort; private final DocumentProcessingCoordinator documentProcessingCoordinator; + private final ProcessingLogger logger; /** * Creates the batch use case with the already-loaded startup configuration and all @@ -105,6 +102,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa * must not be null * @param documentProcessingCoordinator for applying decision logic and persisting results; * must not be null + * @param logger for processing-related logging; must not be null * @throws NullPointerException if any parameter is null */ public DefaultBatchRunProcessingUseCase( @@ -113,7 +111,8 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa SourceDocumentCandidatesPort sourceDocumentCandidatesPort, PdfTextExtractionPort pdfTextExtractionPort, FingerprintPort fingerprintPort, - DocumentProcessingCoordinator documentProcessingCoordinator) { + DocumentProcessingCoordinator documentProcessingCoordinator, + ProcessingLogger logger) { this.configuration = Objects.requireNonNull(configuration, "configuration must not be null"); this.runLockPort = Objects.requireNonNull(runLockPort, "runLockPort must not be null"); this.sourceDocumentCandidatesPort = Objects.requireNonNull( @@ -123,11 +122,12 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa this.fingerprintPort = Objects.requireNonNull(fingerprintPort, "fingerprintPort must not be null"); this.documentProcessingCoordinator = Objects.requireNonNull( documentProcessingCoordinator, "documentProcessingCoordinator must not be null"); + this.logger = Objects.requireNonNull(logger, "logger must not be null"); } @Override public BatchRunOutcome execute(BatchRunContext context) { - LOG.info("Batch processing initiated. RunId: {}", context.runId()); + logger.info("Batch processing initiated. RunId: {}", context.runId()); boolean lockAcquired = false; try { @@ -135,23 +135,23 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa try { runLockPort.acquire(); lockAcquired = true; - LOG.debug("Run lock acquired successfully."); + logger.debug("Run lock acquired successfully."); } catch (RunLockUnavailableException e) { - LOG.warn("Run lock not available – another instance is already running. " + logger.warn("Run lock not available – another instance is already running. " + "This instance terminates immediately."); return BatchRunOutcome.LOCK_UNAVAILABLE; } - LOG.debug("Configuration in use: source={}, target={}", + logger.debug("Configuration in use: source={}, target={}", configuration.sourceFolder(), configuration.targetFolder()); - LOG.info("Batch run started. RunId: {}, Start: {}", + logger.info("Batch run started. RunId: {}, Start: {}", context.runId(), context.startInstant()); // Load and process all candidates return processCandidates(context); } catch (Exception e) { - LOG.error("Unexpected error during batch processing", e); + logger.error("Unexpected error during batch processing", e); return BatchRunOutcome.FAILURE; } finally { releaseLockIfAcquired(lockAcquired); @@ -169,17 +169,17 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa try { candidates = sourceDocumentCandidatesPort.loadCandidates(); } catch (SourceDocumentAccessException e) { - LOG.error("Cannot access source folder: {}", e.getMessage(), e); + logger.error("Cannot access source folder: {}", e.getMessage(), e); return BatchRunOutcome.FAILURE; } - LOG.info("Found {} PDF candidate(s) in source folder.", candidates.size()); + logger.info("Found {} PDF candidate(s) in source folder.", candidates.size()); // Process each candidate for (SourceDocumentCandidate candidate : candidates) { processCandidate(candidate, context); } - LOG.info("Batch run completed. Processed {} candidate(s). RunId: {}", + logger.info("Batch run completed. Processed {} candidate(s). RunId: {}", candidates.size(), context.runId()); return BatchRunOutcome.SUCCESS; } @@ -196,9 +196,9 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa if (lockAcquired) { try { runLockPort.release(); - LOG.debug("Run lock released."); + logger.debug("Run lock released."); } catch (Exception e) { - LOG.warn("Warning: Failed to release run lock.", e); + logger.warn("Warning: Failed to release run lock.", e); } } } @@ -230,7 +230,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa * @param context the current batch run context */ private void processCandidate(SourceDocumentCandidate candidate, BatchRunContext context) { - LOG.debug("Processing candidate: {}", candidate.uniqueIdentifier()); + logger.debug("Processing candidate: {}", candidate.uniqueIdentifier()); Instant attemptStart = Instant.now(); FingerprintResult fingerprintResult = fingerprintPort.computeFingerprint(candidate); @@ -253,7 +253,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa * @param error the fingerprint error */ private void handleFingerprintError(SourceDocumentCandidate candidate, FingerprintTechnicalError error) { - LOG.warn("Fingerprint computation failed for '{}': {} — candidate skipped (not historised).", + logger.warn("Fingerprint computation failed for '{}': {} — candidate skipped (not historised).", candidate.uniqueIdentifier(), error.errorMessage()); } @@ -273,7 +273,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa BatchRunContext context, Instant attemptStart) { DocumentFingerprint fingerprint = fingerprintSuccess.fingerprint(); - LOG.debug("Fingerprint computed for '{}': {}", + logger.debug("Fingerprint computed for '{}': {}", candidate.uniqueIdentifier(), fingerprint.sha256Hex()); documentProcessingCoordinator.processDeferredOutcome( @@ -317,17 +317,17 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa private void logExtractionResult(SourceDocumentCandidate candidate, PdfExtractionResult result) { switch (result) { case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionSuccess success -> { - LOG.debug("PDF extraction successful for '{}'. Pages: {}, Text length: {} chars.", + logger.debug("PDF extraction successful for '{}'. Pages: {}, Text length: {} chars.", candidate.uniqueIdentifier(), success.pageCount().value(), success.extractedText().length()); } case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionContentError contentError -> { - LOG.debug("PDF content extraction failed for '{}' (content problem): {}", + logger.debug("PDF content extraction failed for '{}' (content problem): {}", candidate.uniqueIdentifier(), contentError.reason()); } case de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionTechnicalError technicalError -> { - LOG.debug("PDF extraction technical error for '{}': {}", + logger.debug("PDF extraction technical error for '{}': {}", candidate.uniqueIdentifier(), technicalError.errorMessage()); } default -> { @@ -345,15 +345,15 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa private void logProcessingOutcome(SourceDocumentCandidate candidate, DocumentProcessingOutcome outcome) { switch (outcome) { case de.gecheckt.pdf.umbenenner.domain.model.PreCheckPassed passed -> { - LOG.info("Pre-checks PASSED for '{}'. Candidate ready for persistence.", + logger.info("Pre-checks PASSED for '{}'. Candidate ready for persistence.", candidate.uniqueIdentifier()); } case de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed failed -> { - LOG.info("Pre-checks FAILED for '{}': {} (Deterministic content error).", + logger.info("Pre-checks FAILED for '{}': {} (Deterministic content error).", candidate.uniqueIdentifier(), failed.failureReasonDescription()); } case de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError technicalError -> { - LOG.warn("Processing FAILED for '{}': {} (Technical error – retryable).", + logger.warn("Processing FAILED for '{}': {} (Technical error – retryable).", candidate.uniqueIdentifier(), technicalError.errorMessage()); } default -> { diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java index d903039..4c98ab2 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/service/DocumentProcessingCoordinatorTest.java @@ -12,6 +12,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters; import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; +import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint; @@ -71,7 +72,7 @@ class DocumentProcessingCoordinatorTest { recordRepo = new CapturingDocumentRecordRepository(); attemptRepo = new CapturingProcessingAttemptRepository(); unitOfWorkPort = new CapturingUnitOfWorkPort(recordRepo, attemptRepo); - processor = new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort); + processor = new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort, new NoOpProcessingLogger()); candidate = new SourceDocumentCandidate( "test.pdf", 1024L, new SourceDocumentLocator("/tmp/test.pdf")); @@ -467,4 +468,26 @@ class DocumentProcessingCoordinatorTest { operations.accept(mockOps); } } + + private static class NoOpProcessingLogger implements ProcessingLogger { + @Override + public void info(String message, Object... args) { + // No-op + } + + @Override + public void debug(String message, Object... args) { + // No-op + } + + @Override + public void warn(String message, Object... args) { + // No-op + } + + @Override + public void error(String message, Object... args) { + // No-op + } + } } \ No newline at end of file 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 df83bec..405dc2c 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 @@ -13,6 +13,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; +import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException; import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentAccessException; @@ -435,7 +436,8 @@ class BatchRunProcessingUseCaseTest { FingerprintPort fingerprintPort, DocumentProcessingCoordinator processor) { return new DefaultBatchRunProcessingUseCase( - config, lockPort, candidatesPort, extractionPort, fingerprintPort, processor); + config, lockPort, candidatesPort, extractionPort, fingerprintPort, processor, + new NoOpProcessingLogger()); } private static StartConfiguration buildConfig(Path tempDir) throws Exception { @@ -617,7 +619,8 @@ class BatchRunProcessingUseCaseTest { */ private static class NoOpDocumentProcessingCoordinator extends DocumentProcessingCoordinator { NoOpDocumentProcessingCoordinator() { - super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort()); + super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort(), + new NoOpProcessingLogger()); } } @@ -628,7 +631,8 @@ class BatchRunProcessingUseCaseTest { private int processCallCount = 0; TrackingDocumentProcessingCoordinator() { - super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort()); + super(new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(), new NoOpUnitOfWorkPort(), + new NoOpProcessingLogger()); } @Override @@ -705,12 +709,12 @@ class BatchRunProcessingUseCaseTest { public void saveProcessingAttempt(ProcessingAttempt attempt) { // No-op } - + @Override public void createDocumentRecord(DocumentRecord record) { // No-op } - + @Override public void updateDocumentRecord(DocumentRecord record) { // No-op @@ -718,4 +722,27 @@ class BatchRunProcessingUseCaseTest { }); } } + + /** No-op ProcessingLogger for use in test instances. */ + private static class NoOpProcessingLogger implements ProcessingLogger { + @Override + public void info(String message, Object... args) { + // No-op + } + + @Override + public void debug(String message, Object... args) { + // No-op + } + + @Override + public void warn(String message, Object... args) { + // No-op + } + + @Override + public void error(String message, Object... args) { + // No-op + } + } } \ No newline at end of file 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 ebf1101..329bdc9 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 @@ -29,10 +29,12 @@ import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort; import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttemptRepository; +import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator; import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase; +import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.RunId; @@ -171,15 +173,17 @@ public class BootstrapRunner { new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl); UnitOfWorkPort unitOfWorkPort = new SqliteUnitOfWorkAdapter(jdbcUrl); + ProcessingLogger logger = new Log4jProcessingLogger(DefaultBatchRunProcessingUseCase.class); DocumentProcessingCoordinator documentProcessingCoordinator = - new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository, unitOfWorkPort); + new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository, unitOfWorkPort, logger); return new DefaultBatchRunProcessingUseCase( config, lock, new SourceDocumentCandidatesPortAdapter(config.sourceFolder()), new PdfTextExtractionPortAdapter(), fingerprintPort, - documentProcessingCoordinator); + documentProcessingCoordinator, + logger); }; this.commandFactory = SchedulerBatchCommand::new; } diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java new file mode 100644 index 0000000..9d103c5 --- /dev/null +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jProcessingLogger.java @@ -0,0 +1,57 @@ +package de.gecheckt.pdf.umbenenner.bootstrap.adapter; + +import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Log4j-based adapter implementing the {@link ProcessingLogger} port. + *
+ * This adapter bridges the application layer to the concrete Log4j2 framework, + * allowing the application to remain decoupled from logging technology choices. + *
+ * The error method intelligently detects if the last argument is a Throwable + * and logs accordingly. + */ +public class Log4jProcessingLogger implements ProcessingLogger { + + private final Logger log4jLogger; + + /** + * Creates a logger instance for the given class. + * + * @param clazz the class to derive the logger name from; must not be null + */ + public Log4jProcessingLogger(Class> clazz) { + this.log4jLogger = LogManager.getLogger(clazz); + } + + @Override + public void info(String message, Object... args) { + log4jLogger.info(message, args); + } + + @Override + public void debug(String message, Object... args) { + log4jLogger.debug(message, args); + } + + @Override + public void warn(String message, Object... args) { + log4jLogger.warn(message, args); + } + + @Override + public void error(String message, Object... args) { + // If the last argument is a Throwable, extract it and pass separately + if (args.length > 0 && args[args.length - 1] instanceof Throwable throwable) { + Object[] messageArgs = new Object[args.length - 1]; + System.arraycopy(args, 0, messageArgs, 0, args.length - 1); + log4jLogger.error(message, throwable, messageArgs); + } else { + log4jLogger.error(message, args); + } + } + +}