M1 Vollständiger Grundstand mit Build, Konfiguration, Tests und Smoke-Tests
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
# Keep directory
|
||||
+165
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -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;
|
||||
+8
@@ -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;
|
||||
Reference in New Issue
Block a user