V1.1 Änderungen
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.ai.AnthropicClaudeHttpAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.ai.OpenAiHttpAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
|
||||
|
||||
/**
|
||||
* Selects and instantiates the active {@link AiInvocationPort} implementation
|
||||
* based on the configured provider family.
|
||||
* <p>
|
||||
* This component lives in the bootstrap layer and is the single point where
|
||||
* the active provider family is mapped to its corresponding adapter implementation.
|
||||
* Exactly one provider is selected per application run; the selection is driven
|
||||
* by the value of {@code ai.provider.active}.
|
||||
*
|
||||
* <h2>Registered providers</h2>
|
||||
* <ul>
|
||||
* <li>{@link AiProviderFamily#OPENAI_COMPATIBLE} — {@link OpenAiHttpAdapter}</li>
|
||||
* <li>{@link AiProviderFamily#CLAUDE} — {@link AnthropicClaudeHttpAdapter}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Hard start failure</h2>
|
||||
* <p>
|
||||
* If the requested provider family has no registered implementation, an
|
||||
* {@link InvalidStartConfigurationException} is thrown immediately, which the
|
||||
* bootstrap runner maps to exit code 1.
|
||||
*/
|
||||
public class AiProviderSelector {
|
||||
|
||||
/**
|
||||
* Selects and constructs the {@link AiInvocationPort} implementation for the given
|
||||
* provider family using the supplied provider configuration.
|
||||
*
|
||||
* @param family the active provider family; must not be {@code null}
|
||||
* @param config the configuration for the active provider; must not be {@code null}
|
||||
* @return the constructed adapter instance; never {@code null}
|
||||
* @throws InvalidStartConfigurationException if no implementation is registered
|
||||
* for the requested provider family
|
||||
*/
|
||||
public AiInvocationPort select(AiProviderFamily family, ProviderConfiguration config) {
|
||||
Objects.requireNonNull(family, "provider family must not be null");
|
||||
Objects.requireNonNull(config, "provider configuration must not be null");
|
||||
|
||||
if (family == AiProviderFamily.OPENAI_COMPATIBLE) {
|
||||
return new OpenAiHttpAdapter(config);
|
||||
}
|
||||
|
||||
if (family == AiProviderFamily.CLAUDE) {
|
||||
return new AnthropicClaudeHttpAdapter(config);
|
||||
}
|
||||
|
||||
throw new InvalidStartConfigurationException(
|
||||
"No AI adapter implementation registered for provider family: "
|
||||
+ family.getIdentifier()
|
||||
+ ". Supported in the current build: openai-compatible, claude");
|
||||
}
|
||||
}
|
||||
+96
-14
@@ -9,11 +9,11 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.ai.OpenAiHttpAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.clock.SystemClockAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.configuration.ConfigurationLoadingException;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.configuration.LegacyConfigurationMigrator;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.configuration.PropertiesConfigurationPortAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.fingerprint.Sha256FingerprintAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.lock.FilesystemRunLockPortAdapter;
|
||||
@@ -27,6 +27,8 @@ import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteUnitOfWorkAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.targetcopy.FilesystemTargetFileCopyAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.adapter.out.targetfolder.FilesystemTargetFolderAdapter;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.RuntimeConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
||||
@@ -68,6 +70,12 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
* configuration is handed to the use case factory which extracts the minimal runtime
|
||||
* configuration for the application layer.
|
||||
*
|
||||
* <h2>Active AI provider</h2>
|
||||
* <p>
|
||||
* The active AI provider family is determined from the configuration and logged at run start.
|
||||
* The {@link AiProviderSelector} in the bootstrap layer selects the appropriate
|
||||
* {@link AiInvocationPort} implementation. Exactly one provider is active per run.
|
||||
*
|
||||
* <h2>Exit code semantics</h2>
|
||||
* <ul>
|
||||
* <li>{@code 0}: Batch run executed successfully; individual document failures do not
|
||||
@@ -82,10 +90,12 @@ import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
* <p>
|
||||
* The production constructor wires the following key adapters:
|
||||
* <ul>
|
||||
* <li>{@link PropertiesConfigurationPortAdapter} — loads configuration from properties and environment.</li>
|
||||
* <li>{@link PropertiesConfigurationPortAdapter} — loads configuration from the multi-provider
|
||||
* properties schema and environment.</li>
|
||||
* <li>{@link AiProviderSelector} — selects the active {@link AiInvocationPort} implementation
|
||||
* based on {@code ai.provider.active}.</li>
|
||||
* <li>{@link FilesystemRunLockPortAdapter} — ensures exclusive execution via a lock file.</li>
|
||||
* <li>{@link SqliteSchemaInitializationAdapter} — initializes SQLite schema (including target-copy
|
||||
* schema evolution) at startup.</li>
|
||||
* <li>{@link SqliteSchemaInitializationAdapter} — initializes SQLite schema at startup.</li>
|
||||
* <li>{@link Sha256FingerprintAdapter} — provides content-based document identification.</li>
|
||||
* <li>{@link SqliteDocumentRecordRepositoryAdapter} — manages document master records.</li>
|
||||
* <li>{@link SqliteProcessingAttemptRepositoryAdapter} — maintains attempt history.</li>
|
||||
@@ -103,6 +113,7 @@ public class BootstrapRunner {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(BootstrapRunner.class);
|
||||
|
||||
private final MigrationStep migrationStep;
|
||||
private final ConfigurationPortFactory configPortFactory;
|
||||
private final RunLockPortFactory runLockPortFactory;
|
||||
private final ValidatorFactory validatorFactory;
|
||||
@@ -110,6 +121,19 @@ public class BootstrapRunner {
|
||||
private final UseCaseFactory useCaseFactory;
|
||||
private final CommandFactory commandFactory;
|
||||
|
||||
/**
|
||||
* Functional interface encapsulating the legacy configuration migration step.
|
||||
* <p>
|
||||
* The production implementation calls {@link LegacyConfigurationMigrator#migrateIfLegacy}
|
||||
* on the active configuration file before any configuration is loaded. In tests, a
|
||||
* no-op lambda is injected so that migration does not interfere with mock configuration ports.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MigrationStep {
|
||||
/** Runs the legacy configuration migration if the configuration file is in legacy form. */
|
||||
void runIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for creating a ConfigurationPort.
|
||||
*/
|
||||
@@ -175,12 +199,12 @@ public class BootstrapRunner {
|
||||
* Wires the processing pipeline with the following adapters:
|
||||
* <ul>
|
||||
* <li>{@link PropertiesConfigurationPortAdapter} for configuration loading.</li>
|
||||
* <li>{@link AiProviderSelector} for selecting the active AI provider implementation.</li>
|
||||
* <li>{@link FilesystemRunLockPortAdapter} for exclusive run locking.</li>
|
||||
* <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 and target-copy schema
|
||||
* evolution at startup.</li>
|
||||
* <li>{@link SqliteSchemaInitializationAdapter} for SQLite schema DDL at startup.</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>
|
||||
@@ -199,6 +223,8 @@ public class BootstrapRunner {
|
||||
* begins. Failure during initialisation aborts the run with exit code 1.
|
||||
*/
|
||||
public BootstrapRunner() {
|
||||
this.migrationStep = () -> new LegacyConfigurationMigrator()
|
||||
.migrateIfLegacy(Paths.get("config/application.properties"));
|
||||
this.configPortFactory = PropertiesConfigurationPortAdapter::new;
|
||||
this.runLockPortFactory = FilesystemRunLockPortAdapter::new;
|
||||
this.validatorFactory = StartConfigurationValidator::new;
|
||||
@@ -206,7 +232,13 @@ public class BootstrapRunner {
|
||||
this.useCaseFactory = (startConfig, lock) -> {
|
||||
// Extract runtime configuration from startup configuration
|
||||
AiContentSensitivity aiContentSensitivity = resolveAiContentSensitivity(startConfig.logAiSensitive());
|
||||
RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(startConfig.maxPages(), startConfig.maxRetriesTransient(), aiContentSensitivity);
|
||||
RuntimeConfiguration runtimeConfig = new RuntimeConfiguration(
|
||||
startConfig.maxPages(), startConfig.maxRetriesTransient(), aiContentSensitivity);
|
||||
|
||||
// Select the active AI provider adapter
|
||||
AiProviderFamily activeFamily = startConfig.multiProviderConfiguration().activeProviderFamily();
|
||||
ProviderConfiguration providerConfig = startConfig.multiProviderConfiguration().activeProviderConfiguration();
|
||||
AiInvocationPort aiInvocationPort = new AiProviderSelector().select(activeFamily, providerConfig);
|
||||
|
||||
String jdbcUrl = buildJdbcUrl(startConfig);
|
||||
FingerprintPort fingerprintPort = new Sha256FingerprintAdapter();
|
||||
@@ -216,17 +248,18 @@ public class BootstrapRunner {
|
||||
new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
|
||||
UnitOfWorkPort unitOfWorkPort =
|
||||
new SqliteUnitOfWorkAdapter(jdbcUrl);
|
||||
// Wire coordinators logger with AI content sensitivity setting
|
||||
ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(DocumentProcessingCoordinator.class, aiContentSensitivity);
|
||||
// Wire coordinator logger with AI content sensitivity setting
|
||||
ProcessingLogger coordinatorLogger = new Log4jProcessingLogger(
|
||||
DocumentProcessingCoordinator.class, aiContentSensitivity);
|
||||
TargetFolderPort targetFolderPort = new FilesystemTargetFolderAdapter(startConfig.targetFolder());
|
||||
TargetFileCopyPort targetFileCopyPort = new FilesystemTargetFileCopyAdapter(startConfig.targetFolder());
|
||||
DocumentProcessingCoordinator documentProcessingCoordinator =
|
||||
new DocumentProcessingCoordinator(documentRecordRepository, processingAttemptRepository,
|
||||
unitOfWorkPort, targetFolderPort, targetFileCopyPort, coordinatorLogger,
|
||||
startConfig.maxRetriesTransient());
|
||||
startConfig.maxRetriesTransient(),
|
||||
activeFamily.getIdentifier());
|
||||
|
||||
// Wire AI naming pipeline
|
||||
AiInvocationPort aiInvocationPort = new OpenAiHttpAdapter(startConfig);
|
||||
PromptPort promptPort = new FilesystemPromptPortAdapter(startConfig.promptTemplateFile());
|
||||
ClockPort clockPort = new SystemClockAdapter();
|
||||
AiResponseValidator aiResponseValidator = new AiResponseValidator(clockPort);
|
||||
@@ -234,11 +267,12 @@ public class BootstrapRunner {
|
||||
aiInvocationPort,
|
||||
promptPort,
|
||||
aiResponseValidator,
|
||||
startConfig.apiModel(),
|
||||
providerConfig.model(),
|
||||
startConfig.maxTextCharacters());
|
||||
|
||||
// Wire use case logger with AI content sensitivity setting
|
||||
ProcessingLogger useCaseLogger = new Log4jProcessingLogger(DefaultBatchRunProcessingUseCase.class, aiContentSensitivity);
|
||||
ProcessingLogger useCaseLogger = new Log4jProcessingLogger(
|
||||
DefaultBatchRunProcessingUseCase.class, aiContentSensitivity);
|
||||
return new DefaultBatchRunProcessingUseCase(
|
||||
runtimeConfig,
|
||||
lock,
|
||||
@@ -254,6 +288,9 @@ public class BootstrapRunner {
|
||||
|
||||
/**
|
||||
* Creates the BootstrapRunner with custom factories for testing.
|
||||
* <p>
|
||||
* The migration step is set to a no-op; tests that need to exercise the migration
|
||||
* path use the full seven-parameter constructor.
|
||||
*
|
||||
* @param configPortFactory factory for creating ConfigurationPort instances
|
||||
* @param runLockPortFactory factory for creating RunLockPort instances
|
||||
@@ -268,6 +305,32 @@ public class BootstrapRunner {
|
||||
SchemaInitializationPortFactory schemaInitPortFactory,
|
||||
UseCaseFactory useCaseFactory,
|
||||
CommandFactory commandFactory) {
|
||||
this(() -> { /* no-op: tests inject mock ConfigurationPort directly */ },
|
||||
configPortFactory, runLockPortFactory, validatorFactory,
|
||||
schemaInitPortFactory, useCaseFactory, commandFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the BootstrapRunner with all factories including an explicit migration step.
|
||||
* <p>
|
||||
* Use this constructor in tests that need to exercise the full migration-then-load path.
|
||||
*
|
||||
* @param migrationStep the legacy configuration migration step to run before loading
|
||||
* @param configPortFactory factory for creating ConfigurationPort instances
|
||||
* @param runLockPortFactory factory for creating RunLockPort instances
|
||||
* @param validatorFactory factory for creating StartConfigurationValidator instances
|
||||
* @param schemaInitPortFactory factory for creating PersistenceSchemaInitializationPort instances
|
||||
* @param useCaseFactory factory for creating BatchRunProcessingUseCase instances
|
||||
* @param commandFactory factory for creating SchedulerBatchCommand instances
|
||||
*/
|
||||
public BootstrapRunner(MigrationStep migrationStep,
|
||||
ConfigurationPortFactory configPortFactory,
|
||||
RunLockPortFactory runLockPortFactory,
|
||||
ValidatorFactory validatorFactory,
|
||||
SchemaInitializationPortFactory schemaInitPortFactory,
|
||||
UseCaseFactory useCaseFactory,
|
||||
CommandFactory commandFactory) {
|
||||
this.migrationStep = migrationStep;
|
||||
this.configPortFactory = configPortFactory;
|
||||
this.runLockPortFactory = runLockPortFactory;
|
||||
this.validatorFactory = validatorFactory;
|
||||
@@ -299,6 +362,7 @@ public class BootstrapRunner {
|
||||
LOG.info("Bootstrap flow started.");
|
||||
try {
|
||||
// Bootstrap Phase: prepare configuration and persistence
|
||||
migrateConfigurationIfNeeded();
|
||||
StartConfiguration config = loadAndValidateConfiguration();
|
||||
initializeSchema(config);
|
||||
// Execution Phase: run batch processing
|
||||
@@ -318,6 +382,20 @@ public class BootstrapRunner {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the legacy configuration migration step exactly once before configuration loading.
|
||||
* <p>
|
||||
* If the configuration file is in the legacy flat-key format, it is migrated in-place to the
|
||||
* multi-provider schema before the normal configuration loading path is entered. If the file
|
||||
* is already in the current schema, this method returns immediately without any I/O side effect.
|
||||
* <p>
|
||||
* A migration failure is a hard startup error and propagates as a
|
||||
* {@link ConfigurationLoadingException}.
|
||||
*/
|
||||
private void migrateConfigurationIfNeeded() {
|
||||
migrationStep.runIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads configuration via {@link ConfigurationPort} and validates it via
|
||||
* {@link StartConfigurationValidator}.
|
||||
@@ -329,13 +407,17 @@ public class BootstrapRunner {
|
||||
* creatable (validator attempts {@code Files.createDirectories} if absent;
|
||||
* failure here is a hard startup error).</li>
|
||||
* <li>{@code sqlite.file}: parent directory must exist.</li>
|
||||
* <li>All numeric and URI constraints.</li>
|
||||
* <li>All numeric and path constraints.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* After successful validation, the active AI provider identifier is logged at INFO level.
|
||||
*/
|
||||
private StartConfiguration loadAndValidateConfiguration() {
|
||||
ConfigurationPort configPort = configPortFactory.create();
|
||||
StartConfiguration config = configPort.loadConfiguration();
|
||||
validatorFactory.create().validate(config);
|
||||
LOG.info("Active AI provider: {}",
|
||||
config.multiProviderConfiguration().activeProviderFamily().getIdentifier());
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user