Nachbearbeitung: Konfigurationsgrenze architekturtreu in Richtung
Bootstrap verschoben
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
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.
|
||||
* <p>
|
||||
* Supports injected source folder validation for testability
|
||||
* (allows mocking of platform-dependent filesystem checks).
|
||||
*/
|
||||
public class StartConfigurationValidator {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StartConfigurationValidator.class);
|
||||
|
||||
/**
|
||||
* Abstraction for source folder existence, type, and readability checks.
|
||||
* <p>
|
||||
* Separates filesystem operations from validation logic to enable
|
||||
* platform-independent unit testing (mocking) of readability edge cases.
|
||||
* <p>
|
||||
* Implementation note: The default implementation uses {@code java.nio.file.Files}
|
||||
* static methods directly; tests can substitute alternative implementations.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SourceFolderChecker {
|
||||
/**
|
||||
* Checks source folder and returns validation error message, or null if valid.
|
||||
* <p>
|
||||
* Checks (in order):
|
||||
* 1. Folder exists
|
||||
* 2. Is a directory
|
||||
* 3. Is readable
|
||||
*
|
||||
* @param path the source folder path
|
||||
* @return error message string, or null if all checks pass
|
||||
*/
|
||||
String checkSourceFolder(Path path);
|
||||
}
|
||||
|
||||
private final SourceFolderChecker sourceFolderChecker;
|
||||
|
||||
/**
|
||||
* Creates a validator with the default source folder checker (NIO-based).
|
||||
*/
|
||||
public StartConfigurationValidator() {
|
||||
this(new DefaultSourceFolderChecker());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a validator with a custom source folder checker (primarily for testing).
|
||||
*
|
||||
* @param sourceFolderChecker the checker to use (must not be null)
|
||||
*/
|
||||
public StartConfigurationValidator(SourceFolderChecker sourceFolderChecker) {
|
||||
this.sourceFolderChecker = sourceFolderChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
String checkError = sourceFolderChecker.checkSourceFolder(sourceFolder);
|
||||
if (checkError != null) {
|
||||
errors.add(checkError);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default NIO-based implementation of {@link SourceFolderChecker}.
|
||||
* <p>
|
||||
* Uses {@code java.nio.file.Files} static methods to check existence, type, and readability.
|
||||
* <p>
|
||||
* This separation allows unit tests to inject alternative implementations
|
||||
* that control the outcome of readability checks without relying on actual filesystem
|
||||
* permissions (which are platform-dependent).
|
||||
*/
|
||||
private static class DefaultSourceFolderChecker implements SourceFolderChecker {
|
||||
@Override
|
||||
public String checkSourceFolder(Path path) {
|
||||
if (!Files.exists(path)) {
|
||||
return "- source.folder: path does not exist: " + path;
|
||||
}
|
||||
if (!Files.isDirectory(path)) {
|
||||
return "- source.folder: path is not a directory: " + path;
|
||||
}
|
||||
if (!Files.isReadable(path)) {
|
||||
return "- source.folder: directory is not readable: " + path;
|
||||
}
|
||||
return null; // All checks passed
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user