1
0

Nachbearbeitung: Logging aus der Application-Schicht entkoppelt

This commit is contained in:
2026-04-04 14:31:14 +02:00
parent deaa8c9fa3
commit 8e6d745e4b
9 changed files with 210 additions and 55 deletions

View File

@@ -35,6 +35,10 @@
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>

View File

@@ -19,12 +19,6 @@
<version>${project.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>

View File

@@ -0,0 +1,46 @@
package de.gecheckt.pdf.umbenenner.application.port.out;
/**
* Outbound port for processing-related logging operations.
* <p>
* 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.
* <p>
* 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);
}

View File

@@ -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);
}
}

View File

@@ -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 -> {

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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;
}

View File

@@ -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.
* <p>
* This adapter bridges the application layer to the concrete Log4j2 framework,
* allowing the application to remain decoupled from logging technology choices.
* <p>
* 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);
}
}
}