diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/ConfigurationLoadingException.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/ConfigurationLoadingException.java new file mode 100644 index 0000000..84783a1 --- /dev/null +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/ConfigurationLoadingException.java @@ -0,0 +1,38 @@ +package de.gecheckt.pdf.umbenenner.adapter.out.configuration; + +/** + * Exception thrown when configuration loading or parsing fails. + *
+ * This exception covers all failures related to loading, reading, or parsing the configuration, + * including: + *
+ * This is a controlled failure mode that prevents processing from starting. + */ +public class ConfigurationLoadingException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Creates the exception with an error message. + * + * @param message the error message describing what failed during configuration loading + */ + public ConfigurationLoadingException(String message) { + super(message); + } + + /** + * Creates the exception with an error message and a cause. + * + * @param message the error message describing what failed during configuration loading + * @param cause the underlying exception that caused the configuration failure + */ + public ConfigurationLoadingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java index bc8e3b8..7d1faa6 100644 --- a/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java +++ b/pdf-umbenenner-adapter-out/src/main/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapter.java @@ -93,7 +93,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort { String escapedContent = escapeBackslashes(content); props.load(new StringReader(escapedContent)); } catch (IOException e) { - throw new RuntimeException("Failed to load configuration from " + configFilePath, e); + throw new ConfigurationLoadingException("Failed to load configuration from " + configFilePath, e); } return props; } @@ -137,7 +137,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort { private String getRequiredProperty(Properties props, String key) { String value = props.getProperty(key); if (value == null || value.isBlank()) { - throw new IllegalStateException("Required property missing: " + key); + throw new ConfigurationLoadingException("Required property missing: " + key); } return normalizePath(value.trim()); } @@ -161,7 +161,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort { try { return Integer.parseInt(value.trim()); } catch (NumberFormatException e) { - throw new IllegalStateException("Invalid integer value for property: " + value, e); + throw new ConfigurationLoadingException("Invalid integer value for property: " + value, e); } } @@ -169,7 +169,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort { try { return new URI(value.trim()); } catch (URISyntaxException e) { - throw new IllegalStateException("Invalid URI value for property: " + value, e); + throw new ConfigurationLoadingException("Invalid URI value for property: " + value, e); } } } \ No newline at end of file diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java index 52ffcff..7ad2be6 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/configuration/PropertiesConfigurationPortAdapterTest.java @@ -127,13 +127,13 @@ class PropertiesConfigurationPortAdapterTest { } @Test - void loadConfiguration_throwsIllegalStateExceptionWhenRequiredPropertyMissing() throws Exception { + void loadConfiguration_throwsConfigurationLoadingExceptionWhenRequiredPropertyMissing() throws Exception { Path configFile = createConfigFile("missing-required.properties"); PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); - IllegalStateException exception = assertThrows( - IllegalStateException.class, + ConfigurationLoadingException exception = assertThrows( + ConfigurationLoadingException.class, () -> adapter.loadConfiguration() ); @@ -142,13 +142,13 @@ class PropertiesConfigurationPortAdapterTest { } @Test - void loadConfiguration_throwsRuntimeExceptionWhenConfigFileNotFound() { + void loadConfiguration_throwsConfigurationLoadingExceptionWhenConfigFileNotFound() { Path nonExistentFile = tempDir.resolve("nonexistent.properties"); PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile); - RuntimeException exception = assertThrows( - RuntimeException.class, + ConfigurationLoadingException exception = assertThrows( + ConfigurationLoadingException.class, () -> adapter.loadConfiguration() ); @@ -206,7 +206,7 @@ class PropertiesConfigurationPortAdapterTest { } @Test - void loadConfiguration_throwsIllegalStateExceptionForInvalidIntegerValue() throws Exception { + void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidIntegerValue() throws Exception { Path configFile = createInlineConfig( "source.folder=/tmp/source\n" + "target.folder=/tmp/target\n" + @@ -223,8 +223,8 @@ class PropertiesConfigurationPortAdapterTest { PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile); - IllegalStateException exception = assertThrows( - IllegalStateException.class, + ConfigurationLoadingException exception = assertThrows( + ConfigurationLoadingException.class, () -> adapter.loadConfiguration() ); @@ -279,6 +279,55 @@ class PropertiesConfigurationPortAdapterTest { assertEquals("INFO", config.logLevel(), "log.level should default to INFO"); } + @Test + void allConfigurationFailuresAreClassifiedAsConfigurationLoadingException() throws Exception { + // Verify that file I/O failure uses ConfigurationLoadingException + Path nonExistentFile = tempDir.resolve("nonexistent.properties"); + PropertiesConfigurationPortAdapter adapter1 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile); + assertThrows(ConfigurationLoadingException.class, () -> adapter1.loadConfiguration(), + "File I/O failure should throw ConfigurationLoadingException"); + + // Verify that missing required property uses ConfigurationLoadingException + Path missingPropFile = createConfigFile("missing-required.properties"); + PropertiesConfigurationPortAdapter adapter2 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, missingPropFile); + assertThrows(ConfigurationLoadingException.class, () -> adapter2.loadConfiguration(), + "Missing required property should throw ConfigurationLoadingException"); + + // Verify that invalid integer value uses ConfigurationLoadingException + Path invalidIntFile = 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=invalid\n" + + "max.retries.transient=2\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + ); + PropertiesConfigurationPortAdapter adapter3 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, invalidIntFile); + assertThrows(ConfigurationLoadingException.class, () -> adapter3.loadConfiguration(), + "Invalid integer value should throw ConfigurationLoadingException"); + + // Verify that invalid URI value uses ConfigurationLoadingException + Path invalidUriFile = createInlineConfig( + "source.folder=/tmp/source\n" + + "target.folder=/tmp/target\n" + + "sqlite.file=/tmp/db.sqlite\n" + + "api.baseUrl=not a valid uri\n" + + "api.model=gpt-4\n" + + "api.timeoutSeconds=30\n" + + "max.retries.transient=2\n" + + "max.pages=100\n" + + "max.text.characters=50000\n" + + "prompt.template.file=/tmp/prompt.txt\n" + ); + PropertiesConfigurationPortAdapter adapter4 = new PropertiesConfigurationPortAdapter(emptyEnvLookup, invalidUriFile); + assertThrows(ConfigurationLoadingException.class, () -> adapter4.loadConfiguration(), + "Invalid URI value should throw ConfigurationLoadingException"); + } + private Path createConfigFile(String resourceName) throws Exception { Path sourceResource = Path.of("src/test/resources", resourceName); Path targetConfigFile = tempDir.resolve("application.properties"); diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index 1e6b564..a5c62b3 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -11,6 +11,7 @@ import org.apache.logging.log4j.Logger; import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand; import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException; import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator; +import de.gecheckt.pdf.umbenenner.adapter.out.configuration.ConfigurationLoadingException; import de.gecheckt.pdf.umbenenner.adapter.out.configuration.PropertiesConfigurationPortAdapter; import de.gecheckt.pdf.umbenenner.adapter.out.fingerprint.Sha256FingerprintAdapter; import de.gecheckt.pdf.umbenenner.adapter.out.lock.FilesystemRunLockPortAdapter; @@ -249,12 +250,12 @@ public class BootstrapRunner { BatchRunOutcome outcome = command.run(runContext); runContext.setEndInstant(Instant.now()); return mapOutcomeToExitCode(outcome, runContext); + } catch (ConfigurationLoadingException e) { + LOG.error("Configuration loading failed: {}", e.getMessage()); + return 1; } catch (InvalidStartConfigurationException e) { LOG.error("Configuration validation failed: {}", e.getMessage()); return 1; - } catch (IllegalStateException e) { - LOG.error("Configuration loading failed: {}", e.getMessage()); - return 1; } catch (DocumentPersistenceException e) { LOG.error("Persistence operation failed: {}", e.getMessage(), e); return 1; diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java index 68fbce3..17694ea 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java @@ -3,6 +3,7 @@ package de.gecheckt.pdf.umbenenner.bootstrap; import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand; import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException; import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator; +import de.gecheckt.pdf.umbenenner.adapter.out.configuration.ConfigurationLoadingException; import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase; @@ -81,7 +82,7 @@ class BootstrapRunnerTest { @Test void run_returnsOneOnConfigurationLoadingFailure() { ConfigurationPort failingConfigPort = () -> { - throw new IllegalStateException("Simulated configuration loading failure"); + throw new ConfigurationLoadingException("Simulated configuration loading failure"); }; BootstrapRunner runner = new BootstrapRunner(