1
0

M4 AP-007 Scope bereinigen und Startfehler-Test ergänzen

This commit is contained in:
2026-04-03 13:33:01 +02:00
parent 049aa361db
commit a35ac5c8f1
3 changed files with 116 additions and 23 deletions

View File

@@ -16,6 +16,7 @@ import de.gecheckt.pdf.umbenenner.adapter.out.pdfextraction.PdfTextExtractionPor
import de.gecheckt.pdf.umbenenner.adapter.out.sourcedocument.SourceDocumentCandidatesPortAdapter; import de.gecheckt.pdf.umbenenner.adapter.out.sourcedocument.SourceDocumentCandidatesPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDocumentRecordRepositoryAdapter; import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteDocumentRecordRepositoryAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteProcessingAttemptRepositoryAdapter; 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.adapter.out.sqlite.SqliteUnitOfWorkAdapter;
import de.gecheckt.pdf.umbenenner.application.config.InvalidStartConfigurationException; import de.gecheckt.pdf.umbenenner.application.config.InvalidStartConfigurationException;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
@@ -26,6 +27,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository; 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.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.ProcessingAttemptRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort; import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
@@ -40,6 +42,7 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
* Responsibilities: * Responsibilities:
* <ol> * <ol>
* <li>Load and validate the startup configuration.</li> * <li>Load and validate the startup configuration.</li>
* <li>Initialise the SQLite persistence schema (M4-AP-007).</li>
* <li>Resolve the run-lock file path (with default fallback).</li> * <li>Resolve the run-lock file path (with default fallback).</li>
* <li>Create and wire all ports and adapters via configured factories.</li> * <li>Create and wire all ports and adapters via configured factories.</li>
* <li>Start the CLI adapter and execute the batch use case.</li> * <li>Start the CLI adapter and execute the batch use case.</li>
@@ -56,19 +59,22 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
* during the run.</li> * during the run.</li>
* </ul> * </ul>
* *
* <h2>M4 wiring (AP-006)</h2> * <h2>M4 wiring (AP-006 / AP-007)</h2>
* <p> * <p>
* The production constructor wires the following M4 adapters via the UseCaseFactory: * The production constructor wires the following M4 adapters:
* <ul> * <ul>
* <li>{@link SqliteSchemaInitializationAdapter} — SQLite schema DDL at startup (AP-007).</li>
* <li>{@link Sha256FingerprintAdapter} — SHA-256 content fingerprinting.</li> * <li>{@link Sha256FingerprintAdapter} — SHA-256 content fingerprinting.</li>
* <li>{@link SqliteDocumentRecordRepositoryAdapter} — document master record CRUD.</li> * <li>{@link SqliteDocumentRecordRepositoryAdapter} — document master record CRUD.</li>
* <li>{@link SqliteProcessingAttemptRepositoryAdapter} — attempt history CRUD.</li> * <li>{@link SqliteProcessingAttemptRepositoryAdapter} — attempt history CRUD.</li>
* <li>{@link SqliteUnitOfWorkAdapter} — atomic persistence operations.</li> * <li>{@link SqliteUnitOfWorkAdapter} — atomic persistence operations.</li>
* </ul> * </ul>
* <p> * <p>
* Schema initialisation is AP-007 responsibility, not performed in AP-006. * Schema initialisation is performed once in {@link #run()} before the batch loop starts
* (AP-007). A {@link DocumentPersistenceException} during schema initialisation is treated
* as a hard startup failure and results in exit code 1.
* *
* @since M2 (extended in M4-AP-006) * @since M2 (extended in M4-AP-006, M4-AP-007)
*/ */
public class BootstrapRunner { public class BootstrapRunner {
@@ -77,6 +83,7 @@ public class BootstrapRunner {
private final ConfigurationPortFactory configPortFactory; private final ConfigurationPortFactory configPortFactory;
private final RunLockPortFactory runLockPortFactory; private final RunLockPortFactory runLockPortFactory;
private final ValidatorFactory validatorFactory; private final ValidatorFactory validatorFactory;
private final SchemaInitializationPortFactory schemaInitPortFactory;
private final UseCaseFactory useCaseFactory; private final UseCaseFactory useCaseFactory;
private final CommandFactory commandFactory; private final CommandFactory commandFactory;
@@ -104,6 +111,14 @@ public class BootstrapRunner {
StartConfigurationValidator create(); StartConfigurationValidator create();
} }
/**
* Functional interface for creating a PersistenceSchemaInitializationPort from the JDBC URL.
*/
@FunctionalInterface
public interface SchemaInitializationPortFactory {
PersistenceSchemaInitializationPort create(String jdbcUrl);
}
/** /**
* Functional interface for creating a BatchRunProcessingUseCase. * Functional interface for creating a BatchRunProcessingUseCase.
* <p> * <p>
@@ -134,17 +149,20 @@ public class BootstrapRunner {
* <li>{@link SourceDocumentCandidatesPortAdapter} for PDF candidate discovery.</li> * <li>{@link SourceDocumentCandidatesPortAdapter} for PDF candidate discovery.</li>
* <li>{@link PdfTextExtractionPortAdapter} for PDFBox-based text and page count extraction.</li> * <li>{@link PdfTextExtractionPortAdapter} for PDFBox-based text and page count extraction.</li>
* <li>{@link Sha256FingerprintAdapter} for SHA-256 content fingerprinting.</li> * <li>{@link Sha256FingerprintAdapter} for SHA-256 content fingerprinting.</li>
* <li>{@link SqliteSchemaInitializationAdapter} for SQLite schema DDL at startup (AP-007).</li>
* <li>{@link SqliteDocumentRecordRepositoryAdapter} for document master record CRUD.</li> * <li>{@link SqliteDocumentRecordRepositoryAdapter} for document master record CRUD.</li>
* <li>{@link SqliteProcessingAttemptRepositoryAdapter} for attempt history CRUD.</li> * <li>{@link SqliteProcessingAttemptRepositoryAdapter} for attempt history CRUD.</li>
* <li>{@link SqliteUnitOfWorkAdapter} for atomic persistence operations.</li> * <li>{@link SqliteUnitOfWorkAdapter} for atomic persistence operations.</li>
* </ul> * </ul>
* <p> * <p>
* Schema initialisation is AP-007 responsibility and is NOT performed here. * Schema initialisation is performed explicitly in {@link #run()} before the batch loop
* begins (AP-007). Failure during initialisation aborts the run with exit code 1.
*/ */
public BootstrapRunner() { public BootstrapRunner() {
this.configPortFactory = PropertiesConfigurationPortAdapter::new; this.configPortFactory = PropertiesConfigurationPortAdapter::new;
this.runLockPortFactory = FilesystemRunLockPortAdapter::new; this.runLockPortFactory = FilesystemRunLockPortAdapter::new;
this.validatorFactory = StartConfigurationValidator::new; this.validatorFactory = StartConfigurationValidator::new;
this.schemaInitPortFactory = SqliteSchemaInitializationAdapter::new;
this.useCaseFactory = (config, lock) -> { this.useCaseFactory = (config, lock) -> {
String jdbcUrl = buildJdbcUrl(config); String jdbcUrl = buildJdbcUrl(config);
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter(); FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
@@ -173,17 +191,20 @@ public class BootstrapRunner {
* @param configPortFactory factory for creating ConfigurationPort instances * @param configPortFactory factory for creating ConfigurationPort instances
* @param runLockPortFactory factory for creating RunLockPort instances * @param runLockPortFactory factory for creating RunLockPort instances
* @param validatorFactory factory for creating StartConfigurationValidator instances * @param validatorFactory factory for creating StartConfigurationValidator instances
* @param schemaInitPortFactory factory for creating PersistenceSchemaInitializationPort instances (AP-007)
* @param useCaseFactory factory for creating BatchRunProcessingUseCase instances * @param useCaseFactory factory for creating BatchRunProcessingUseCase instances
* @param commandFactory factory for creating SchedulerBatchCommand instances * @param commandFactory factory for creating SchedulerBatchCommand instances
*/ */
public BootstrapRunner(ConfigurationPortFactory configPortFactory, public BootstrapRunner(ConfigurationPortFactory configPortFactory,
RunLockPortFactory runLockPortFactory, RunLockPortFactory runLockPortFactory,
ValidatorFactory validatorFactory, ValidatorFactory validatorFactory,
SchemaInitializationPortFactory schemaInitPortFactory,
UseCaseFactory useCaseFactory, UseCaseFactory useCaseFactory,
CommandFactory commandFactory) { CommandFactory commandFactory) {
this.configPortFactory = configPortFactory; this.configPortFactory = configPortFactory;
this.runLockPortFactory = runLockPortFactory; this.runLockPortFactory = runLockPortFactory;
this.validatorFactory = validatorFactory; this.validatorFactory = validatorFactory;
this.schemaInitPortFactory = schemaInitPortFactory;
this.useCaseFactory = useCaseFactory; this.useCaseFactory = useCaseFactory;
this.commandFactory = commandFactory; this.commandFactory = commandFactory;
} }
@@ -191,13 +212,27 @@ public class BootstrapRunner {
/** /**
* Runs the application startup sequence. * Runs the application startup sequence.
* <p> * <p>
* M4 additions: * M4 startup flow (AP-007):
* <ul> * <ol>
* <li>Derives the SQLite JDBC URL from the configured {@code sqlite.file} path.</li> * <li>Load configuration via {@link ConfigurationPort}.</li>
* <li>Creates the M4-aware use case via the {@link UseCaseFactory}, which wires persistence ports.</li> * <li>Validate the configuration via {@link StartConfigurationValidator}; validation
* </ul> * includes checking that the {@code sqlite.file} parent directory exists or is
* technically creatable.</li>
* <li>Initialise the SQLite persistence schema explicitly via
* {@link PersistenceSchemaInitializationPort}. This step happens once, before the
* batch document loop begins. A {@link DocumentPersistenceException} here is a hard
* startup failure and causes exit code 1.</li>
* <li>Resolve the run-lock file path, apply default if not configured.</li>
* <li>Create the batch use case with all M4 adapters wired.</li>
* <li>Execute the CLI command and map the outcome to an exit code.</li>
* </ol>
* <p>
* Document-level failures during the batch loop (step 6) are not startup failures and
* do not change the exit code as long as the run itself completes without a hard
* infrastructure error.
* *
* @return exit code: 0 for success, 1 for invalid configuration or unexpected bootstrap failure * @return exit code: 0 for a technically completed run, 1 for any hard startup or
* bootstrap failure (configuration invalid, schema init failed, etc.)
*/ */
public int run() { public int run() {
LOG.info("Bootstrap flow started."); LOG.info("Bootstrap flow started.");
@@ -208,11 +243,18 @@ public class BootstrapRunner {
// Step 2: Load configuration // Step 2: Load configuration
var config = configPort.loadConfiguration(); var config = configPort.loadConfiguration();
// Step 3: Validate configuration // Step 3: Validate configuration.
// Includes checking that sqlite.file parent directory exists or is creatable.
StartConfigurationValidator validator = validatorFactory.create(); StartConfigurationValidator validator = validatorFactory.create();
validator.validate(config); validator.validate(config);
// Step 4: Resolve lock file path apply default if not configured // Step 4 (M4-AP-007): Initialise SQLite persistence schema before the batch loop.
// Must happen once at startup; failure here is a hard bootstrap error → exit code 1.
String jdbcUrl = buildJdbcUrl(config);
PersistenceSchemaInitializationPort schemaInitPort = schemaInitPortFactory.create(jdbcUrl);
schemaInitPort.initializeSchema();
// Step 5: Resolve lock file path apply default if not configured
Path lockFilePath = config.runtimeLockFile(); Path lockFilePath = config.runtimeLockFile();
if (lockFilePath == null || lockFilePath.toString().isBlank()) { if (lockFilePath == null || lockFilePath.toString().isBlank()) {
lockFilePath = Paths.get("pdf-umbenenner.lock"); lockFilePath = Paths.get("pdf-umbenenner.lock");
@@ -221,21 +263,20 @@ public class BootstrapRunner {
} }
RunLockPort runLockPort = runLockPortFactory.create(lockFilePath); RunLockPort runLockPort = runLockPortFactory.create(lockFilePath);
// Step 5: Create the batch run context // Step 6: Create the batch run context
RunId runId = new RunId(UUID.randomUUID().toString()); RunId runId = new RunId(UUID.randomUUID().toString());
BatchRunContext runContext = new BatchRunContext(runId, Instant.now()); BatchRunContext runContext = new BatchRunContext(runId, Instant.now());
LOG.info("Batch run started. RunId: {}", runId); LOG.info("Batch run started. RunId: {}", runId);
// Step 6: Create the use case with the validated config and run lock. // Step 7: Create the use case with the validated config and run lock.
// Config is passed directly; the use case does not re-read the properties file. // Config is passed directly; the use case does not re-read the properties file.
// Adapters (source document port, PDF extraction port, M4 ports) are wired by the factory. // Adapters (source document port, PDF extraction port, M4 ports) are wired by the factory.
// Schema initialization is AP-007 responsibility, not performed in AP-006.
BatchRunProcessingUseCase useCase = useCaseFactory.create(config, runLockPort); BatchRunProcessingUseCase useCase = useCaseFactory.create(config, runLockPort);
// Step 7: Create the CLI command adapter with the use case // Step 8: Create the CLI command adapter with the use case
SchedulerBatchCommand command = commandFactory.create(useCase); SchedulerBatchCommand command = commandFactory.create(useCase);
// Step 8: Execute the command with the run context and handle the outcome // Step 9: Execute the command with the run context and handle the outcome
BatchRunOutcome outcome = command.run(runContext); BatchRunOutcome outcome = command.run(runContext);
// Mark run as completed // Mark run as completed

View File

@@ -25,5 +25,10 @@
* AP-005: CLI adapter and complete M2 object graph wiring. * AP-005: CLI adapter and complete M2 object graph wiring.
* <p> * <p>
* AP-006: Wires FilesystemRunLockPortAdapter (adapter-out) from validated config; retired temporary no-op lock. * AP-006: Wires FilesystemRunLockPortAdapter (adapter-out) from validated config; retired temporary no-op lock.
* <p>
* AP-007: Adds SQLite persistence schema initialization at startup via
* {@link de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort},
* ensuring the database is ready before document processing begins. Schema initialization failure
* is treated as a hard bootstrap error and causes exit code 1.
*/ */
package de.gecheckt.pdf.umbenenner.bootstrap; package de.gecheckt.pdf.umbenenner.bootstrap;

View File

@@ -7,6 +7,8 @@ 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.BatchRunOutcome;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort; import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
@@ -41,6 +43,7 @@ class BootstrapRunnerTest {
() -> mockConfigPort, () -> mockConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> new MockRunBatchProcessingUseCase(true), (config, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -65,6 +68,7 @@ class BootstrapRunnerTest {
() -> mockConfigPort, () -> mockConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
() -> failingValidator, () -> failingValidator,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> new MockRunBatchProcessingUseCase(true), (config, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -84,6 +88,7 @@ class BootstrapRunnerTest {
() -> failingConfigPort, () -> failingConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> new MockRunBatchProcessingUseCase(true), (config, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -103,6 +108,7 @@ class BootstrapRunnerTest {
() -> throwingConfigPort, () -> throwingConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> new MockRunBatchProcessingUseCase(true), (config, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -121,6 +127,7 @@ class BootstrapRunnerTest {
() -> mockConfigPort, () -> mockConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> failingUseCase, (config, lock) -> failingUseCase,
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -140,6 +147,7 @@ class BootstrapRunnerTest {
() -> mockConfigPort, () -> mockConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> lockUnavailableUseCase, (config, lock) -> lockUnavailableUseCase,
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -191,6 +199,7 @@ class BootstrapRunnerTest {
return new MockRunLockPort(); return new MockRunLockPort();
}, },
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> new MockRunBatchProcessingUseCase(true), (config, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase) useCase -> new SchedulerBatchCommand(useCase)
); );
@@ -219,6 +228,7 @@ class BootstrapRunnerTest {
() -> mockConfigPort, () -> mockConfigPort,
lockFile -> new MockRunLockPort(), lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new, StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> useCaseWithDocumentFailures, (config, lock) -> useCaseWithDocumentFailures,
SchedulerBatchCommand::new SchedulerBatchCommand::new
); );
@@ -233,6 +243,36 @@ class BootstrapRunnerTest {
assertNotNull(runner, "Default constructor should create a valid BootstrapRunner"); assertNotNull(runner, "Default constructor should create a valid BootstrapRunner");
} }
/**
* AP-007: Hard startup failure test — schema initialization failure must be treated as
* a startup error and result in exit code 1. This verifies that
* {@link DocumentPersistenceException} thrown from
* {@link PersistenceSchemaInitializationPort#initializeSchema()} is correctly caught
* and handled as a bootstrap failure.
*/
@Test
void run_returnsOneWhenSchemaInitializationFails() throws Exception {
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
BootstrapRunner runner = new BootstrapRunner(
() -> mockConfigPort,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
jdbcUrl -> new PersistenceSchemaInitializationPort() {
@Override
public void initializeSchema() {
throw new DocumentPersistenceException("Simulated schema initialization failure");
}
},
(config, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase)
);
int exitCode = runner.run();
assertEquals(1, exitCode, "Schema initialization failure should return exit code 1");
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Mocks // Mocks
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -306,4 +346,11 @@ class BootstrapRunnerTest {
@Override @Override
public void release() { } public void release() { }
} }
private static class MockSchemaInitializationPort implements PersistenceSchemaInitializationPort {
@Override
public void initializeSchema() {
// Success by default; can be subclassed to throw exceptions for testing
}
}
} }