M1 Vollständiger Grundstand mit Build, Konfiguration, Tests und Smoke-Tests
This commit is contained in:
73
.gitignore
vendored
Normal file
73
.gitignore
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
# =========================================================
|
||||
# Build-Artefakte (Maven)
|
||||
# =========================================================
|
||||
**/target/
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
# =========================================================
|
||||
# Eclipse / IDE
|
||||
# Gilt für Parent und alle Module
|
||||
# =========================================================
|
||||
**/.project
|
||||
**/.classpath
|
||||
**/.settings/
|
||||
.metadata/
|
||||
|
||||
# APT / Eclipse Build
|
||||
**/.apt_generated/
|
||||
**/.factorypath
|
||||
|
||||
# =========================================================
|
||||
# Weitere IDEs / Editoren
|
||||
# =========================================================
|
||||
.idea/
|
||||
.vscode/
|
||||
**/*.iml
|
||||
**/*.ipr
|
||||
**/*.iws
|
||||
|
||||
# =========================================================
|
||||
# Laufzeit-Konfiguration
|
||||
# Diese Datei bleibt lokal und wird nicht eingecheckt.
|
||||
# Beispiel-Dateien wie
|
||||
# - config/application-local.example.properties
|
||||
# - config/application-test.example.properties
|
||||
# sollen weiterhin eingecheckt werden.
|
||||
# =========================================================
|
||||
config/application.properties
|
||||
|
||||
# =========================================================
|
||||
# Laufzeitdaten / lokale Arbeitsverzeichnisse
|
||||
# =========================================================
|
||||
work/
|
||||
logs/
|
||||
data/
|
||||
|
||||
# =========================================================
|
||||
# Lokale Datenbanken / Lock-/PID-/Log-Dateien
|
||||
# =========================================================
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.log
|
||||
*.lck
|
||||
*.lock
|
||||
*.pid
|
||||
|
||||
# =========================================================
|
||||
# Temporäre Dateien / OS-Dateien
|
||||
# =========================================================
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# =========================================================
|
||||
# JVM Crash Logs
|
||||
# =========================================================
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
21
config/application-local.example.properties
Normal file
21
config/application-local.example.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# PDF Umbenenner Local Configuration Example
|
||||
# AP-005: Copy this file to config/application.properties and adjust values for local development
|
||||
|
||||
# Mandatory M1 properties
|
||||
source.folder=./work/local/source
|
||||
target.folder=./work/local/target
|
||||
sqlite.file=./work/local/pdf-umbenenner.db
|
||||
api.baseUrl=http://localhost:8080/api
|
||||
api.model=gpt-4o-mini
|
||||
api.timeoutSeconds=30
|
||||
max.retries.transient=3
|
||||
max.pages=10
|
||||
max.text.characters=5000
|
||||
prompt.template.file=./config/prompts/local-template.txt
|
||||
|
||||
# Optional properties
|
||||
runtime.lock.file=./work/local/lock.pid
|
||||
log.directory=./work/local/logs
|
||||
log.level=INFO
|
||||
# api.key can also be set via environment variable PDF_UMBENENNER_API_KEY
|
||||
api.key=your-local-api-key-here
|
||||
21
config/application-test.example.properties
Normal file
21
config/application-test.example.properties
Normal file
@@ -0,0 +1,21 @@
|
||||
# PDF Umbenenner Test Configuration Example
|
||||
# AP-005: Copy this file to config/application.properties and adjust values for testing
|
||||
|
||||
# Mandatory M1 properties
|
||||
source.folder=./work/test/source
|
||||
target.folder=./work/test/target
|
||||
sqlite.file=./work/test/pdf-umbenenner-test.db
|
||||
api.baseUrl=http://localhost:8081/api
|
||||
api.model=gpt-4o-mini-test
|
||||
api.timeoutSeconds=10
|
||||
max.retries.transient=1
|
||||
max.pages=5
|
||||
max.text.characters=2000
|
||||
prompt.template.file=./config/prompts/test-template.txt
|
||||
|
||||
# Optional properties
|
||||
runtime.lock.file=./work/test/lock.pid
|
||||
log.directory=./work/test/logs
|
||||
log.level=DEBUG
|
||||
# api.key can also be set via environment variable PDF_UMBENENNER_API_KEY
|
||||
api.key=test-api-key-placeholder
|
||||
1
config/prompts/template.txt
Normal file
1
config/prompts/template.txt
Normal file
@@ -0,0 +1 @@
|
||||
This is a test prompt template for AP-006 validation.
|
||||
42
pdf-umbenenner-adapter-in-cli/pom.xml
Normal file
42
pdf-umbenenner-adapter-in-cli/pom.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>pdf-umbenenner-adapter-in-cli</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Internal dependencies -->
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-domain</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.inbound.cli;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase;
|
||||
|
||||
/**
|
||||
* CLI command adapter for batch processing scheduling.
|
||||
* <p>
|
||||
* This class acts as the technical entry point that delegates to the application layer.
|
||||
* AP-003 Implementation: Minimal no-op command to validate the call chain from CLI to Application.
|
||||
*/
|
||||
public class SchedulerBatchCommand {
|
||||
|
||||
private final RunBatchProcessingUseCase useCase;
|
||||
|
||||
/**
|
||||
* Creates a new SchedulerBatchCommand with the given use case.
|
||||
*
|
||||
* @param useCase the batch processing use case to delegate to
|
||||
*/
|
||||
public SchedulerBatchCommand(RunBatchProcessingUseCase useCase) {
|
||||
this.useCase = useCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the batch processing command.
|
||||
* <p>
|
||||
* AP-003: Delegates to the use case without any additional logic.
|
||||
*
|
||||
* @return true if execution succeeded, false otherwise
|
||||
*/
|
||||
public boolean run() {
|
||||
return useCase.execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* CLI adapter for inbound commands.
|
||||
* This package contains the technical entry points that delegate to application use cases.
|
||||
* <p>
|
||||
* AP-003: Contains minimal command classes for validating the startup path.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.adapter.inbound.cli;
|
||||
1
pdf-umbenenner-adapter-in-cli/src/test/java/.gitkeep
Normal file
1
pdf-umbenenner-adapter-in-cli/src/test/java/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
56
pdf-umbenenner-adapter-out/pom.xml
Normal file
56
pdf-umbenenner-adapter-out/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>pdf-umbenenner-adapter-out</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Internal dependencies -->
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-domain</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Infrastructure dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,165 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound.configuration;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Properties-based implementation of {@link ConfigurationPort}.
|
||||
* AP-005: Loads configuration from config/application.properties with environment variable precedence.
|
||||
*/
|
||||
public class PropertiesConfigurationPortAdapter implements ConfigurationPort {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(PropertiesConfigurationPortAdapter.class);
|
||||
private static final String DEFAULT_CONFIG_FILE_PATH = "config/application.properties";
|
||||
private static final String API_KEY_ENV_VAR = "PDF_UMBENENNER_API_KEY";
|
||||
|
||||
private final Function<String, String> environmentLookup;
|
||||
private final Path configFilePath;
|
||||
|
||||
/**
|
||||
* Creates the adapter with default system environment lookup and default config file path.
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter() {
|
||||
this(System::getenv, Paths.get(DEFAULT_CONFIG_FILE_PATH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the adapter with a custom environment lookup function.
|
||||
* <p>
|
||||
* This constructor is primarily intended for testing purposes to allow deterministic
|
||||
* control over environment variable values without modifying the real process environment.
|
||||
*
|
||||
* @param environmentLookup a function that looks up environment variables by name
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter(Function<String, String> environmentLookup) {
|
||||
this(environmentLookup, Paths.get(DEFAULT_CONFIG_FILE_PATH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the adapter with a custom config file path.
|
||||
* <p>
|
||||
* This constructor is primarily intended for testing purposes to allow loading
|
||||
* configuration from arbitrary temporary files.
|
||||
*
|
||||
* @param configFilePath the path to the configuration properties file
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter(Path configFilePath) {
|
||||
this(System::getenv, configFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the adapter with custom environment lookup and config file path.
|
||||
* <p>
|
||||
* This constructor is primarily intended for testing purposes.
|
||||
*
|
||||
* @param environmentLookup a function that looks up environment variables by name
|
||||
* @param configFilePath the path to the configuration properties file
|
||||
*/
|
||||
public PropertiesConfigurationPortAdapter(Function<String, String> environmentLookup, Path configFilePath) {
|
||||
this.environmentLookup = environmentLookup;
|
||||
this.configFilePath = configFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartConfiguration loadConfiguration() {
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
// Check if file exists first to preserve FileNotFoundException behavior for tests
|
||||
if (!Files.exists(configFilePath)) {
|
||||
throw new java.io.FileNotFoundException("Config file not found: " + configFilePath);
|
||||
}
|
||||
// Read file content as string to avoid escape sequence interpretation issues
|
||||
String content = Files.readString(configFilePath, StandardCharsets.UTF_8);
|
||||
// Escape backslashes to prevent Java Properties from interpreting them as escape sequences
|
||||
// This is needed because Windows paths use backslashes (e.g., C:\temp\...)
|
||||
// and Java Properties interprets \t as tab, \n as newline, etc.
|
||||
String escapedContent = content.replace("\\", "\\\\");
|
||||
props.load(new StringReader(escapedContent));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load configuration from " + configFilePath, e);
|
||||
}
|
||||
|
||||
// Apply environment variable precedence for api.key
|
||||
String apiKey = getApiKey(props);
|
||||
|
||||
return new StartConfiguration(
|
||||
Paths.get(getRequiredProperty(props, "source.folder")),
|
||||
Paths.get(getRequiredProperty(props, "target.folder")),
|
||||
Paths.get(getRequiredProperty(props, "sqlite.file")),
|
||||
parseUri(getRequiredProperty(props, "api.baseUrl")),
|
||||
getRequiredProperty(props, "api.model"),
|
||||
parseInt(getRequiredProperty(props, "api.timeoutSeconds")),
|
||||
parseInt(getRequiredProperty(props, "max.retries.transient")),
|
||||
parseInt(getRequiredProperty(props, "max.pages")),
|
||||
parseInt(getRequiredProperty(props, "max.text.characters")),
|
||||
Paths.get(getRequiredProperty(props, "prompt.template.file")),
|
||||
Paths.get(getOptionalProperty(props, "runtime.lock.file", "")),
|
||||
Paths.get(getOptionalProperty(props, "log.directory", "")),
|
||||
getOptionalProperty(props, "log.level", "INFO"),
|
||||
apiKey
|
||||
);
|
||||
}
|
||||
|
||||
private String getApiKey(Properties props) {
|
||||
String envApiKey = environmentLookup.apply(API_KEY_ENV_VAR);
|
||||
if (envApiKey != null && !envApiKey.isBlank()) {
|
||||
LOG.info("Using API key from environment variable {}", API_KEY_ENV_VAR);
|
||||
return envApiKey;
|
||||
}
|
||||
String propsApiKey = props.getProperty("api.key");
|
||||
return propsApiKey != null ? propsApiKey : "";
|
||||
}
|
||||
|
||||
private String getRequiredProperty(Properties props, String key) {
|
||||
String value = props.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalStateException("Required property missing: " + key);
|
||||
}
|
||||
return normalizePath(value.trim());
|
||||
}
|
||||
|
||||
private String getOptionalProperty(Properties props, String key, String defaultValue) {
|
||||
String value = props.getProperty(key);
|
||||
return (value == null || value.isBlank()) ? defaultValue : normalizePath(value.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a property value that represents a path.
|
||||
* Converts backslashes to forward slashes to avoid issues with Java Properties
|
||||
* escape sequence interpretation (e.g., \t becoming tab).
|
||||
* Also trims whitespace from the value.
|
||||
*/
|
||||
private String normalizePath(String value) {
|
||||
return value.replace('\\', '/');
|
||||
}
|
||||
|
||||
private int parseInt(String value) {
|
||||
try {
|
||||
return Integer.parseInt(value.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalStateException("Invalid integer value for property: " + value, e);
|
||||
}
|
||||
}
|
||||
|
||||
private URI parseUri(String value) {
|
||||
try {
|
||||
return new URI(value.trim());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("Invalid URI value for property: " + value, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Configuration adapters for outbound infrastructure access.
|
||||
* Contains implementations of configuration loading from external sources.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound.configuration;
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Outbound adapters for infrastructure interactions.
|
||||
* <p>
|
||||
* This package contains implementations of outbound ports for external systems
|
||||
* (PDF processing, database access, HTTP clients, etc.).
|
||||
* AP-003: Currently empty as no outbound implementations have been created yet.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound;
|
||||
1
pdf-umbenenner-adapter-out/src/test/java/.gitkeep
Normal file
1
pdf-umbenenner-adapter-out/src/test/java/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,295 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.outbound.configuration;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.FileWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link PropertiesConfigurationPortAdapter}.
|
||||
* <p>
|
||||
* Tests cover valid configuration loading, missing mandatory properties,
|
||||
* invalid property values, and API-key environment variable precedence.
|
||||
*/
|
||||
class PropertiesConfigurationPortAdapterTest {
|
||||
|
||||
private Function<String, String> emptyEnvLookup;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
emptyEnvLookup = key -> null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_successWithValidProperties() throws Exception {
|
||||
Path configFile = createConfigFile("valid-config.properties");
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertNotNull(config);
|
||||
// Use endsWith to handle platform-specific path separators
|
||||
assertTrue(config.sourceFolder().toString().endsWith("source"));
|
||||
assertTrue(config.targetFolder().toString().endsWith("target"));
|
||||
assertTrue(config.sqliteFile().toString().endsWith("db.sqlite"));
|
||||
assertEquals("https://api.example.com", config.apiBaseUrl().toString());
|
||||
assertEquals("gpt-4", config.apiModel());
|
||||
assertEquals(30, config.apiTimeoutSeconds());
|
||||
assertEquals(3, config.maxRetriesTransient());
|
||||
assertEquals(100, config.maxPages());
|
||||
assertEquals(50000, config.maxTextCharacters());
|
||||
assertTrue(config.promptTemplateFile().toString().endsWith("prompt.txt"));
|
||||
assertTrue(config.runtimeLockFile().toString().endsWith("lock.lock"));
|
||||
assertTrue(config.logDirectory().toString().endsWith("logs"));
|
||||
assertEquals("DEBUG", config.logLevel());
|
||||
assertEquals("test-api-key-from-properties", config.apiKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsAbsent() throws Exception {
|
||||
Path configFile = createConfigFile("no-api-key.properties");
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("", config.apiKey(), "API key should be empty when not in properties and no env var");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsNull() throws Exception {
|
||||
Path configFile = createConfigFile("no-api-key.properties");
|
||||
|
||||
Function<String, String> envLookup = key -> null;
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("", config.apiKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsEmpty() throws Exception {
|
||||
Path configFile = createConfigFile("no-api-key.properties");
|
||||
|
||||
Function<String, String> envLookup = key -> "";
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("", config.apiKey(), "Empty env var should fall back to empty string");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_usesPropertiesApiKeyWhenEnvVarIsBlank() throws Exception {
|
||||
Path configFile = createConfigFile("no-api-key.properties");
|
||||
|
||||
Function<String, String> envLookup = key -> " ";
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("", config.apiKey(), "Blank env var should fall back to empty string");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_envVarOverridesPropertiesApiKey() throws Exception {
|
||||
Path configFile = createConfigFile("valid-config.properties");
|
||||
|
||||
Function<String, String> envLookup = key -> {
|
||||
if ("PDF_UMBENENNER_API_KEY".equals(key)) {
|
||||
return "env-api-key-override";
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(envLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("env-api-key-override", config.apiKey(), "Environment variable should override properties");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_throwsIllegalStateExceptionWhenRequiredPropertyMissing() throws Exception {
|
||||
Path configFile = createConfigFile("missing-required.properties");
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
IllegalStateException exception = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> adapter.loadConfiguration()
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("Required property missing"));
|
||||
assertTrue(exception.getMessage().contains("sqlite.file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_throwsRuntimeExceptionWhenConfigFileNotFound() {
|
||||
Path nonExistentFile = tempDir.resolve("nonexistent.properties");
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile);
|
||||
|
||||
RuntimeException exception = assertThrows(
|
||||
RuntimeException.class,
|
||||
() -> adapter.loadConfiguration()
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("Failed to load configuration"));
|
||||
assertTrue(exception.getCause() instanceof java.io.FileNotFoundException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_parsesIntegerValuesCorrectly() throws Exception {
|
||||
Path configFile = createInlineConfig(
|
||||
"source.folder=/tmp/source\n" +
|
||||
"target.folder=/tmp/target\n" +
|
||||
"sqlite.file=/tmp/db.sqlite\n" +
|
||||
"api.baseUrl=https://api.example.com\n" +
|
||||
"api.model=gpt-4\n" +
|
||||
"api.timeoutSeconds=60\n" +
|
||||
"max.retries.transient=5\n" +
|
||||
"max.pages=200\n" +
|
||||
"max.text.characters=100000\n" +
|
||||
"prompt.template.file=/tmp/prompt.txt\n" +
|
||||
"api.key=test-key\n"
|
||||
);
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals(60, config.apiTimeoutSeconds());
|
||||
assertEquals(5, config.maxRetriesTransient());
|
||||
assertEquals(200, config.maxPages());
|
||||
assertEquals(100000, config.maxTextCharacters());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_handlesWhitespaceInIntegerValues() throws Exception {
|
||||
Path configFile = createInlineConfig(
|
||||
"source.folder=/tmp/source\n" +
|
||||
"target.folder=/tmp/target\n" +
|
||||
"sqlite.file=/tmp/db.sqlite\n" +
|
||||
"api.baseUrl=https://api.example.com\n" +
|
||||
"api.model=gpt-4\n" +
|
||||
"api.timeoutSeconds= 45 \n" +
|
||||
"max.retries.transient=2\n" +
|
||||
"max.pages=150\n" +
|
||||
"max.text.characters=75000\n" +
|
||||
"prompt.template.file=/tmp/prompt.txt\n" +
|
||||
"api.key=test-key\n"
|
||||
);
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals(45, config.apiTimeoutSeconds(), "Whitespace should be trimmed from integer values");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_throwsIllegalStateExceptionForInvalidIntegerValue() throws Exception {
|
||||
Path configFile = createInlineConfig(
|
||||
"source.folder=/tmp/source\n" +
|
||||
"target.folder=/tmp/target\n" +
|
||||
"sqlite.file=/tmp/db.sqlite\n" +
|
||||
"api.baseUrl=https://api.example.com\n" +
|
||||
"api.model=gpt-4\n" +
|
||||
"api.timeoutSeconds=not-a-number\n" +
|
||||
"max.retries.transient=2\n" +
|
||||
"max.pages=150\n" +
|
||||
"max.text.characters=75000\n" +
|
||||
"prompt.template.file=/tmp/prompt.txt\n" +
|
||||
"api.key=test-key\n"
|
||||
);
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
IllegalStateException exception = assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> adapter.loadConfiguration()
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("Invalid integer value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_parsesUriCorrectly() throws Exception {
|
||||
Path configFile = createInlineConfig(
|
||||
"source.folder=/tmp/source\n" +
|
||||
"target.folder=/tmp/target\n" +
|
||||
"sqlite.file=/tmp/db.sqlite\n" +
|
||||
"api.baseUrl=https://api.example.com:8080/v1\n" +
|
||||
"api.model=gpt-4\n" +
|
||||
"api.timeoutSeconds=30\n" +
|
||||
"max.retries.transient=3\n" +
|
||||
"max.pages=100\n" +
|
||||
"max.text.characters=50000\n" +
|
||||
"prompt.template.file=/tmp/prompt.txt\n" +
|
||||
"api.key=test-key\n"
|
||||
);
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("https://api.example.com:8080/v1", config.apiBaseUrl().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadConfiguration_defaultsOptionalValuesWhenNotPresent() throws Exception {
|
||||
Path configFile = createInlineConfig(
|
||||
"source.folder=/tmp/source\n" +
|
||||
"target.folder=/tmp/target\n" +
|
||||
"sqlite.file=/tmp/db.sqlite\n" +
|
||||
"api.baseUrl=https://api.example.com\n" +
|
||||
"api.model=gpt-4\n" +
|
||||
"api.timeoutSeconds=30\n" +
|
||||
"max.retries.transient=3\n" +
|
||||
"max.pages=100\n" +
|
||||
"max.text.characters=50000\n" +
|
||||
"prompt.template.file=/tmp/prompt.txt\n" +
|
||||
"api.key=test-key\n"
|
||||
);
|
||||
|
||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||
|
||||
var config = adapter.loadConfiguration();
|
||||
|
||||
assertEquals("", config.runtimeLockFile().toString(), "runtime.lock.file should default to empty");
|
||||
assertEquals("", config.logDirectory().toString(), "log.directory should default to empty");
|
||||
assertEquals("INFO", config.logLevel(), "log.level should default to INFO");
|
||||
}
|
||||
|
||||
private Path createConfigFile(String resourceName) throws Exception {
|
||||
Path sourceResource = Path.of("src/test/resources", resourceName);
|
||||
Path targetConfigFile = tempDir.resolve("application.properties");
|
||||
|
||||
// Copy content from resource file
|
||||
Files.copy(sourceResource, targetConfigFile);
|
||||
return targetConfigFile;
|
||||
}
|
||||
|
||||
private Path createInlineConfig(String content) throws Exception {
|
||||
Path configFile = tempDir.resolve("config.properties");
|
||||
try (FileWriter writer = new FileWriter(configFile.toFile())) {
|
||||
writer.write(content);
|
||||
}
|
||||
return configFile;
|
||||
}
|
||||
}
|
||||
1
pdf-umbenenner-adapter-out/src/test/resources/.gitkeep
Normal file
1
pdf-umbenenner-adapter-out/src/test/resources/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,11 @@
|
||||
source.folder=/tmp/source
|
||||
target.folder=/tmp/target
|
||||
# sqlite.file is missing
|
||||
api.baseUrl=https://api.example.com
|
||||
api.model=gpt-4
|
||||
api.timeoutSeconds=30
|
||||
max.retries.transient=3
|
||||
max.pages=100
|
||||
max.text.characters=50000
|
||||
prompt.template.file=/tmp/prompt.txt
|
||||
api.key=test-api-key
|
||||
@@ -0,0 +1,10 @@
|
||||
source.folder=/tmp/source
|
||||
target.folder=/tmp/target
|
||||
sqlite.file=/tmp/db.sqlite
|
||||
api.baseUrl=https://api.example.com
|
||||
api.model=gpt-4
|
||||
api.timeoutSeconds=30
|
||||
max.retries.transient=3
|
||||
max.pages=100
|
||||
max.text.characters=50000
|
||||
prompt.template.file=/tmp/prompt.txt
|
||||
@@ -0,0 +1,14 @@
|
||||
source.folder=/tmp/source
|
||||
target.folder=/tmp/target
|
||||
sqlite.file=/tmp/db.sqlite
|
||||
api.baseUrl=https://api.example.com
|
||||
api.model=gpt-4
|
||||
api.timeoutSeconds=30
|
||||
max.retries.transient=3
|
||||
max.pages=100
|
||||
max.text.characters=50000
|
||||
prompt.template.file=/tmp/prompt.txt
|
||||
runtime.lock.file=/tmp/lock.lock
|
||||
log.directory=/tmp/logs
|
||||
log.level=DEBUG
|
||||
api.key=test-api-key-from-properties
|
||||
43
pdf-umbenenner-application/pom.xml
Normal file
43
pdf-umbenenner-application/pom.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>pdf-umbenenner-application</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Internal dependency -->
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-domain</artifactId>
|
||||
<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>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config;
|
||||
|
||||
/**
|
||||
* Exception thrown when startup configuration validation fails.
|
||||
* <p>
|
||||
* Contains an aggregated message describing all validation errors found.
|
||||
* This is a controlled failure mode that prevents processing from starting.
|
||||
*/
|
||||
public class InvalidStartConfigurationException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates the exception with an aggregated error message.
|
||||
*
|
||||
* @param message the aggregated validation error message
|
||||
*/
|
||||
public InvalidStartConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Typed immutable configuration model for PDF Umbenenner startup parameters.
|
||||
* AP-005: Represents all M1-relevant configuration properties with strong typing.
|
||||
*/
|
||||
public record StartConfiguration(
|
||||
Path sourceFolder,
|
||||
Path targetFolder,
|
||||
Path sqliteFile,
|
||||
URI apiBaseUrl,
|
||||
String apiModel,
|
||||
int apiTimeoutSeconds,
|
||||
int maxRetriesTransient,
|
||||
int maxPages,
|
||||
int maxTextCharacters,
|
||||
Path promptTemplateFile,
|
||||
Path runtimeLockFile,
|
||||
Path logDirectory,
|
||||
String logLevel,
|
||||
String apiKey
|
||||
)
|
||||
{ }
|
||||
@@ -0,0 +1,198 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Validates {@link StartConfiguration} before processing can begin.
|
||||
* <p>
|
||||
* Performs mandatory field checks, numeric range validation, URI scheme validation,
|
||||
* and basic path existence checks. Throws {@link InvalidStartConfigurationException}
|
||||
* if any validation rule fails.
|
||||
*/
|
||||
public class StartConfigurationValidator {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StartConfigurationValidator.class);
|
||||
|
||||
/**
|
||||
* Validates the given configuration.
|
||||
* <p>
|
||||
* Checks all mandatory fields, numeric constraints, URI validity, and path existence.
|
||||
* If validation fails, throws {@link InvalidStartConfigurationException} with an
|
||||
* aggregated error message listing all problems.
|
||||
*
|
||||
* @param config the configuration to validate, must not be null
|
||||
* @throws InvalidStartConfigurationException if any validation rule fails
|
||||
*/
|
||||
public void validate(StartConfiguration config) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
// Mandatory string/path presence checks
|
||||
validateSourceFolder(config.sourceFolder(), errors);
|
||||
validateTargetFolder(config.targetFolder(), errors);
|
||||
validateSqliteFile(config.sqliteFile(), errors);
|
||||
validateApiBaseUrl(config.apiBaseUrl(), errors);
|
||||
validateApiModel(config.apiModel(), errors);
|
||||
validatePromptTemplateFile(config.promptTemplateFile(), errors);
|
||||
|
||||
// Numeric validation
|
||||
validateApiTimeoutSeconds(config.apiTimeoutSeconds(), errors);
|
||||
validateMaxRetriesTransient(config.maxRetriesTransient(), errors);
|
||||
validateMaxPages(config.maxPages(), errors);
|
||||
validateMaxTextCharacters(config.maxTextCharacters(), errors);
|
||||
|
||||
// Path relationship validation
|
||||
validateSourceAndTargetNotSame(config.sourceFolder(), config.targetFolder(), errors);
|
||||
|
||||
// Optional path validations (only if present)
|
||||
validateRuntimeLockFile(config.runtimeLockFile(), errors);
|
||||
validateLogDirectory(config.logDirectory(), errors);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
String errorMessage = "Invalid startup configuration:\n" + String.join("\n", errors);
|
||||
throw new InvalidStartConfigurationException(errorMessage);
|
||||
}
|
||||
|
||||
LOG.info("Configuration validation successful.");
|
||||
}
|
||||
|
||||
private void validateSourceFolder(Path sourceFolder, List<String> errors) {
|
||||
if (sourceFolder == null) {
|
||||
errors.add("- source.folder: must not be null");
|
||||
return;
|
||||
}
|
||||
if (!Files.exists(sourceFolder)) {
|
||||
errors.add("- source.folder: path does not exist: " + sourceFolder);
|
||||
} else if (!Files.isDirectory(sourceFolder)) {
|
||||
errors.add("- source.folder: path is not a directory: " + sourceFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTargetFolder(Path targetFolder, List<String> errors) {
|
||||
if (targetFolder == null) {
|
||||
errors.add("- target.folder: must not be null");
|
||||
return;
|
||||
}
|
||||
if (!Files.exists(targetFolder)) {
|
||||
errors.add("- target.folder: path does not exist: " + targetFolder);
|
||||
} else if (!Files.isDirectory(targetFolder)) {
|
||||
errors.add("- target.folder: path is not a directory: " + targetFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSqliteFile(Path sqliteFile, List<String> errors) {
|
||||
if (sqliteFile == null) {
|
||||
errors.add("- sqlite.file: must not be null");
|
||||
return;
|
||||
}
|
||||
Path parent = sqliteFile.getParent();
|
||||
if (parent == null) {
|
||||
errors.add("- sqlite.file: has no parent directory: " + sqliteFile);
|
||||
} else if (!Files.exists(parent)) {
|
||||
errors.add("- sqlite.file: parent directory does not exist: " + parent);
|
||||
} else if (!Files.isDirectory(parent)) {
|
||||
errors.add("- sqlite.file: parent is not a directory: " + parent);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateApiBaseUrl(java.net.URI apiBaseUrl, List<String> errors) {
|
||||
if (apiBaseUrl == null) {
|
||||
errors.add("- api.baseUrl: must not be null");
|
||||
return;
|
||||
}
|
||||
if (!apiBaseUrl.isAbsolute()) {
|
||||
errors.add("- api.baseUrl: must be an absolute URI: " + apiBaseUrl);
|
||||
return;
|
||||
}
|
||||
String scheme = apiBaseUrl.getScheme();
|
||||
if (scheme == null || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
|
||||
errors.add("- api.baseUrl: scheme must be http or https, got: " + scheme);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateApiModel(String apiModel, List<String> errors) {
|
||||
if (apiModel == null || apiModel.isBlank()) {
|
||||
errors.add("- api.model: must not be null or blank");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateApiTimeoutSeconds(int apiTimeoutSeconds, List<String> errors) {
|
||||
if (apiTimeoutSeconds <= 0) {
|
||||
errors.add("- api.timeoutSeconds: must be > 0, got: " + apiTimeoutSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMaxRetriesTransient(int maxRetriesTransient, List<String> errors) {
|
||||
if (maxRetriesTransient < 0) {
|
||||
errors.add("- max.retries.transient: must be >= 0, got: " + maxRetriesTransient);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMaxPages(int maxPages, List<String> errors) {
|
||||
if (maxPages <= 0) {
|
||||
errors.add("- max.pages: must be > 0, got: " + maxPages);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMaxTextCharacters(int maxTextCharacters, List<String> errors) {
|
||||
if (maxTextCharacters <= 0) {
|
||||
errors.add("- max.text.characters: must be > 0, got: " + maxTextCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePromptTemplateFile(Path promptTemplateFile, List<String> errors) {
|
||||
if (promptTemplateFile == null) {
|
||||
errors.add("- prompt.template.file: must not be null");
|
||||
return;
|
||||
}
|
||||
if (!Files.exists(promptTemplateFile)) {
|
||||
errors.add("- prompt.template.file: path does not exist: " + promptTemplateFile);
|
||||
} else if (!Files.isRegularFile(promptTemplateFile)) {
|
||||
errors.add("- prompt.template.file: path is not a regular file: " + promptTemplateFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSourceAndTargetNotSame(Path sourceFolder, Path targetFolder, List<String> errors) {
|
||||
if (sourceFolder != null && targetFolder != null) {
|
||||
try {
|
||||
Path normalizedSource = sourceFolder.toRealPath();
|
||||
Path normalizedTarget = targetFolder.toRealPath();
|
||||
if (normalizedSource.equals(normalizedTarget)) {
|
||||
errors.add("- source.folder and target.folder must not resolve to the same path: " + normalizedSource);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// If toRealPath fails (e.g., path doesn't exist), skip this check
|
||||
// The individual existence checks will catch missing paths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRuntimeLockFile(Path runtimeLockFile, List<String> errors) {
|
||||
if (runtimeLockFile != null && !runtimeLockFile.toString().isBlank()) {
|
||||
Path parent = runtimeLockFile.getParent();
|
||||
if (parent != null) {
|
||||
if (!Files.exists(parent)) {
|
||||
errors.add("- runtime.lock.file: parent directory does not exist: " + parent);
|
||||
} else if (!Files.isDirectory(parent)) {
|
||||
errors.add("- runtime.lock.file: parent is not a directory: " + parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLogDirectory(Path logDirectory, List<String> errors) {
|
||||
if (logDirectory != null && !logDirectory.toString().isBlank()) {
|
||||
if (Files.exists(logDirectory)) {
|
||||
if (!Files.isDirectory(logDirectory)) {
|
||||
errors.add("- log.directory: exists but is not a directory: " + logDirectory);
|
||||
}
|
||||
}
|
||||
// If it doesn't exist yet, that's acceptable - we don't auto-create
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Configuration model types for the PDF Umbenenner application.
|
||||
* Contains typed configuration objects representing startup parameters.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.config;
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Application layer containing use cases and inbound ports.
|
||||
* This package defines the business logic boundary without infrastructure details.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application;
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
|
||||
/**
|
||||
* Inbound port for batch processing execution.
|
||||
* This interface defines the contract for triggering batch operations.
|
||||
* <p>
|
||||
* AP-003 Implementation: Currently a no-op placeholder to establish the technical startup path.
|
||||
*/
|
||||
public interface RunBatchProcessingUseCase {
|
||||
/**
|
||||
* Executes the batch processing workflow.
|
||||
* <p>
|
||||
* AP-003: This method performs no actual work, only validates the call chain.
|
||||
*
|
||||
* @return true if the workflow completed successfully, false otherwise
|
||||
*/
|
||||
boolean execute();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Inbound ports (application service interfaces) for the hexagonal architecture.
|
||||
* Use cases in this package are invoked by adapters from the outside world.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
|
||||
/**
|
||||
* Outbound port for configuration access.
|
||||
* AP-005: Minimal interface for loading typed startup configuration.
|
||||
*/
|
||||
public interface ConfigurationPort {
|
||||
|
||||
/**
|
||||
* Loads and returns the startup configuration.
|
||||
*
|
||||
* @return the loaded StartConfiguration
|
||||
*/
|
||||
StartConfiguration loadConfiguration();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Outbound ports for the application layer.
|
||||
* Defines interfaces for infrastructure access from the application layer.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Minimal no-op implementation of {@link RunBatchProcessingUseCase}.
|
||||
* <p>
|
||||
* AP-003 Implementation: Provides a controlled, non-functional startup path
|
||||
* without any business logic, PDF processing, or infrastructure access.
|
||||
* <p>
|
||||
* AP-005: Accepts {@link ConfigurationPort} to load typed startup configuration.
|
||||
*/
|
||||
public class NoOpRunBatchProcessingUseCase implements RunBatchProcessingUseCase {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(NoOpRunBatchProcessingUseCase.class);
|
||||
private final ConfigurationPort configurationPort;
|
||||
|
||||
/**
|
||||
* Creates the no-op use case with a configuration port.
|
||||
*
|
||||
* @param configurationPort the configuration port for loading startup configuration
|
||||
*/
|
||||
public NoOpRunBatchProcessingUseCase(ConfigurationPort configurationPort) {
|
||||
this.configurationPort = configurationPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute() {
|
||||
// AP-005: Load configuration through the port (technical loading only)
|
||||
StartConfiguration config = configurationPort.loadConfiguration();
|
||||
LOG.info("Configuration loaded successfully. Source: {}, Target: {}", config.sourceFolder(), config.targetFolder());
|
||||
|
||||
// AP-003: Intentional no-op - validates the technical call chain only
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Use case implementations that realize the inbound port contracts.
|
||||
* Currently contains minimal no-op implementations for AP-003 technical validation.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
1
pdf-umbenenner-application/src/test/java/.gitkeep
Normal file
1
pdf-umbenenner-application/src/test/java/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,686 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link StartConfigurationValidator}.
|
||||
* <p>
|
||||
* Tests cover valid configuration, missing mandatory fields, invalid values,
|
||||
* and path relationship validations.
|
||||
*/
|
||||
class StartConfigurationValidatorTest {
|
||||
|
||||
private final StartConfigurationValidator validator = new StartConfigurationValidator();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void validate_successWithValidConfiguration() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
tempDir.resolve("lock.lock"),
|
||||
tempDir.resolve("logs"),
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
assertDoesNotThrow(() -> validator.validate(config));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenSourceFolderIsNull() {
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
null,
|
||||
tempDir.resolve("target"),
|
||||
tempDir.resolve("db.sqlite"),
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
tempDir.resolve("prompt.txt"),
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("source.folder: must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenTargetFolderIsNull() {
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
tempDir.resolve("source"),
|
||||
null,
|
||||
tempDir.resolve("db.sqlite"),
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
tempDir.resolve("prompt.txt"),
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("target.folder: must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenSqliteFileIsNull() {
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
tempDir.resolve("source"),
|
||||
tempDir.resolve("target"),
|
||||
null,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
tempDir.resolve("prompt.txt"),
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("sqlite.file: must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenApiBaseUrlIsNull() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
null,
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("api.baseUrl: must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenApiModelIsNull() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
null,
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("api.model: must not be null or blank"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenPromptTemplateFileIsNull() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("prompt.template.file: must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenApiTimeoutSecondsIsZeroOrNegative() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
0,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("api.timeoutSeconds: must be > 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenMaxRetriesTransientIsNegative() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
-1,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("max.retries.transient: must be >= 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenMaxPagesIsZeroOrNegative() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
0,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("max.pages: must be > 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenMaxTextCharactersIsZeroOrNegative() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
-1,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("max.text.characters: must be > 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenSourceFolderDoesNotExist() throws Exception {
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
tempDir.resolve("nonexistent"),
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("source.folder: path does not exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenSourceFolderIsNotADirectory() throws Exception {
|
||||
Path sourceFile = Files.createFile(tempDir.resolve("sourcefile.txt"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFile,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("source.folder: path is not a directory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenTargetFolderDoesNotExist() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
tempDir.resolve("nonexistent"),
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("target.folder: path does not exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenTargetFolderIsNotADirectory() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFile = Files.createFile(tempDir.resolve("targetfile.txt"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFile,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("target.folder: path is not a directory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenSqliteFileParentDoesNotExist() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
tempDir.resolve("nonexistent/db.sqlite"),
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("sqlite.file: parent directory does not exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenApiBaseUrlIsNotAbsolute() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("/api/v1"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("api.baseUrl: must be an absolute URI"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenApiBaseUrlHasUnsupportedScheme() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("ftp://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("api.baseUrl: scheme must be http or https"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenPromptTemplateFileDoesNotExist() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
tempDir.resolve("nonexistent.txt"),
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("prompt.template.file: path does not exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenPromptTemplateFileIsNotARegularFile() throws Exception {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path dirForPrompt = Files.createDirectory(tempDir.resolve("promptdir"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
dirForPrompt,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("prompt.template.file: path is not a regular file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenSourceAndTargetAreSamePath() throws Exception {
|
||||
Path sameFolder = Files.createDirectory(tempDir.resolve("samefolder"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
sameFolder,
|
||||
sameFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
assertTrue(exception.getMessage().contains("source.folder and target.folder must not resolve to the same path"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validate_failsWhenMultipleErrorsOccur() {
|
||||
StartConfiguration config = new StartConfiguration(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
-1,
|
||||
0,
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
|
||||
InvalidStartConfigurationException exception = assertThrows(
|
||||
InvalidStartConfigurationException.class,
|
||||
() -> validator.validate(config)
|
||||
);
|
||||
String message = exception.getMessage();
|
||||
assertTrue(message.contains("source.folder: must not be null"));
|
||||
assertTrue(message.contains("target.folder: must not be null"));
|
||||
assertTrue(message.contains("sqlite.file: must not be null"));
|
||||
assertTrue(message.contains("api.baseUrl: must not be null"));
|
||||
assertTrue(message.contains("api.model: must not be null or blank"));
|
||||
assertTrue(message.contains("prompt.template.file: must not be null"));
|
||||
assertTrue(message.contains("api.timeoutSeconds: must be > 0"));
|
||||
assertTrue(message.contains("max.retries.transient: must be >= 0"));
|
||||
assertTrue(message.contains("max.pages: must be > 0"));
|
||||
assertTrue(message.contains("max.text.characters: must be > 0"));
|
||||
}
|
||||
}
|
||||
1
pdf-umbenenner-application/src/test/resources/.gitkeep
Normal file
1
pdf-umbenenner-application/src/test/resources/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
104
pdf-umbenenner-bootstrap/pom.xml
Normal file
104
pdf-umbenenner-bootstrap/pom.xml
Normal file
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>pdf-umbenenner-bootstrap</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Internal dependencies -->
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-domain</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-application</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-adapter-in-cli</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-adapter-out</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>de.gecheckt.pdf.umbenenner.bootstrap.PdfUmbenennerApplication</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,143 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
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.application.config.InvalidStartConfigurationException;
|
||||
import de.gecheckt.pdf.umbenenner.application.config.StartConfigurationValidator;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Manual bootstrap runner that constructs the object graph and drives the startup flow.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* AP-006: Validates configuration before processing begins, returns exit code 2 on invalid config.
|
||||
*/
|
||||
public class BootstrapRunner {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(BootstrapRunner.class);
|
||||
|
||||
private final ConfigurationPortFactory configPortFactory;
|
||||
private final ValidatorFactory validatorFactory;
|
||||
private final UseCaseFactory useCaseFactory;
|
||||
private final CommandFactory commandFactory;
|
||||
|
||||
/**
|
||||
* Functional interface for creating a ConfigurationPort.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ConfigurationPortFactory {
|
||||
ConfigurationPort create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for creating a StartConfigurationValidator.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ValidatorFactory {
|
||||
StartConfigurationValidator create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for creating a RunBatchProcessingUseCase.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface UseCaseFactory {
|
||||
RunBatchProcessingUseCase create(ConfigurationPort configPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for creating a SchedulerBatchCommand.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandFactory {
|
||||
SchedulerBatchCommand create(RunBatchProcessingUseCase useCase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the BootstrapRunner with default factories for production use.
|
||||
*/
|
||||
public BootstrapRunner() {
|
||||
this.configPortFactory = PropertiesConfigurationPortAdapter::new;
|
||||
this.validatorFactory = StartConfigurationValidator::new;
|
||||
this.useCaseFactory = NoOpRunBatchProcessingUseCase::new;
|
||||
this.commandFactory = SchedulerBatchCommand::new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the BootstrapRunner with custom factories for testing.
|
||||
*
|
||||
* @param configPortFactory factory for creating ConfigurationPort 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,
|
||||
ValidatorFactory validatorFactory,
|
||||
UseCaseFactory useCaseFactory,
|
||||
CommandFactory commandFactory) {
|
||||
this.configPortFactory = configPortFactory;
|
||||
this.validatorFactory = validatorFactory;
|
||||
this.useCaseFactory = useCaseFactory;
|
||||
this.commandFactory = commandFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the application startup sequence.
|
||||
* <p>
|
||||
* AP-003: Manually wires the object graph and invokes the CLI command.
|
||||
* AP-005: Wires ConfigurationPort adapter and passes it to the use case.
|
||||
* AP-006: Validates configuration before allowing processing to start.
|
||||
*
|
||||
* @return exit code: 0 for success, 1 for unexpected failure, 2 for invalid configuration
|
||||
*/
|
||||
public int run() {
|
||||
LOG.info("Bootstrap flow started.");
|
||||
try {
|
||||
// Step 1: Create the configuration port adapter (adapter-out layer)
|
||||
ConfigurationPort configPort = configPortFactory.create();
|
||||
|
||||
// Step 2: Load configuration
|
||||
var config = configPort.loadConfiguration();
|
||||
|
||||
// Step 3: Validate configuration (AP-006)
|
||||
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 5: Create the CLI command adapter with the use case
|
||||
SchedulerBatchCommand command = commandFactory.create(useCase);
|
||||
|
||||
// Step 6: Execute the command
|
||||
boolean success = command.run();
|
||||
|
||||
if (success) {
|
||||
LOG.info("No-op startup path completed successfully.");
|
||||
}
|
||||
|
||||
return success ? 0 : 1;
|
||||
} catch (InvalidStartConfigurationException e) {
|
||||
// Controlled failure for invalid configuration - log clearly without stack trace
|
||||
LOG.error("Configuration validation failed: {}", e.getMessage());
|
||||
return 2;
|
||||
} catch (IllegalStateException e) {
|
||||
// Configuration loading failed due to missing/invalid required properties
|
||||
// Treat as invalid configuration for controlled failure
|
||||
LOG.error("Configuration loading failed: {}", e.getMessage());
|
||||
return 2;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Bootstrap failure during startup.", e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Main entry point for the PDF Umbenenner application.
|
||||
* <p>
|
||||
* AP-003: Delegates to {@link BootstrapRunner} for manual object graph construction
|
||||
* and execution of the minimal no-op startup path.
|
||||
*/
|
||||
public class PdfUmbenennerApplication {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(PdfUmbenennerApplication.class);
|
||||
|
||||
/**
|
||||
* Application entry point.
|
||||
*
|
||||
* @param args command line arguments (currently unused)
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
LOG.info("Starting PDF Umbenenner application...");
|
||||
try {
|
||||
BootstrapRunner runner = new BootstrapRunner();
|
||||
int exitCode = runner.run();
|
||||
if (exitCode == 0) {
|
||||
LOG.info("PDF Umbenenner application completed successfully.");
|
||||
} else {
|
||||
LOG.error("PDF Umbenenner application terminated with error code {}.", exitCode);
|
||||
}
|
||||
System.exit(exitCode);
|
||||
} catch (Exception e) {
|
||||
LOG.fatal("Unexpected technical bootstrap failure in PDF Umbenenner application.", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Bootstrap module for application startup and technical wiring.
|
||||
* <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.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap;
|
||||
28
pdf-umbenenner-bootstrap/src/main/resources/log4j2.xml
Normal file
28
pdf-umbenenner-bootstrap/src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<!-- Console appender for stdout -->
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
|
||||
<!-- Rolling file appender for logs in ./logs/ directory -->
|
||||
<RollingFile name="File" fileName="logs/pdf-umbenenner.log"
|
||||
filePattern="logs/pdf-umbenenner-%d{yyyy-MM-dd}-%i.log.gz">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||
<SizeBasedTriggeringPolicy size="10 MB"/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="7"/>
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!-- Root logger at INFO level -->
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="File"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
1
pdf-umbenenner-bootstrap/src/test/java/.gitkeep
Normal file
1
pdf-umbenenner-bootstrap/src/test/java/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,201 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap;
|
||||
|
||||
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.RunBatchProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link BootstrapRunner}.
|
||||
* <p>
|
||||
* Tests cover the bootstrap orchestration behavior including success path,
|
||||
* invalid configuration handling, and unexpected failure handling.
|
||||
*/
|
||||
class BootstrapRunnerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void run_returnsZeroOnSuccess() throws Exception {
|
||||
// Create a mock configuration port that returns valid config
|
||||
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
|
||||
|
||||
// Create mock factories that return working components
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> mockConfigPort,
|
||||
StartConfigurationValidator::new,
|
||||
port -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
int exitCode = runner.run();
|
||||
|
||||
assertEquals(0, exitCode, "Success path should return exit code 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_returnsTwoOnInvalidConfiguration() throws Exception {
|
||||
// Create a mock configuration port that returns valid config
|
||||
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
|
||||
|
||||
// Create a custom validator that always throws InvalidStartConfigurationException
|
||||
StartConfigurationValidator failingValidator = new StartConfigurationValidator() {
|
||||
@Override
|
||||
public void validate(StartConfiguration config) {
|
||||
throw new InvalidStartConfigurationException("Simulated validation failure");
|
||||
}
|
||||
};
|
||||
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> mockConfigPort,
|
||||
() -> failingValidator,
|
||||
port -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
int exitCode = runner.run();
|
||||
|
||||
assertEquals(2, exitCode, "Invalid configuration should return exit code 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_returnsTwoOnConfigurationLoadingFailure() {
|
||||
// Create a mock configuration port that throws IllegalStateException
|
||||
ConfigurationPort failingConfigPort = () -> {
|
||||
throw new IllegalStateException("Simulated configuration loading failure");
|
||||
};
|
||||
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> failingConfigPort,
|
||||
StartConfigurationValidator::new,
|
||||
port -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
int exitCode = runner.run();
|
||||
|
||||
assertEquals(2, exitCode, "Configuration loading failure should return exit code 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_returnsOneOnUnexpectedException() {
|
||||
// Create a mock configuration port that throws a generic exception
|
||||
ConfigurationPort throwingConfigPort = () -> {
|
||||
throw new RuntimeException("Simulated unexpected failure");
|
||||
};
|
||||
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> throwingConfigPort,
|
||||
StartConfigurationValidator::new,
|
||||
port -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
int exitCode = runner.run();
|
||||
|
||||
assertEquals(1, exitCode, "Unexpected exception should return exit code 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_returnsOneWhenCommandReturnsFalse() 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;
|
||||
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> mockConfigPort,
|
||||
StartConfigurationValidator::new,
|
||||
port -> failingUseCase,
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
int exitCode = runner.run();
|
||||
|
||||
assertEquals(1, exitCode, "Command returning false should return exit code 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_withDefaultConstructor_usesRealImplementations() {
|
||||
// This test verifies that the default constructor creates a functional runner
|
||||
// We can't fully test it without actual config files, but we can verify instantiation
|
||||
BootstrapRunner runner = new BootstrapRunner();
|
||||
|
||||
assertNotNull(runner, "Default constructor should create a valid BootstrapRunner");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock ConfigurationPort that returns a valid StartConfiguration.
|
||||
*/
|
||||
private static class MockConfigurationPort implements ConfigurationPort {
|
||||
private final Path tempDir;
|
||||
private final boolean shouldSucceed;
|
||||
|
||||
MockConfigurationPort(Path tempDir, boolean shouldSucceed) {
|
||||
this.tempDir = tempDir;
|
||||
this.shouldSucceed = shouldSucceed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StartConfiguration loadConfiguration() {
|
||||
if (!shouldSucceed) {
|
||||
throw new IllegalStateException("Mock configuration loading failed");
|
||||
}
|
||||
|
||||
try {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
return new StartConfiguration(
|
||||
sourceFolder,
|
||||
targetFolder,
|
||||
sqliteFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptTemplateFile,
|
||||
tempDir.resolve("lock.lock"),
|
||||
tempDir.resolve("logs"),
|
||||
"INFO",
|
||||
"test-api-key"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create mock configuration", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock RunBatchProcessingUseCase that returns a configurable result.
|
||||
*/
|
||||
private static class MockRunBatchProcessingUseCase implements RunBatchProcessingUseCase {
|
||||
private final boolean shouldSucceed;
|
||||
|
||||
MockRunBatchProcessingUseCase(boolean shouldSucceed) {
|
||||
this.shouldSucceed = shouldSucceed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute() {
|
||||
return shouldSucceed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package de.gecheckt.pdf.umbenenner.bootstrap;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* AP-008: Executable JAR smoke tests for M1 target state verification.
|
||||
* <p>
|
||||
* These tests verify that the shaded executable JAR can be run via {@code java -jar}
|
||||
* and behaves correctly for both success and invalid configuration scenarios.
|
||||
* <p>
|
||||
* Tests are executed by the maven-failsafe-plugin after the package phase.
|
||||
* The *IT suffix ensures failsafe picks them up as integration tests.
|
||||
*/
|
||||
class ExecutableJarSmokeTestIT {
|
||||
|
||||
private static final String JAVA_EXECUTABLE = "java";
|
||||
private static final long PROCESS_TIMEOUT_MS = 30000;
|
||||
|
||||
/**
|
||||
* Success-path smoke test: verifies the JAR starts successfully with valid configuration.
|
||||
* <p>
|
||||
* Verifies:
|
||||
* - The shaded JAR file exists
|
||||
* - java -jar executes successfully
|
||||
* - Exit code is 0
|
||||
* - Configuration is loaded successfully
|
||||
* - Logging produces output
|
||||
* - Process ends in a controlled way
|
||||
* - No functional PDF processing occurs
|
||||
*/
|
||||
@Test
|
||||
void jar_startsSuccessfullyWithValidConfiguration(@TempDir Path workDir) throws Exception {
|
||||
// Create runtime fixtures in the temporary working directory
|
||||
Path configDir = Files.createDirectory(workDir.resolve("config"));
|
||||
Path sourceDir = Files.createDirectory(workDir.resolve("source"));
|
||||
Path targetDir = Files.createDirectory(workDir.resolve("target"));
|
||||
Path logsDir = Files.createDirectory(workDir.resolve("logs"));
|
||||
Path dbParent = Files.createDirectory(workDir.resolve("data"));
|
||||
Path promptDir = Files.createDirectory(workDir.resolve("config/prompts"));
|
||||
|
||||
Path sqliteFile = Files.createFile(dbParent.resolve("pdf-umbenenner.db"));
|
||||
Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt"));
|
||||
Files.writeString(promptTemplateFile, "Test prompt template for smoke test.");
|
||||
|
||||
// Write valid application.properties
|
||||
Path configFile = configDir.resolve("application.properties");
|
||||
String validConfig = """
|
||||
source.folder=%s
|
||||
target.folder=%s
|
||||
sqlite.file=%s
|
||||
api.baseUrl=http://localhost:8080/api
|
||||
api.model=gpt-4o-mini
|
||||
api.timeoutSeconds=30
|
||||
max.retries.transient=3
|
||||
max.pages=10
|
||||
max.text.characters=5000
|
||||
prompt.template.file=%s
|
||||
runtime.lock.file=%s/lock.pid
|
||||
log.directory=%s
|
||||
log.level=INFO
|
||||
api.key=test-api-key-for-smoke-test
|
||||
""".formatted(
|
||||
sourceDir.toAbsolutePath(),
|
||||
targetDir.toAbsolutePath(),
|
||||
sqliteFile.toAbsolutePath(),
|
||||
promptTemplateFile.toAbsolutePath(),
|
||||
workDir.toAbsolutePath(),
|
||||
logsDir.toAbsolutePath()
|
||||
);
|
||||
Files.writeString(configFile, validConfig);
|
||||
|
||||
// Find the shaded JAR - look in target directory relative to project root
|
||||
Path projectRoot = Paths.get(System.getProperty("user.dir"));
|
||||
Path bootstrapTarget = projectRoot.resolve("pdf-umbenenner-bootstrap/target");
|
||||
|
||||
if (!Files.exists(bootstrapTarget)) {
|
||||
// Fallback: try relative from current execution context
|
||||
bootstrapTarget = Paths.get("target");
|
||||
}
|
||||
|
||||
assertTrue(Files.exists(bootstrapTarget), "Bootstrap target directory must exist: " + bootstrapTarget);
|
||||
|
||||
File[] jars = bootstrapTarget.toFile().listFiles((dir, name) ->
|
||||
name.endsWith(".jar") && !name.contains("original") && !name.contains("tests")
|
||||
);
|
||||
|
||||
assertNotNull(jars, "JAR files should exist in target directory");
|
||||
assertTrue(jars.length > 0, "At least one JAR should exist in target directory");
|
||||
|
||||
Path shadedJar = Paths.get(jars[0].getAbsolutePath());
|
||||
// Prefer the shaded JAR if multiple exist
|
||||
for (File jar : jars) {
|
||||
if (jar.getName().contains("shaded") || jar.getName().equals("pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar")) {
|
||||
shadedJar = jar.toPath().toAbsolutePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(Files.exists(shadedJar), "Shaded JAR file must exist: " + shadedJar);
|
||||
|
||||
// Build the java -jar command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(JAVA_EXECUTABLE);
|
||||
command.add("-jar");
|
||||
command.add(shadedJar.toString());
|
||||
|
||||
// Run the process in the work directory where config/application.properties will be found
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.directory(workDir.toFile());
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
System.out.println("[SMOKE-TEST] JAR path: " + shadedJar.toAbsolutePath());
|
||||
System.out.println("[SMOKE-TEST] Working directory: " + workDir.toAbsolutePath());
|
||||
System.out.println("[SMOKE-TEST] Command: " + String.join(" ", command));
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
// Wait for process completion with timeout
|
||||
boolean completed = process.waitFor(PROCESS_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
assertTrue(completed, "Process should complete within " + PROCESS_TIMEOUT_MS + "ms timeout");
|
||||
|
||||
int exitCode = process.exitValue();
|
||||
|
||||
// Capture all output for diagnostic purposes
|
||||
byte[] outputBytes = process.getInputStream().readAllBytes();
|
||||
String outputText = new String(outputBytes);
|
||||
|
||||
System.out.println("[SMOKE-TEST] Exit code: " + exitCode);
|
||||
System.out.println("[SMOKE-TEST] Subprocess stdout/stderr:\n" + outputText);
|
||||
|
||||
assertEquals(0, exitCode, "Successful startup should return exit code 0. Output was: " + outputText);
|
||||
|
||||
// Verify logging output was produced (check console output)
|
||||
assertTrue(
|
||||
outputText.contains("Starting") ||
|
||||
outputText.contains("Bootstrap") ||
|
||||
outputText.contains("completed") ||
|
||||
outputText.contains("successfully"),
|
||||
"Output should contain startup/shutdown indicators. Got: " + outputText
|
||||
);
|
||||
|
||||
// Verify no unexpected artifacts were created beyond our fixtures
|
||||
// Count top-level entries - should only be our created directories
|
||||
long fileCount = Files.list(workDir).count();
|
||||
assertTrue(fileCount <= 7, "No extra files should be created beyond fixtures. Found: " + fileCount + " entries");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalid-configuration smoke test: verifies controlled failure with exit code 2.
|
||||
* <p>
|
||||
* Verifies:
|
||||
* - java -jar runs against invalid configuration
|
||||
* - Exit code is 2
|
||||
* - Startup fails before any processing
|
||||
* - Failure is controlled (not a crash/hang)
|
||||
* - Error output indicates configuration validation failure
|
||||
*/
|
||||
@Test
|
||||
void jar_failsControlledWithInvalidConfiguration(@TempDir Path workDir) throws Exception {
|
||||
// Create runtime fixtures with INVALID configuration
|
||||
Path configDir = Files.createDirectory(workDir.resolve("config"));
|
||||
Path sourceDir = Files.createDirectory(workDir.resolve("source"));
|
||||
// Intentionally missing target folder - this should cause validation failure
|
||||
|
||||
Path dbParent = Files.createDirectory(workDir.resolve("data"));
|
||||
Path promptDir = Files.createDirectory(workDir.resolve("config/prompts"));
|
||||
|
||||
Path sqliteFile = Files.createFile(dbParent.resolve("pdf-umbenenner.db"));
|
||||
Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt"));
|
||||
Files.writeString(promptTemplateFile, "Test prompt template.");
|
||||
|
||||
// Write INVALID application.properties (missing required target.folder)
|
||||
Path configFile = configDir.resolve("application.properties");
|
||||
String invalidConfig = """
|
||||
source.folder=%s
|
||||
# target.folder is intentionally missing - should cause validation failure
|
||||
sqlite.file=%s
|
||||
api.baseUrl=http://localhost:8080/api
|
||||
api.model=gpt-4o-mini
|
||||
api.timeoutSeconds=30
|
||||
max.retries.transient=3
|
||||
max.pages=10
|
||||
max.text.characters=5000
|
||||
prompt.template.file=%s
|
||||
log.directory=%s/logs
|
||||
log.level=INFO
|
||||
api.key=test-api-key
|
||||
""".formatted(
|
||||
sourceDir.toAbsolutePath(),
|
||||
sqliteFile.toAbsolutePath(),
|
||||
promptTemplateFile.toAbsolutePath(),
|
||||
workDir.toAbsolutePath()
|
||||
);
|
||||
Files.writeString(configFile, invalidConfig);
|
||||
|
||||
// Find the shaded JAR
|
||||
Path projectRoot = Paths.get(System.getProperty("user.dir"));
|
||||
Path bootstrapTarget = projectRoot.resolve("pdf-umbenenner-bootstrap/target");
|
||||
|
||||
if (!Files.exists(bootstrapTarget)) {
|
||||
bootstrapTarget = Paths.get("target");
|
||||
}
|
||||
|
||||
assertTrue(Files.exists(bootstrapTarget), "Bootstrap target directory must exist: " + bootstrapTarget);
|
||||
|
||||
File[] jars = bootstrapTarget.toFile().listFiles((dir, name) ->
|
||||
name.endsWith(".jar") && !name.contains("original") && !name.contains("tests")
|
||||
);
|
||||
|
||||
assertNotNull(jars, "JAR files should exist in target directory");
|
||||
assertTrue(jars.length > 0, "At least one JAR should exist");
|
||||
|
||||
Path shadedJar = Paths.get(jars[0].getAbsolutePath());
|
||||
for (File jar : jars) {
|
||||
if (jar.getName().contains("shaded") || jar.getName().equals("pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar")) {
|
||||
shadedJar = jar.toPath().toAbsolutePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(Files.exists(shadedJar), "Shaded JAR file must exist: " + shadedJar);
|
||||
|
||||
// Build the java -jar command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(JAVA_EXECUTABLE);
|
||||
command.add("-jar");
|
||||
command.add(shadedJar.toString());
|
||||
|
||||
// Run the process
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.directory(workDir.toFile());
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
System.out.println("[SMOKE-TEST-INVALID] JAR path: " + shadedJar.toAbsolutePath());
|
||||
System.out.println("[SMOKE-TEST-INVALID] Working directory: " + workDir.toAbsolutePath());
|
||||
System.out.println("[SMOKE-TEST-INVALID] Command: " + String.join(" ", command));
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
// Wait for process completion with timeout
|
||||
boolean completed = process.waitFor(PROCESS_TIMEOUT_MS, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
assertTrue(completed, "Process should complete within timeout even on failure");
|
||||
|
||||
int exitCode = process.exitValue();
|
||||
|
||||
// Capture all output for diagnostic purposes
|
||||
byte[] outputBytes = process.getInputStream().readAllBytes();
|
||||
String outputText = new String(outputBytes);
|
||||
|
||||
System.out.println("[SMOKE-TEST-INVALID] Exit code: " + exitCode);
|
||||
System.out.println("[SMOKE-TEST-INVALID] Subprocess stdout/stderr:\n" + outputText);
|
||||
|
||||
assertEquals(2, exitCode, "Invalid configuration should return exit code 2. Output was: " + outputText);
|
||||
|
||||
// Verify error output indicates configuration failure
|
||||
assertTrue(
|
||||
outputText.toLowerCase().contains("config") ||
|
||||
outputText.toLowerCase().contains("validation") ||
|
||||
outputText.toLowerCase().contains("invalid") ||
|
||||
outputText.toLowerCase().contains("error") ||
|
||||
outputText.toLowerCase().contains("failed"),
|
||||
"Output should indicate configuration/validation error. Got: " + outputText
|
||||
);
|
||||
}
|
||||
}
|
||||
1
pdf-umbenenner-bootstrap/src/test/resources/.gitkeep
Normal file
1
pdf-umbenenner-bootstrap/src/test/resources/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
20
pdf-umbenenner-domain/pom.xml
Normal file
20
pdf-umbenenner-domain/pom.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>pdf-umbenenner-domain</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Domain layer containing business entities and value objects.
|
||||
* <p>
|
||||
* This package is infrastructure-agnostic and contains no dependencies on external systems.
|
||||
* AP-003: Currently empty as no domain logic has been implemented yet.
|
||||
*/
|
||||
package de.gecheckt.pdf.umbenenner.domain;
|
||||
1
pdf-umbenenner-domain/src/test/java/.gitkeep
Normal file
1
pdf-umbenenner-domain/src/test/java/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
1
pdf-umbenenner-domain/src/test/resources/.gitkeep
Normal file
1
pdf-umbenenner-domain/src/test/resources/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
145
pom.xml
Normal file
145
pom.xml
Normal file
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>de.gecheckt</groupId>
|
||||
<artifactId>pdf-umbenenner-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>pdf-umbenenner-domain</module>
|
||||
<module>pdf-umbenenner-application</module>
|
||||
<module>pdf-umbenenner-adapter-in-cli</module>
|
||||
<module>pdf-umbenenner-adapter-out</module>
|
||||
<module>pdf-umbenenner-bootstrap</module>
|
||||
</modules>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
|
||||
<!-- Library versions -->
|
||||
<log4j.version>2.23.1</log4j.version>
|
||||
<pdfbox.version>3.0.2</pdfbox.version>
|
||||
<sqlite-jdbc.version>3.45.1.0</sqlite-jdbc.version>
|
||||
<json.version>20240303</json.version>
|
||||
<junit.version>5.10.2</junit.version>
|
||||
<mockito.version>5.11.0</mockito.version>
|
||||
|
||||
<!-- Plugin versions -->
|
||||
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
|
||||
<maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
|
||||
<maven-enforcer-plugin.version>3.4.1</maven-enforcer-plugin.version>
|
||||
<maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j2-impl</artifactId>
|
||||
<version>${log4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- PDF processing -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>${pdfbox.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Database -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>${sqlite-jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>${json.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<release>21</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>${maven-enforcer-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>enforce-java</id>
|
||||
<goals>
|
||||
<goal>enforce</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<rules>
|
||||
<requireMavenVersion>
|
||||
<version>[3.9,)</version>
|
||||
</requireMavenVersion>
|
||||
<requireJavaVersion>
|
||||
<version>[21,)</version>
|
||||
</requireJavaVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>${maven-shade-plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
Reference in New Issue
Block a user