M1 Vollständiger Grundstand mit Build, Konfiguration, Tests und Smoke-Tests

This commit is contained in:
2026-03-31 14:04:47 +02:00
commit ea83f8fa8c
52 changed files with 2819 additions and 0 deletions
@@ -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;