AppConfigValidator and AppConfigValidatorTest added and integrated into ApplicationInitializer
This commit is contained in:
parent
9ae52a9785
commit
26a253abf6
@ -3,6 +3,7 @@ package vassistent.bootstrap;
|
|||||||
import com.formdev.flatlaf.FlatDarkLaf;
|
import com.formdev.flatlaf.FlatDarkLaf;
|
||||||
import vassistent.model.AppState;
|
import vassistent.model.AppState;
|
||||||
import vassistent.service.*;
|
import vassistent.service.*;
|
||||||
|
import vassistent.util.AppConfigValidator;
|
||||||
import vassistent.util.ConfigLoader;
|
import vassistent.util.ConfigLoader;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
@ -25,6 +26,8 @@ public class ApplicationInitializer {
|
|||||||
*/
|
*/
|
||||||
public static ApplicationContext initialize() {
|
public static ApplicationContext initialize() {
|
||||||
|
|
||||||
|
AppConfigValidator.validate(config);
|
||||||
|
|
||||||
ApplicationContext context = new ApplicationContext();
|
ApplicationContext context = new ApplicationContext();
|
||||||
|
|
||||||
// ===== Model State =====
|
// ===== Model State =====
|
||||||
|
|||||||
591
src/main/java/vassistent/util/AppConfigValidator.java
Normal file
591
src/main/java/vassistent/util/AppConfigValidator.java
Normal file
@ -0,0 +1,591 @@
|
|||||||
|
package vassistent.util;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.InvalidPathException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates startup configuration and fails fast on invalid values.
|
||||||
|
*/
|
||||||
|
public final class AppConfigValidator {
|
||||||
|
|
||||||
|
private static final Set<String> SUPPORTED_MQTT_SCHEMES =
|
||||||
|
Set.of("tcp", "ssl", "ws", "wss");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class constructor.
|
||||||
|
*/
|
||||||
|
private AppConfigValidator() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates all required startup configuration values.
|
||||||
|
*
|
||||||
|
* @param config loaded application properties
|
||||||
|
*/
|
||||||
|
public static void validate(Properties config) {
|
||||||
|
Objects.requireNonNull(config, "config must not be null");
|
||||||
|
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
validateRequiredMqttSettings(config, errors);
|
||||||
|
validateCoreUiAndOutputSettings(config, errors);
|
||||||
|
|
||||||
|
boolean mqttSimulatorEnabled =
|
||||||
|
readBoolean(config, errors, "mqtt_sim.enabled", false);
|
||||||
|
boolean unrealEnabled =
|
||||||
|
readBoolean(config, errors, "unreal.enabled", false);
|
||||||
|
|
||||||
|
if (mqttSimulatorEnabled) {
|
||||||
|
validateMqttSimulatorSettings(config, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unrealEnabled) {
|
||||||
|
validateUnrealSettings(config, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new IllegalStateException(buildValidationMessage(errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates mandatory MQTT runtime keys.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
*/
|
||||||
|
private static void validateRequiredMqttSettings(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors
|
||||||
|
) {
|
||||||
|
validateRequiredNonBlank(config, errors, "mqtt.topic");
|
||||||
|
validateRequiredNonBlank(config, errors, "mqtt.client.id");
|
||||||
|
|
||||||
|
String brokerUrl =
|
||||||
|
validateRequiredNonBlank(config, errors, "mqtt.broker.url");
|
||||||
|
if (brokerUrl == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(brokerUrl);
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
if (scheme == null
|
||||||
|
|| !SUPPORTED_MQTT_SCHEMES.contains(
|
||||||
|
scheme.toLowerCase(Locale.ROOT))) {
|
||||||
|
errors.add(
|
||||||
|
"Property 'mqtt.broker.url' must use one of "
|
||||||
|
+ SUPPORTED_MQTT_SCHEMES + ": "
|
||||||
|
+ brokerUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.getHost() == null || uri.getHost().isBlank()) {
|
||||||
|
errors.add(
|
||||||
|
"Property 'mqtt.broker.url' must include a host: "
|
||||||
|
+ brokerUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int port = uri.getPort();
|
||||||
|
if (port != -1 && (port < 1 || port > 65535)) {
|
||||||
|
errors.add(
|
||||||
|
"Property 'mqtt.broker.url' contains invalid port "
|
||||||
|
+ port + ": " + brokerUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
errors.add(
|
||||||
|
"Property 'mqtt.broker.url' is not a valid URI: " + brokerUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates UI and animation output settings.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
*/
|
||||||
|
private static void validateCoreUiAndOutputSettings(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors
|
||||||
|
) {
|
||||||
|
String streamingUrl =
|
||||||
|
validateRequiredNonBlank(config, errors, "streaming.url");
|
||||||
|
if (streamingUrl != null) {
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(streamingUrl);
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
if (scheme == null
|
||||||
|
|| (!"http".equalsIgnoreCase(scheme)
|
||||||
|
&& !"https".equalsIgnoreCase(scheme))) {
|
||||||
|
errors.add(
|
||||||
|
"Property 'streaming.url' must start with http:// or https://: "
|
||||||
|
+ streamingUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
errors.add(
|
||||||
|
"Property 'streaming.url' is not a valid URI: " + streamingUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRequiredNonBlank(config, errors, "animation.output.path");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates simulator settings that are required only when simulator startup is enabled.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
*/
|
||||||
|
private static void validateMqttSimulatorSettings(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors
|
||||||
|
) {
|
||||||
|
validateExistingFile(config, errors, "python.path");
|
||||||
|
validateExistingFile(config, errors, "mqtt_sim.script");
|
||||||
|
validateRequiredNonBlank(config, errors, "mqtt_sim.broker");
|
||||||
|
validateRequiredIntegerInRange(config, errors, "mqtt_sim.port", 1, 65535);
|
||||||
|
validateOptionalIntegerInRange(config, errors, "mqtt_sim.qos", 0, 2);
|
||||||
|
validateOptionalPositiveDouble(config, errors, "mqtt_sim.interval_seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates Unreal-related settings that are required only when Unreal startup is enabled.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
*/
|
||||||
|
private static void validateUnrealSettings(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors
|
||||||
|
) {
|
||||||
|
validateExistingFile(config, errors, "unreal.executable");
|
||||||
|
validateExistingFile(config, errors, "unreal.signalling_server.script");
|
||||||
|
validateExistingFile(config, errors, "unreal.target.executable");
|
||||||
|
validateExistingDirectory(config, errors, "unreal.target.working_dir");
|
||||||
|
validateOptionalExistingDirectory(config, errors, "unreal.pid.dir");
|
||||||
|
validateOptionalNonNegativeInteger(config, errors, "unreal.startup.delay.seconds");
|
||||||
|
validateOptionalPidFileParent(config, errors, "unreal.pid.file");
|
||||||
|
validateOptionalPidFileParent(config, errors, "unreal.signalling.pid.file");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a required property exists and is not blank.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
* @return normalized value or {@code null} when missing/blank
|
||||||
|
*/
|
||||||
|
private static String validateRequiredNonBlank(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String value = normalizeConfigValue(config.getProperty(key));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
errors.add("Missing required property '" + key + "'.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a property points to an existing regular file.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
*/
|
||||||
|
private static void validateExistingFile(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String rawValue = config.getProperty(key);
|
||||||
|
String normalized = validateRequiredNonBlank(config, errors, key);
|
||||||
|
if (normalized == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = parsePath(rawValue, normalized, key, errors);
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must reference an existing file: " + normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a property points to an existing directory.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
*/
|
||||||
|
private static void validateExistingDirectory(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String rawValue = config.getProperty(key);
|
||||||
|
String normalized = validateRequiredNonBlank(config, errors, key);
|
||||||
|
if (normalized == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = parsePath(rawValue, normalized, key, errors);
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(path)) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must reference an existing directory: " + normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an optional directory property when a value is provided.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
*/
|
||||||
|
private static void validateOptionalExistingDirectory(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String rawValue = config.getProperty(key);
|
||||||
|
String normalized = normalizeConfigValue(rawValue);
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = parsePath(rawValue, normalized, key, errors);
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.isDirectory(path)) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must reference an existing directory: " + normalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an optional integer property with a minimum/maximum range.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
* @param min minimum valid value
|
||||||
|
* @param max maximum valid value
|
||||||
|
*/
|
||||||
|
private static void validateOptionalIntegerInRange(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key,
|
||||||
|
int min,
|
||||||
|
int max
|
||||||
|
) {
|
||||||
|
String value = normalizeConfigValue(config.getProperty(key));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(value);
|
||||||
|
if (parsed < min || parsed > max) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be between "
|
||||||
|
+ min + " and " + max + ", but was " + parsed + "."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be an integer, but was '" + value + "'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a required integer property with a minimum/maximum range.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
* @param min minimum valid value
|
||||||
|
* @param max maximum valid value
|
||||||
|
*/
|
||||||
|
private static void validateRequiredIntegerInRange(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key,
|
||||||
|
int min,
|
||||||
|
int max
|
||||||
|
) {
|
||||||
|
String value = validateRequiredNonBlank(config, errors, key);
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(value);
|
||||||
|
if (parsed < min || parsed > max) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be between "
|
||||||
|
+ min + " and " + max + ", but was " + parsed + "."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be an integer, but was '" + value + "'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an optional numeric property that must be greater than zero.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
*/
|
||||||
|
private static void validateOptionalPositiveDouble(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String value = normalizeConfigValue(config.getProperty(key));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
double parsed = Double.parseDouble(value);
|
||||||
|
if (parsed <= 0.0) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be greater than 0, but was " + parsed + "."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be numeric, but was '" + value + "'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an optional integer that must be non-negative.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
*/
|
||||||
|
private static void validateOptionalNonNegativeInteger(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String value = normalizeConfigValue(config.getProperty(key));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int parsed = Integer.parseInt(value);
|
||||||
|
if (parsed < 0) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be >= 0, but was " + parsed + "."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be an integer, but was '" + value + "'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that an optional PID file path has an existing parent directory.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
*/
|
||||||
|
private static void validateOptionalPidFileParent(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
String rawValue = config.getProperty(key);
|
||||||
|
String normalized = normalizeConfigValue(rawValue);
|
||||||
|
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path path = parsePath(rawValue, normalized, key, errors);
|
||||||
|
if (path == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path parent = path.getParent();
|
||||||
|
if (parent != null && !Files.isDirectory(parent)) {
|
||||||
|
errors.add(
|
||||||
|
"Parent directory for '" + key + "' does not exist: " + parent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a boolean property using strict true/false values.
|
||||||
|
*
|
||||||
|
* @param config loaded properties
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @param key property key
|
||||||
|
* @param defaultValue fallback when value is absent/blank
|
||||||
|
* @return parsed boolean value or default value
|
||||||
|
*/
|
||||||
|
private static boolean readBoolean(
|
||||||
|
Properties config,
|
||||||
|
List<String> errors,
|
||||||
|
String key,
|
||||||
|
boolean defaultValue
|
||||||
|
) {
|
||||||
|
String value = normalizeConfigValue(config.getProperty(key));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("true".equalsIgnoreCase(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("false".equalsIgnoreCase(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' must be 'true' or 'false', but was '" + value + "'."
|
||||||
|
);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a path value and reports invalid quoting/path syntax.
|
||||||
|
*
|
||||||
|
* @param rawValue raw property value
|
||||||
|
* @param normalized normalized property value
|
||||||
|
* @param key property key
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @return parsed path or {@code null} when invalid
|
||||||
|
*/
|
||||||
|
private static Path parsePath(
|
||||||
|
String rawValue,
|
||||||
|
String normalized,
|
||||||
|
String key,
|
||||||
|
List<String> errors
|
||||||
|
) {
|
||||||
|
if (hasMismatchedWrappingQuote(rawValue)) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' has mismatched wrapping quotes: " + rawValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Path.of(normalized);
|
||||||
|
} catch (InvalidPathException e) {
|
||||||
|
errors.add(
|
||||||
|
"Property '" + key + "' contains an invalid path: " + normalized
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects mismatched leading/trailing single or double quote wrappers.
|
||||||
|
*
|
||||||
|
* @param rawValue raw property value
|
||||||
|
* @return {@code true} when wrapper quotes are mismatched
|
||||||
|
*/
|
||||||
|
private static boolean hasMismatchedWrappingQuote(String rawValue) {
|
||||||
|
if (rawValue == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = rawValue.trim();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean startsDouble = trimmed.startsWith("\"");
|
||||||
|
boolean endsDouble = trimmed.endsWith("\"");
|
||||||
|
|
||||||
|
boolean startsSingle = trimmed.startsWith("'");
|
||||||
|
boolean endsSingle = trimmed.endsWith("'");
|
||||||
|
|
||||||
|
return startsDouble != endsDouble || startsSingle != endsSingle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trims config values and removes matching single/double quote wrappers.
|
||||||
|
*
|
||||||
|
* @param value raw config value
|
||||||
|
* @return normalized value or {@code null}
|
||||||
|
*/
|
||||||
|
private static String normalizeConfigValue(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = value.trim();
|
||||||
|
|
||||||
|
if ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
|
||||||
|
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
||||||
|
return trimmed.substring(1, trimmed.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a single human-readable validation exception message.
|
||||||
|
*
|
||||||
|
* @param errors collected validation errors
|
||||||
|
* @return formatted message
|
||||||
|
*/
|
||||||
|
private static String buildValidationMessage(List<String> errors) {
|
||||||
|
StringBuilder message = new StringBuilder(
|
||||||
|
"Configuration validation failed (" + errors.size() + " issue(s)):"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int i = 0; i < errors.size(); i++) {
|
||||||
|
message.append(System.lineSeparator())
|
||||||
|
.append(i + 1)
|
||||||
|
.append(". ")
|
||||||
|
.append(errors.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/test/java/vassistent/util/AppConfigValidatorTest.java
Normal file
155
src/test/java/vassistent/util/AppConfigValidatorTest.java
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package vassistent.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link AppConfigValidator}.
|
||||||
|
*/
|
||||||
|
class AppConfigValidatorTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that a minimal core configuration passes validation.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void validate_withCoreSettingsOnly_doesNotThrow() {
|
||||||
|
Properties config = baseConfig();
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> AppConfigValidator.validate(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies strict boolean validation for feature toggles.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void validate_withInvalidBooleanToggle_throws() {
|
||||||
|
Properties config = baseConfig();
|
||||||
|
config.setProperty("mqtt_sim.enabled", "yes");
|
||||||
|
|
||||||
|
IllegalStateException exception =
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() -> AppConfigValidator.validate(config)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("mqtt_sim.enabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies simulator-specific numeric validation when simulator is enabled.
|
||||||
|
*
|
||||||
|
* @throws Exception if test files cannot be created
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void validate_withMqttSimulatorInvalidPort_throws() throws Exception {
|
||||||
|
Properties config = baseConfig();
|
||||||
|
Path pythonExe = Files.createFile(tempDir.resolve("python.exe"));
|
||||||
|
Path simulatorScript = Files.createFile(tempDir.resolve("mqtt_simulator.py"));
|
||||||
|
|
||||||
|
config.setProperty("mqtt_sim.enabled", "true");
|
||||||
|
config.setProperty("python.path", pythonExe.toString());
|
||||||
|
config.setProperty("mqtt_sim.script", simulatorScript.toString());
|
||||||
|
config.setProperty("mqtt_sim.broker", "localhost");
|
||||||
|
config.setProperty("mqtt_sim.port", "99999");
|
||||||
|
|
||||||
|
IllegalStateException exception =
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() -> AppConfigValidator.validate(config)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("mqtt_sim.port"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies Unreal validation detects paths with mismatched quote wrappers.
|
||||||
|
*
|
||||||
|
* @throws Exception if test files cannot be created
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void validate_withUnrealSignallingPathTrailingQuote_throws() throws Exception {
|
||||||
|
Properties config = baseConfig();
|
||||||
|
Path launcherScript = Files.createFile(tempDir.resolve("start_avatar.ps1"));
|
||||||
|
Path signallingScript = Files.createFile(tempDir.resolve("start_with_stun.bat"));
|
||||||
|
Path unrealExe = Files.createFile(tempDir.resolve("Prototyp1.exe"));
|
||||||
|
Path unrealWorkDir = Files.createDirectory(tempDir.resolve("Windows"));
|
||||||
|
Path pidDir = Files.createDirectory(tempDir.resolve("pids"));
|
||||||
|
|
||||||
|
config.setProperty("unreal.enabled", "true");
|
||||||
|
config.setProperty("unreal.executable", launcherScript.toString());
|
||||||
|
config.setProperty(
|
||||||
|
"unreal.signalling_server.script",
|
||||||
|
signallingScript.toString() + "\""
|
||||||
|
);
|
||||||
|
config.setProperty("unreal.target.executable", unrealExe.toString());
|
||||||
|
config.setProperty("unreal.target.working_dir", unrealWorkDir.toString());
|
||||||
|
config.setProperty("unreal.pid.dir", pidDir.toString());
|
||||||
|
|
||||||
|
IllegalStateException exception =
|
||||||
|
assertThrows(
|
||||||
|
IllegalStateException.class,
|
||||||
|
() -> AppConfigValidator.validate(config)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("unreal.signalling_server.script"));
|
||||||
|
assertTrue(exception.getMessage().toLowerCase().contains("quote"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies Unreal validation accepts a complete and valid local setup.
|
||||||
|
*
|
||||||
|
* @throws Exception if test files cannot be created
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void validate_withUnrealEnabledAndValidPaths_doesNotThrow() throws Exception {
|
||||||
|
Properties config = baseConfig();
|
||||||
|
Path launcherScript = Files.createFile(tempDir.resolve("start_avatar.ps1"));
|
||||||
|
Path signallingScript = Files.createFile(tempDir.resolve("start_with_stun.bat"));
|
||||||
|
Path unrealExe = Files.createFile(tempDir.resolve("Prototyp1.exe"));
|
||||||
|
Path unrealWorkDir = Files.createDirectory(tempDir.resolve("Windows"));
|
||||||
|
Path pidDir = Files.createDirectory(tempDir.resolve("pids"));
|
||||||
|
|
||||||
|
config.setProperty("unreal.enabled", "true");
|
||||||
|
config.setProperty("unreal.executable", launcherScript.toString());
|
||||||
|
config.setProperty("unreal.signalling_server.script", signallingScript.toString());
|
||||||
|
config.setProperty("unreal.target.executable", unrealExe.toString());
|
||||||
|
config.setProperty("unreal.target.working_dir", unrealWorkDir.toString());
|
||||||
|
config.setProperty("unreal.pid.dir", pidDir.toString());
|
||||||
|
config.setProperty("unreal.startup.delay.seconds", "5");
|
||||||
|
config.setProperty("unreal.pid.file", pidDir.resolve("unreal.pid").toString());
|
||||||
|
config.setProperty(
|
||||||
|
"unreal.signalling.pid.file",
|
||||||
|
pidDir.resolve("signalling.pid").toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> AppConfigValidator.validate(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a baseline config with optional integrations disabled.
|
||||||
|
*
|
||||||
|
* @return baseline application configuration
|
||||||
|
*/
|
||||||
|
private Properties baseConfig() {
|
||||||
|
Properties config = new Properties();
|
||||||
|
config.setProperty("mqtt.topic", "PREDICTION");
|
||||||
|
config.setProperty("mqtt.broker.url", "tcp://localhost:1883");
|
||||||
|
config.setProperty("mqtt.client.id", "JavaClientPublisherSubscriber");
|
||||||
|
config.setProperty("streaming.url", "http://localhost");
|
||||||
|
config.setProperty("animation.output.path", "data/animation.json");
|
||||||
|
config.setProperty("mqtt_sim.enabled", "false");
|
||||||
|
config.setProperty("unreal.enabled", "false");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user