1
0

Implementierung für M2 vorläufig abgeschlossen

This commit is contained in:
2026-03-31 21:52:48 +02:00
parent 301f1acf08
commit 9d66a446b3
29 changed files with 1892 additions and 52 deletions

View File

@@ -5,11 +5,20 @@ import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.adapter.inbound.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.adapter.outbound.configuration.PropertiesConfigurationPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.outbound.lock.FilesystemRunLockPortAdapter;
import de.gecheckt.pdf.umbenenner.application.config.InvalidStartConfigurationException;
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.RunBatchProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.application.usecase.NoOpRunBatchProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.usecase.M2BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import java.nio.file.Path;
import java.time.Instant;
import java.util.UUID;
/**
* Manual bootstrap runner that constructs the object graph and drives the startup flow.
@@ -17,7 +26,7 @@ import de.gecheckt.pdf.umbenenner.application.usecase.NoOpRunBatchProcessingUseC
* AP-003 Implementation: Creates all required components using plain Java constructor injection
* and executes the minimal no-op batch processing path.
* <p>
* AP-005: Integrates configuration loading via PropertiesConfigurationPortAdapter.
* AP-005: CLI adapter and bootstrap wiring for M2 batch orchestration with run lock integration.
* <p>
* AP-006: Validates configuration before processing begins, returns exit code 1 on invalid config.
*/
@@ -26,6 +35,7 @@ public class BootstrapRunner {
private static final Logger LOG = LogManager.getLogger(BootstrapRunner.class);
private final ConfigurationPortFactory configPortFactory;
private final RunLockPortFactory runLockPortFactory;
private final ValidatorFactory validatorFactory;
private final UseCaseFactory useCaseFactory;
private final CommandFactory commandFactory;
@@ -38,6 +48,14 @@ public class BootstrapRunner {
ConfigurationPort create();
}
/**
* Functional interface for creating a RunLockPort from the configured lock file path.
*/
@FunctionalInterface
public interface RunLockPortFactory {
RunLockPort create(Path lockFilePath);
}
/**
* Functional interface for creating a StartConfigurationValidator.
*/
@@ -51,7 +69,7 @@ public class BootstrapRunner {
*/
@FunctionalInterface
public interface UseCaseFactory {
RunBatchProcessingUseCase create(ConfigurationPort configPort);
RunBatchProcessingUseCase create(ConfigurationPort configPort, RunLockPort runLockPort);
}
/**
@@ -64,11 +82,14 @@ public class BootstrapRunner {
/**
* Creates the BootstrapRunner with default factories for production use.
* <p>
* AP-006: Uses FilesystemRunLockPortAdapter for file-based exclusive run locking.
*/
public BootstrapRunner() {
this.configPortFactory = PropertiesConfigurationPortAdapter::new;
this.runLockPortFactory = FilesystemRunLockPortAdapter::new;
this.validatorFactory = StartConfigurationValidator::new;
this.useCaseFactory = NoOpRunBatchProcessingUseCase::new;
this.useCaseFactory = M2BatchRunProcessingUseCase::new;
this.commandFactory = SchedulerBatchCommand::new;
}
@@ -76,15 +97,18 @@ public class BootstrapRunner {
* Creates the BootstrapRunner with custom factories for testing.
*
* @param configPortFactory factory for creating ConfigurationPort instances
* @param runLockPortFactory factory for creating RunLockPort instances
* @param validatorFactory factory for creating StartConfigurationValidator instances
* @param useCaseFactory factory for creating RunBatchProcessingUseCase instances
* @param commandFactory factory for creating SchedulerBatchCommand instances
*/
public BootstrapRunner(ConfigurationPortFactory configPortFactory,
RunLockPortFactory runLockPortFactory,
ValidatorFactory validatorFactory,
UseCaseFactory useCaseFactory,
CommandFactory commandFactory) {
this.configPortFactory = configPortFactory;
this.runLockPortFactory = runLockPortFactory;
this.validatorFactory = validatorFactory;
this.useCaseFactory = useCaseFactory;
this.commandFactory = commandFactory;
@@ -112,20 +136,37 @@ public class BootstrapRunner {
StartConfigurationValidator validator = validatorFactory.create();
validator.validate(config);
// Step 4: Create the use case with the configuration port (application layer)
RunBatchProcessingUseCase useCase = useCaseFactory.create(configPort);
// Step 4: Create the run lock port from the validated config (AP-006)
RunLockPort runLockPort = runLockPortFactory.create(config.runtimeLockFile());
// Step 5: Create the CLI command adapter with the use case
// Step 5: Create the batch run context (M2-AP-003)
// Generate a unique run ID and initialize the 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 configuration port and run lock (application layer)
RunBatchProcessingUseCase useCase = useCaseFactory.create(configPort, runLockPort);
// Step 7: Create the CLI command adapter with the use case
SchedulerBatchCommand command = commandFactory.create(useCase);
// Step 6: Execute the command
boolean success = command.run();
// Step 8: Execute the command with the run context and handle the outcome
BatchRunOutcome outcome = command.run(runContext);
if (success) {
LOG.info("No-op startup path completed successfully.");
// Mark run as completed (AP-003)
runContext.setEndInstant(Instant.now());
if (outcome.isSuccess()) {
LOG.info("Batch run completed successfully. RunId: {}", runContext.runId());
return 0;
} else if (outcome.isLockUnavailable()) {
LOG.warn("Batch run aborted: another instance is already running. RunId: {}", runContext.runId());
return 1;
} else {
LOG.error("Batch run failed. RunId: {}", runContext.runId());
return 1;
}
return success ? 0 : 1;
} catch (InvalidStartConfigurationException e) {
// Controlled failure for invalid configuration - log clearly without stack trace
LOG.error("Configuration validation failed: {}", e.getMessage());

View File

@@ -1,7 +1,29 @@
/**
* Bootstrap module for application startup and technical wiring.
* Bootstrap module for application startup and technical object graph construction.
* <p>
* Responsibility: Orchestrate the startup flow, load configuration, validate it,
* create and wire all application components, and invoke the CLI adapter entry point.
* <p>
* Components:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.bootstrap.BootstrapRunner}
* — Orchestrator of startup sequence and object graph construction (M2-AP-005)</li>
* <li>{@link de.gecheckt.pdf.umbenenner.bootstrap.PdfUmbenennerApplication}
* — Main entry point that invokes BootstrapRunner (M1)</li>
* </ul>
* <p>
* M2 Implementation:
* <ul>
* <li>Uses factory pattern with pluggable interfaces for all ports and use cases</li>
* <li>Manually constructs object graph without framework dependencies</li>
* <li>Ensures strict inward dependency direction toward application and domain</li>
* <li>Ready for extension by later milestones via factory methods</li>
* </ul>
* <p>
* This package contains the main entry point and manual object graph construction.
* AP-003: Provides a minimal, controlled startup path without dependency injection frameworks.
* <p>
* AP-005: CLI adapter and complete M2 object graph wiring.
* <p>
* AP-006: Wires FilesystemRunLockPortAdapter (adapter-out) from validated config; retired temporary no-op lock.
*/
package de.gecheckt.pdf.umbenenner.bootstrap;

View File

@@ -4,8 +4,11 @@ import de.gecheckt.pdf.umbenenner.adapter.inbound.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.application.config.InvalidStartConfigurationException;
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
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.RunBatchProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -35,8 +38,9 @@ class BootstrapRunnerTest {
// Create mock factories that return working components
BootstrapRunner runner = new BootstrapRunner(
() -> mockConfigPort,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
port -> new MockRunBatchProcessingUseCase(true),
(port, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase)
);
@@ -60,8 +64,9 @@ class BootstrapRunnerTest {
BootstrapRunner runner = new BootstrapRunner(
() -> mockConfigPort,
lockFile -> new MockRunLockPort(),
() -> failingValidator,
port -> new MockRunBatchProcessingUseCase(true),
(port, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase)
);
@@ -79,8 +84,9 @@ class BootstrapRunnerTest {
BootstrapRunner runner = new BootstrapRunner(
() -> failingConfigPort,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
port -> new MockRunBatchProcessingUseCase(true),
(port, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase)
);
@@ -98,8 +104,9 @@ class BootstrapRunnerTest {
BootstrapRunner runner = new BootstrapRunner(
() -> throwingConfigPort,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
port -> new MockRunBatchProcessingUseCase(true),
(port, lock) -> new MockRunBatchProcessingUseCase(true),
useCase -> new SchedulerBatchCommand(useCase)
);
@@ -109,23 +116,45 @@ class BootstrapRunnerTest {
}
@Test
void run_returnsOneWhenCommandReturnsFalse() throws Exception {
void run_returnsOneWhenBatchFails() throws Exception {
// Create a mock configuration port that returns valid config
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
// Create a use case that returns false
RunBatchProcessingUseCase failingUseCase = () -> false;
// Create a use case that returns failure outcome
RunBatchProcessingUseCase failingUseCase = (context) -> BatchRunOutcome.FAILURE;
BootstrapRunner runner = new BootstrapRunner(
() -> mockConfigPort,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
port -> failingUseCase,
(port, lock) -> failingUseCase,
useCase -> new SchedulerBatchCommand(useCase)
);
int exitCode = runner.run();
assertEquals(1, exitCode, "Command returning false should return exit code 1");
assertEquals(1, exitCode, "Batch failure outcome should return exit code 1");
}
@Test
void run_returnsOneWhenLockUnavailable() throws Exception {
// AP-007: controlled early termination because another instance is already running
// maps to exit code 1 (start protection counts as a hard startup failure)
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
RunBatchProcessingUseCase lockUnavailableUseCase = (context) -> BatchRunOutcome.LOCK_UNAVAILABLE;
BootstrapRunner runner = new BootstrapRunner(
() -> mockConfigPort,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
(port, lock) -> lockUnavailableUseCase,
useCase -> new SchedulerBatchCommand(useCase)
);
int exitCode = runner.run();
assertEquals(1, exitCode, "Lock unavailable (another instance running) should return exit code 1");
}
@Test
@@ -194,8 +223,23 @@ class BootstrapRunnerTest {
}
@Override
public boolean execute() {
return shouldSucceed;
public BatchRunOutcome execute(BatchRunContext context) {
return shouldSucceed ? BatchRunOutcome.SUCCESS : BatchRunOutcome.FAILURE;
}
}
/**
* Mock RunLockPort for testing.
*/
private static class MockRunLockPort implements RunLockPort {
@Override
public void acquire() {
// Mock: no-op
}
@Override
public void release() {
// Mock: no-op
}
}
}