-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support validation in ConfigMappings.
Showing
11 changed files
with
604 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
implementation/src/main/java/io/smallrye/config/ConfigValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package io.smallrye.config; | ||
|
||
public interface ConfigValidator { | ||
void validateMapping(Class<?> mappingClass, String prefix, Object mappingObject) throws ConfigValidationException; | ||
|
||
ConfigValidator EMPTY = (mappingClass, prefix, mappingObject) -> { | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
~ Copyright 2021 Red Hat, Inc. | ||
~ | ||
~ Licensed under the Apache License, Version 2.0 (the "License"); | ||
~ you may not use this file except in compliance with the License. | ||
~ You may obtain a copy of the License at | ||
~ | ||
~ http://www.apache.org/licenses/LICENSE-2.0 | ||
~ | ||
~ Unless required by applicable law or agreed to in writing, software | ||
~ distributed under the License is distributed on an "AS IS" BASIS, | ||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
~ See the License for the specific language governing permissions and | ||
~ limitations under the License. | ||
--> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>io.smallrye.config</groupId> | ||
<artifactId>smallrye-config-parent</artifactId> | ||
<version>2.2.1-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>smallrye-config-validator</artifactId> | ||
|
||
<name>SmallRye: MicroProfile Config Validator</name> | ||
|
||
<properties> | ||
<version.jakarta.validation>2.0.2</version.jakarta.validation> | ||
|
||
<!-- Test --> | ||
<version.hibernate.validator>6.2.0.Final</version.hibernate.validator> | ||
<version.jakarta.el>3.0.3</version.jakarta.el> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>io.smallrye.config</groupId> | ||
<artifactId>smallrye-config</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>jakarta.validation</groupId> | ||
<artifactId>jakarta.validation-api</artifactId> | ||
<version>${version.jakarta.validation}</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<!-- Test --> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.hibernate.validator</groupId> | ||
<artifactId>hibernate-validator</artifactId> | ||
<version>${version.hibernate.validator}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.glassfish</groupId> | ||
<artifactId>jakarta.el</artifactId> | ||
<version>${version.jakarta.el}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
</project> |
196 changes: 196 additions & 0 deletions
196
validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
package io.smallrye.config.validator; | ||
|
||
import java.lang.reflect.InvocationTargetException; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
|
||
import javax.validation.ConstraintViolation; | ||
import javax.validation.Path; | ||
import javax.validation.Validator; | ||
|
||
import io.smallrye.config.ConfigMappingInterface; | ||
import io.smallrye.config.ConfigMappingInterface.CollectionProperty; | ||
import io.smallrye.config.ConfigMappingInterface.MapProperty; | ||
import io.smallrye.config.ConfigMappingInterface.NamingStrategy; | ||
import io.smallrye.config.ConfigMappingInterface.Property; | ||
import io.smallrye.config.ConfigValidationException; | ||
import io.smallrye.config.ConfigValidationException.Problem; | ||
import io.smallrye.config.ConfigValidator; | ||
|
||
public interface BeanValidationConfigValidator extends ConfigValidator { | ||
|
||
Validator getValidator(); | ||
|
||
@Override | ||
default void validateMapping( | ||
final Class<?> mappingClass, | ||
final String prefix, | ||
final Object mappingObject) | ||
throws ConfigValidationException { | ||
|
||
final List<Problem> problems = new ArrayList<>(); | ||
final ConfigMappingInterface mappingInterface = ConfigMappingInterface.getConfigurationInterface(mappingClass); | ||
if (mappingInterface != null) { | ||
validateMappingInterface(mappingInterface, prefix, mappingInterface.getNamingStrategy(), mappingObject, problems); | ||
} else { | ||
validateMappingClass(mappingObject, problems); | ||
} | ||
|
||
if (!problems.isEmpty()) { | ||
throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS)); | ||
} | ||
} | ||
|
||
default void validateMappingInterface( | ||
final ConfigMappingInterface mappingInterface, | ||
final String currentPath, | ||
final NamingStrategy namingStrategy, | ||
final Object mappingObject, | ||
final List<Problem> problems) { | ||
|
||
for (Property property : mappingInterface.getProperties()) { | ||
validateProperty(property, currentPath, namingStrategy, mappingObject, false, problems); | ||
} | ||
} | ||
|
||
default void validateProperty( | ||
final Property property, | ||
final String currentPath, | ||
final NamingStrategy namingStrategy, | ||
final Object mappingObject, | ||
final boolean optional, | ||
final List<Problem> problems) { | ||
|
||
if (property.isOptional()) { | ||
validateProperty(property.asOptional().getNestedProperty(), currentPath, namingStrategy, mappingObject, true, | ||
problems); | ||
} | ||
|
||
if ((property.isLeaf() || property.isPrimitive()) && !property.isOptional()) { | ||
validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); | ||
} | ||
|
||
if (property.isGroup()) { | ||
try { | ||
Object group = property.getMethod().invoke(mappingObject); | ||
// unwrap | ||
if (optional) { | ||
Optional<?> optionalGroup = (Optional<?>) group; | ||
if (!optionalGroup.isPresent()) { | ||
return; | ||
} | ||
group = optionalGroup.get(); | ||
} | ||
|
||
validateMappingInterface(property.asGroup().getGroupType(), appendPropertyName(currentPath, property), | ||
namingStrategy, group, problems); | ||
} catch (IllegalAccessException | InvocationTargetException e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
if (property.isCollection()) { | ||
CollectionProperty collectionProperty = property.asCollection(); | ||
if (collectionProperty.getElement().isGroup()) { | ||
try { | ||
Collection<?> collection = (Collection<?>) property.getMethod().invoke(mappingObject); | ||
int i = 0; | ||
for (Object element : collection) { | ||
validateMappingInterface(collectionProperty.getElement().asGroup().getGroupType(), | ||
appendPropertyName(currentPath, property) + "[" + i + "]", | ||
namingStrategy, element, problems); | ||
i++; | ||
} | ||
} catch (IllegalAccessException | InvocationTargetException e) { | ||
e.printStackTrace(); | ||
} | ||
} else if (collectionProperty.getElement().isLeaf()) { | ||
validateProperty(collectionProperty.getElement(), currentPath, namingStrategy, mappingObject, optional, | ||
problems); | ||
} | ||
} | ||
|
||
if (property.isMap()) { | ||
MapProperty mapProperty = property.asMap(); | ||
if (mapProperty.getValueProperty().isGroup()) { | ||
try { | ||
Map<?, ?> map = (Map<?, ?>) property.getMethod().invoke(mappingObject); | ||
for (Map.Entry<?, ?> entry : map.entrySet()) { | ||
validateMappingInterface(mapProperty.getValueProperty().asGroup().getGroupType(), | ||
appendPropertyName(currentPath, property) + "." + entry.getKey(), | ||
namingStrategy, entry.getValue(), problems); | ||
} | ||
} catch (IllegalAccessException | InvocationTargetException e) { | ||
e.printStackTrace(); | ||
} | ||
} else if (mapProperty.getValueProperty().isLeaf()) { | ||
validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems); | ||
} | ||
} | ||
} | ||
|
||
default void validatePropertyValue( | ||
final Property property, | ||
final String currentPath, | ||
final NamingStrategy namingStrategy, | ||
final Object mappingObject, | ||
final List<Problem> problems) { | ||
|
||
try { | ||
Set<ConstraintViolation<Object>> violations = getValidator().forExecutables().validateReturnValue(mappingObject, | ||
property.getMethod(), | ||
property.getMethod().invoke(mappingObject)); | ||
for (ConstraintViolation<Object> violation : violations) { | ||
problems.add(new Problem(interpolateMessage(currentPath, namingStrategy, property, violation))); | ||
} | ||
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
|
||
default void validateMappingClass(final Object mappingObject, final List<Problem> problems) { | ||
final Set<ConstraintViolation<Object>> violations = getValidator().validate(mappingObject); | ||
for (ConstraintViolation<Object> violation : violations) { | ||
problems.add(new Problem(violation.getPropertyPath() + " " + violation.getMessage())); | ||
} | ||
} | ||
|
||
default String appendPropertyName(final String currentPath, final Property property) { | ||
if (currentPath.isEmpty()) { | ||
return property.getPropertyName(); | ||
} | ||
|
||
if (property.getPropertyName().isEmpty()) { | ||
return currentPath; | ||
} | ||
|
||
return currentPath + "." + property.getPropertyName(); | ||
} | ||
|
||
default String interpolateMessage( | ||
final String currentPath, | ||
final NamingStrategy namingStrategy, | ||
final Property property, | ||
final ConstraintViolation<?> violation) { | ||
StringBuilder propertyName = new StringBuilder(currentPath); | ||
String name = namingStrategy.apply(property.getPropertyName()); | ||
if (!name.isEmpty()) { | ||
propertyName.append(".").append(name); | ||
} | ||
Path propertyPath = violation.getPropertyPath(); | ||
for (Path.Node node : propertyPath) { | ||
if (node.isInIterable()) { | ||
if (node.getIndex() != null) { | ||
propertyName.append("[").append(node.getIndex()).append("]"); | ||
} else if (node.getKey() != null) { | ||
propertyName.append(".").append(node.getKey()); | ||
} | ||
} | ||
} | ||
return propertyName.toString() + " " + violation.getMessage(); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package io.smallrye.config.validator; | ||
|
||
import javax.validation.Validation; | ||
import javax.validation.Validator; | ||
|
||
public class BeanValidationConfigValidatorImpl implements BeanValidationConfigValidator { | ||
private Validator validator; | ||
|
||
public BeanValidationConfigValidatorImpl() { | ||
this.validator = Validation.buildDefaultValidatorFactory().getValidator(); | ||
} | ||
|
||
@Override | ||
public Validator getValidator() { | ||
return validator; | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
validator/src/test/java/io/smallrye/config/validator/KeyValuesConfigSource.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package io.smallrye.config.validator; | ||
|
||
import java.io.Serializable; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
import org.eclipse.microprofile.config.spi.ConfigSource; | ||
|
||
public class KeyValuesConfigSource implements ConfigSource, Serializable { | ||
|
||
private final Map<String, String> properties = new HashMap<>(); | ||
|
||
private KeyValuesConfigSource(Map<String, String> properties) { | ||
this.properties.putAll(properties); | ||
} | ||
|
||
@Override | ||
public Map<String, String> getProperties() { | ||
return Collections.unmodifiableMap(properties); | ||
} | ||
|
||
@Override | ||
public Set<String> getPropertyNames() { | ||
return Collections.unmodifiableSet(properties.keySet()); | ||
} | ||
|
||
@Override | ||
public String getValue(String propertyName) { | ||
return properties.get(propertyName); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return "KeyValuesConfigSource"; | ||
} | ||
|
||
public static ConfigSource config(Map<String, String> properties) { | ||
return new KeyValuesConfigSource(properties); | ||
} | ||
|
||
public static ConfigSource config(String... keyValues) { | ||
if (keyValues.length % 2 != 0) { | ||
throw new IllegalArgumentException("keyValues array must be a multiple of 2"); | ||
} | ||
|
||
Map<String, String> props = new HashMap<>(); | ||
for (int i = 0; i < keyValues.length; i += 2) { | ||
props.put(keyValues[i], keyValues[i + 1]); | ||
} | ||
return new KeyValuesConfigSource(props); | ||
} | ||
} |
212 changes: 212 additions & 0 deletions
212
validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package io.smallrye.config.validator; | ||
|
||
import static io.smallrye.config.validator.KeyValuesConfigSource.config; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.OptionalInt; | ||
|
||
import javax.validation.constraints.Max; | ||
import javax.validation.constraints.Min; | ||
import javax.validation.constraints.Size; | ||
|
||
import org.eclipse.microprofile.config.inject.ConfigProperties; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import io.smallrye.config.ConfigMapping; | ||
import io.smallrye.config.ConfigValidationException; | ||
import io.smallrye.config.SmallRyeConfig; | ||
import io.smallrye.config.SmallRyeConfigBuilder; | ||
import io.smallrye.config.WithParentName; | ||
|
||
public class ValidateConfigTest { | ||
@Test | ||
void validateConfigMapping() { | ||
SmallRyeConfig config = new SmallRyeConfigBuilder() | ||
.withValidator(new BeanValidationConfigValidatorImpl()) | ||
.withSources(config( | ||
"server.host", "localhost", | ||
"server.port", "8080", | ||
"server.log.days", "20", | ||
"server.proxy.enable", "true", | ||
"server.proxy.timeout", "20", | ||
"server.form.login-page", "login.html", | ||
"server.form.error-page", "error.html", | ||
"server.form.landing-page", "index.html", | ||
"server.cors.origins[0].host", "some-server", | ||
"server.cors.origins[0].port", "9000", | ||
"server.cors.origins[1].host", "localhost", | ||
"server.cors.origins[1].port", "1", | ||
"server.cors.methods[0]", "GET", | ||
"server.cors.methods[1]", "POST", | ||
"server.info.name", "Bond", | ||
"server.info.code", "007", | ||
"server.info.alias[0]", "James", | ||
"server.info.admins.root.username", "root")) | ||
.withMapping(Server.class, "server") | ||
.build(); | ||
|
||
ConfigValidationException validationException = assertThrows(ConfigValidationException.class, | ||
() -> config.getConfigMapping(Server.class, "server")); | ||
List<String> validations = new ArrayList<>(); | ||
for (int i = 0; i < validationException.getProblemCount(); i++) { | ||
validations.add(validationException.getProblem(i).getMessage()); | ||
} | ||
assertTrue(validations.contains("server.port must be less than or equal to 10")); | ||
assertTrue(validations.contains("server.log.days must be less than or equal to 15")); | ||
assertTrue(validations.contains("server.proxy.timeout must be less than or equal to 10")); | ||
assertTrue(validations.contains("server.cors.origins[0].host size must be between 0 and 10")); | ||
assertTrue(validations.contains("server.cors.origins[0].port must be less than or equal to 10")); | ||
assertTrue(validations.contains("server.cors.methods[1] size must be between 0 and 3")); | ||
assertTrue(validations.contains("server.form.login-page size must be between 0 and 3")); | ||
assertTrue(validations.contains("server.form.error-page size must be between 0 and 3")); | ||
assertTrue(validations.contains("server.form.landing-page size must be between 0 and 3")); | ||
assertTrue(validations.contains("server.info.name size must be between 0 and 3")); | ||
assertTrue(validations.contains("server.info.code must be less than or equal to 3")); | ||
assertTrue(validations.contains("server.info.alias[0] size must be between 0 and 3")); | ||
assertTrue(validations.contains("server.info.admins.root.username size must be between 0 and 3")); | ||
} | ||
|
||
@Test | ||
void validateNamingStrategy() { | ||
SmallRyeConfig config = new SmallRyeConfigBuilder() | ||
.withValidator(new BeanValidationConfigValidatorImpl()) | ||
.withSources(config( | ||
"server.the_host", "localhost", | ||
"server.the_port", "8080")) | ||
.withMapping(ServerNamingStrategy.class, "server") | ||
.build(); | ||
|
||
ConfigValidationException validationException = assertThrows(ConfigValidationException.class, | ||
() -> config.getConfigMapping(ServerNamingStrategy.class, "server")); | ||
List<String> validations = new ArrayList<>(); | ||
for (int i = 0; i < validationException.getProblemCount(); i++) { | ||
validations.add(validationException.getProblem(i).getMessage()); | ||
} | ||
|
||
assertTrue(validations.contains("server.the_port must be less than or equal to 10")); | ||
} | ||
|
||
@Test | ||
void validateConfigProperties() { | ||
SmallRyeConfig config = new SmallRyeConfigBuilder() | ||
.withValidator(new BeanValidationConfigValidatorImpl()) | ||
.withMapping(Client.class, "client") | ||
.build(); | ||
|
||
ConfigValidationException validationException = assertThrows(ConfigValidationException.class, | ||
() -> config.getConfigMapping(Client.class, "client")); | ||
assertEquals(1, validationException.getProblemCount()); | ||
List<String> validations = new ArrayList<>(); | ||
validations.add(validationException.getProblem(0).getMessage()); | ||
assertTrue(validations.contains("port must be less than or equal to 10")); | ||
} | ||
|
||
@Test | ||
void validateParent() { | ||
SmallRyeConfig config = new SmallRyeConfigBuilder() | ||
.withValidator(new BeanValidationConfigValidatorImpl()) | ||
.withSources(config( | ||
"server.host", "localhost", | ||
"server.port", "80")) | ||
.withMapping(ServerParent.class, "server") | ||
.build(); | ||
|
||
ConfigValidationException validationException = assertThrows(ConfigValidationException.class, | ||
() -> config.getConfigMapping(ServerParent.class, "server")); | ||
assertEquals("server.port must be greater than or equal to 8000", validationException.getProblem(0).getMessage()); | ||
} | ||
|
||
@ConfigMapping(prefix = "server") | ||
public interface Server { | ||
String host(); | ||
|
||
@Max(10) | ||
int port(); | ||
|
||
Map<String, @Size(max = 3) String> form(); | ||
|
||
Optional<Proxy> proxy(); | ||
|
||
Log log(); | ||
|
||
Cors cors(); | ||
|
||
Info info(); | ||
|
||
interface Proxy { | ||
boolean enable(); | ||
|
||
@Max(10) | ||
int timeout(); | ||
} | ||
|
||
interface Log { | ||
@Max(15) | ||
int days(); | ||
} | ||
|
||
interface Cors { | ||
List<Origin> origins(); | ||
|
||
List<@Size(max = 3) String> methods(); | ||
|
||
interface Origin { | ||
@Size(max = 10) | ||
String host(); | ||
|
||
@Max(10) | ||
int port(); | ||
} | ||
} | ||
|
||
interface Info { | ||
Optional<@Size(max = 3) String> name(); | ||
|
||
@Max(3) | ||
OptionalInt code(); | ||
|
||
Optional<List<@Size(max = 3) String>> alias(); | ||
|
||
Map<String, Admin> admins(); | ||
|
||
interface Admin { | ||
@Size(max = 3) | ||
String username(); | ||
} | ||
} | ||
} | ||
|
||
@ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE) | ||
public interface ServerNamingStrategy { | ||
String theHost(); | ||
|
||
@Max(10) | ||
int thePort(); | ||
} | ||
|
||
@ConfigProperties(prefix = "client") | ||
public static class Client { | ||
public String host = "localhost"; | ||
@Max(10) | ||
public int port = 8080; | ||
} | ||
|
||
@ConfigMapping(prefix = "server") | ||
public interface ServerParent { | ||
@WithParentName | ||
Parent parent(); | ||
|
||
interface Parent { | ||
String host(); | ||
|
||
@Min(8000) | ||
int port(); | ||
} | ||
} | ||
} |