39public final class ConfigValidator {
41 private ConfigValidator() {
44 private static final int MAX_DEPTH = 20;
46 public static void validate(
final Object instance) {
47 Objects.requireNonNull(instance,
"instance");
48 final List<ConfigViolation> violations =
new ArrayList<>();
49 validateInstance(instance,
"", violations, 0);
50 if (!violations.isEmpty()) {
55 private static void validateInstance(
final Object instance,
final String path,
56 final List<ConfigViolation> violations,
final int depth) {
57 if (instance ==
null || !instance.getClass().isRecord()) {
61 for (
final RecordComponent component : instance.getClass().getRecordComponents()) {
62 final var componentPath = path.isBlank() ? component.getName() : path +
"." + component.getName();
65 final var accessor = component.getAccessor();
66 accessor.setAccessible(
true);
67 value = accessor.invoke(instance);
68 }
catch (
final ReflectiveOperationException e) {
69 throw new IllegalStateException(
"Unable to validate component " + componentPath, e);
72 if (component.isAnnotationPresent(Required.class) && value ==
null) {
80 final var notBlank = component.getAnnotation(NotBlank.class);
81 if (notBlank !=
null && value instanceof
final String stringValue && stringValue.isBlank()) {
82 violations.add(
new ConfigViolation(componentPath,
"must not be blank"));
85 final var min = component.getAnnotation(Min.class);
86 if (min !=
null && value instanceof
final Number number && number.longValue() < min.value()) {
87 violations.add(
new ConfigViolation(componentPath,
"must be >= " + min.value()));
90 final var max = component.getAnnotation(Max.class);
91 if (max !=
null && value instanceof
final Number number && number.longValue() > max.value()) {
92 violations.add(
new ConfigViolation(componentPath,
"must be <= " + max.value()));
95 final var pattern = component.getAnnotation(Pattern.class);
96 if (pattern !=
null && value instanceof
final String stringValue) {
98 if (!java.util.regex.Pattern.compile(pattern.value()).matcher(stringValue).matches()) {
99 violations.add(
new ConfigViolation(componentPath,
"must match " + pattern.value()));
101 }
catch (
final PatternSyntaxException e) {
102 throw new IllegalArgumentException(
"Invalid regex on " + componentPath +
": " + pattern.value(), e);
106 final var size = component.getAnnotation(Size.class);
108 final var actualSize = sizeOf(value);
109 if (actualSize >= 0 && (actualSize < size.min() || actualSize > size.max())) {
111 "size must be between " + size.min() +
" and " + size.max()));
115 if (component.isAnnotationPresent(Valid.class)) {
116 validateNested(value, componentPath, violations, depth + 1);
121 private static void validateNested(
final Object value,
final String path,
final List<ConfigViolation> violations,
123 if (depth >= MAX_DEPTH) {
129 if (value.getClass().isRecord()) {
130 validateInstance(value, path, violations, depth + 1);
133 if (value instanceof
final Collection<?> collection) {
135 for (
final Object element : collection) {
136 validateNested(element, path +
"[" + index +
"]", violations, depth + 1);
141 if (value instanceof
final Map<?, ?> map) {
142 for (
final var entry : map.entrySet()) {
143 validateNested(entry.getValue(), path +
"." + entry.getKey(), violations, depth + 1);
148 private static int sizeOf(
final Object value) {
149 return switch (value) {
150 case final String stringValue -> stringValue.length();
151 case final Collection<?> collection -> collection.size();
152 case final Map<?, ?> map -> map.size();
153 case null,
default -> -1;