@@ -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.sqlite.SqliteDocumentRecordRepositoryAdapter ;
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.InvalidStartConfigurationException ;
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.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.RunLockPort ;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort ;
@@ -40,6 +42,7 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
* Responsibilities:
* <ol>
* <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>Create and wire all ports and adapters via configured factories.</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>
* </ul>
*
* <h2>M4 wiring (AP-006)</h2>
* <h2>M4 wiring (AP-006 / AP-007 )</h2>
* <p>
* The production constructor wires the following M4 adapters via the UseCaseFactory :
* The production constructor wires the following M4 adapters:
* <ul>
* <li>{@link SqliteSchemaInitializationAdapter} — SQLite schema DDL at startup (AP-007).</li>
* <li>{@link Sha256FingerprintAdapter} — SHA-256 content fingerprinting.</li>
* <li>{@link SqliteDocumentRecordRepositoryAdapter} — document master record CRUD.</li>
* <li>{@link SqliteProcessingAttemptRepositoryAdapter} — attempt history CRUD.</li>
* <li>{@link SqliteUnitOfWorkAdapter} — atomic persistence operations.</li>
* </ul>
* <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 {
@@ -134,12 +140,14 @@ public class BootstrapRunner {
* <li>{@link SourceDocumentCandidatesPortAdapter} for PDF candidate discovery.</li>
* <li>{@link PdfTextExtractionPortAdapter} for PDFBox-based text and page count extraction.</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 SqliteProcessingAttemptRepositoryAdapter} for attempt history CRUD.</li>
* <li>{@link SqliteUnitOfWorkAdapter} for atomic persistence operations.</li>
* </ul>
* <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 ( ) {
this . configPortFactory = PropertiesConfigurationPortAdapter : : new ;
@@ -191,13 +199,27 @@ public class BootstrapRunner {
/**
* Runs the application startup sequence.
* <p>
* M4 additions :
* <u l>
* <li>Derives the SQLite JDBC URL from the configured {@code sqlite.file} path .</li>
* <li>Cre ates the M4-aware use case via the {@link UseCaseFactory}, which wires persistence ports.</li>
* </ul>
* M4 startup flow (AP-007) :
* <o l>
* <li>Load configuration via {@link ConfigurationPort} .</li>
* <li>Valid ate the configuration via {@link StartConfigurationValidator}; validation
* 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 ( ) {
LOG . info ( " Bootstrap flow started. " ) ;
@@ -208,11 +230,18 @@ public class BootstrapRunner {
// Step 2: Load configuration
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 ( ) ;
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 = new SqliteSchemaInitializationAdapter ( jdbcUrl ) ;
schemaInitPort . initializeSchema ( ) ;
// Step 5: Resolve lock file path – apply default if not configured
Path lockFilePath = config . runtimeLockFile ( ) ;
if ( lockFilePath = = null | | lockFilePath . toString ( ) . isBlank ( ) ) {
lockFilePath = Paths . get ( " pdf-umbenenner.lock " ) ;
@@ -221,21 +250,20 @@ public class BootstrapRunner {
}
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 ( ) ) ;
BatchRunContext runContext = new BatchRunContext ( runId , Instant . now ( ) ) ;
LOG . info ( " Batch run started. RunId: {} " , runId ) ;
// Step 6 : Create the use case with the validated config and run lock.
// 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.
// 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 ) ;
// 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 ) ;
// 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 ) ;
// Mark run as completed