Nachbearbeitung: Konfigurationslade- und Parsefehler einheitlich
klassifiziert
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.adapter.out.configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when configuration loading or parsing fails.
|
||||||
|
* <p>
|
||||||
|
* This exception covers all failures related to loading, reading, or parsing the configuration,
|
||||||
|
* including:
|
||||||
|
* <ul>
|
||||||
|
* <li>I/O failures when reading the configuration file</li>
|
||||||
|
* <li>Missing required properties</li>
|
||||||
|
* <li>Invalid property values (e.g., unparseable integers, invalid URIs)</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,7 +93,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort {
|
|||||||
String escapedContent = escapeBackslashes(content);
|
String escapedContent = escapeBackslashes(content);
|
||||||
props.load(new StringReader(escapedContent));
|
props.load(new StringReader(escapedContent));
|
||||||
} catch (IOException e) {
|
} 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;
|
return props;
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort {
|
|||||||
private String getRequiredProperty(Properties props, String key) {
|
private String getRequiredProperty(Properties props, String key) {
|
||||||
String value = props.getProperty(key);
|
String value = props.getProperty(key);
|
||||||
if (value == null || value.isBlank()) {
|
if (value == null || value.isBlank()) {
|
||||||
throw new IllegalStateException("Required property missing: " + key);
|
throw new ConfigurationLoadingException("Required property missing: " + key);
|
||||||
}
|
}
|
||||||
return normalizePath(value.trim());
|
return normalizePath(value.trim());
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ public class PropertiesConfigurationPortAdapter implements ConfigurationPort {
|
|||||||
try {
|
try {
|
||||||
return Integer.parseInt(value.trim());
|
return Integer.parseInt(value.trim());
|
||||||
} catch (NumberFormatException e) {
|
} 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 {
|
try {
|
||||||
return new URI(value.trim());
|
return new URI(value.trim());
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
throw new IllegalStateException("Invalid URI value for property: " + value, e);
|
throw new ConfigurationLoadingException("Invalid URI value for property: " + value, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,13 +127,13 @@ class PropertiesConfigurationPortAdapterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadConfiguration_throwsIllegalStateExceptionWhenRequiredPropertyMissing() throws Exception {
|
void loadConfiguration_throwsConfigurationLoadingExceptionWhenRequiredPropertyMissing() throws Exception {
|
||||||
Path configFile = createConfigFile("missing-required.properties");
|
Path configFile = createConfigFile("missing-required.properties");
|
||||||
|
|
||||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||||
|
|
||||||
IllegalStateException exception = assertThrows(
|
ConfigurationLoadingException exception = assertThrows(
|
||||||
IllegalStateException.class,
|
ConfigurationLoadingException.class,
|
||||||
() -> adapter.loadConfiguration()
|
() -> adapter.loadConfiguration()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,13 +142,13 @@ class PropertiesConfigurationPortAdapterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadConfiguration_throwsRuntimeExceptionWhenConfigFileNotFound() {
|
void loadConfiguration_throwsConfigurationLoadingExceptionWhenConfigFileNotFound() {
|
||||||
Path nonExistentFile = tempDir.resolve("nonexistent.properties");
|
Path nonExistentFile = tempDir.resolve("nonexistent.properties");
|
||||||
|
|
||||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile);
|
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, nonExistentFile);
|
||||||
|
|
||||||
RuntimeException exception = assertThrows(
|
ConfigurationLoadingException exception = assertThrows(
|
||||||
RuntimeException.class,
|
ConfigurationLoadingException.class,
|
||||||
() -> adapter.loadConfiguration()
|
() -> adapter.loadConfiguration()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ class PropertiesConfigurationPortAdapterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadConfiguration_throwsIllegalStateExceptionForInvalidIntegerValue() throws Exception {
|
void loadConfiguration_throwsConfigurationLoadingExceptionForInvalidIntegerValue() throws Exception {
|
||||||
Path configFile = createInlineConfig(
|
Path configFile = createInlineConfig(
|
||||||
"source.folder=/tmp/source\n" +
|
"source.folder=/tmp/source\n" +
|
||||||
"target.folder=/tmp/target\n" +
|
"target.folder=/tmp/target\n" +
|
||||||
@@ -223,8 +223,8 @@ class PropertiesConfigurationPortAdapterTest {
|
|||||||
|
|
||||||
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
PropertiesConfigurationPortAdapter adapter = new PropertiesConfigurationPortAdapter(emptyEnvLookup, configFile);
|
||||||
|
|
||||||
IllegalStateException exception = assertThrows(
|
ConfigurationLoadingException exception = assertThrows(
|
||||||
IllegalStateException.class,
|
ConfigurationLoadingException.class,
|
||||||
() -> adapter.loadConfiguration()
|
() -> adapter.loadConfiguration()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -279,6 +279,55 @@ class PropertiesConfigurationPortAdapterTest {
|
|||||||
assertEquals("INFO", config.logLevel(), "log.level should default to INFO");
|
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 {
|
private Path createConfigFile(String resourceName) throws Exception {
|
||||||
Path sourceResource = Path.of("src/test/resources", resourceName);
|
Path sourceResource = Path.of("src/test/resources", resourceName);
|
||||||
Path targetConfigFile = tempDir.resolve("application.properties");
|
Path targetConfigFile = tempDir.resolve("application.properties");
|
||||||
|
|||||||
@@ -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.in.cli.SchedulerBatchCommand;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException;
|
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.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.configuration.PropertiesConfigurationPortAdapter;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.out.fingerprint.Sha256FingerprintAdapter;
|
import de.gecheckt.pdf.umbenenner.adapter.out.fingerprint.Sha256FingerprintAdapter;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.out.lock.FilesystemRunLockPortAdapter;
|
import de.gecheckt.pdf.umbenenner.adapter.out.lock.FilesystemRunLockPortAdapter;
|
||||||
@@ -249,12 +250,12 @@ public class BootstrapRunner {
|
|||||||
BatchRunOutcome outcome = command.run(runContext);
|
BatchRunOutcome outcome = command.run(runContext);
|
||||||
runContext.setEndInstant(Instant.now());
|
runContext.setEndInstant(Instant.now());
|
||||||
return mapOutcomeToExitCode(outcome, runContext);
|
return mapOutcomeToExitCode(outcome, runContext);
|
||||||
|
} catch (ConfigurationLoadingException e) {
|
||||||
|
LOG.error("Configuration loading failed: {}", e.getMessage());
|
||||||
|
return 1;
|
||||||
} catch (InvalidStartConfigurationException e) {
|
} catch (InvalidStartConfigurationException e) {
|
||||||
LOG.error("Configuration validation failed: {}", e.getMessage());
|
LOG.error("Configuration validation failed: {}", e.getMessage());
|
||||||
return 1;
|
return 1;
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
LOG.error("Configuration loading failed: {}", e.getMessage());
|
|
||||||
return 1;
|
|
||||||
} catch (DocumentPersistenceException e) {
|
} catch (DocumentPersistenceException e) {
|
||||||
LOG.error("Persistence operation failed: {}", e.getMessage(), e);
|
LOG.error("Persistence operation failed: {}", e.getMessage(), e);
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -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.in.cli.SchedulerBatchCommand;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException;
|
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.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.config.startup.StartConfiguration;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
||||||
@@ -81,7 +82,7 @@ class BootstrapRunnerTest {
|
|||||||
@Test
|
@Test
|
||||||
void run_returnsOneOnConfigurationLoadingFailure() {
|
void run_returnsOneOnConfigurationLoadingFailure() {
|
||||||
ConfigurationPort failingConfigPort = () -> {
|
ConfigurationPort failingConfigPort = () -> {
|
||||||
throw new IllegalStateException("Simulated configuration loading failure");
|
throw new ConfigurationLoadingException("Simulated configuration loading failure");
|
||||||
};
|
};
|
||||||
|
|
||||||
BootstrapRunner runner = new BootstrapRunner(
|
BootstrapRunner runner = new BootstrapRunner(
|
||||||
|
|||||||
Reference in New Issue
Block a user