blob: 0e0d9e62029c9363b5c52d48079e681c996907fa [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to You 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.
*/
package org.apache.tuweni.config;
import static java.util.Objects.requireNonNull;
import static org.apache.tuweni.config.Configuration.canonicalKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
/**
* This interface allows customers to determine a schema to associate with a configuration to validate the entries read
* from configuration files, and provide default values if no value is present in the configuration file.
*/
public final class SchemaBuilder {
private final Map<String, String> propertyDescriptions = new HashMap<>();
private final Map<String, Object> propertyDefaults = new HashMap<>();
private final ListMultimap<String, PropertyValidator<Object>> propertyValidators = ArrayListMultimap.create();
private final List<ConfigurationValidator> configurationValidators = new ArrayList<>();
/**
* Get a new builder for a schema.
*
* @return A new {@link SchemaBuilder}.
*/
public static SchemaBuilder create() {
return new SchemaBuilder();
}
SchemaBuilder() {}
/**
* Provide documentation for a property.
*
* <p>
* Invoking this method with the same key as a previous invocation will replace the description for that key.
*
* @param key The configuration property key.
* @param description The description to associate with the property.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder documentProperty(String key, String description) {
requireNonNull(key);
requireNonNull(description);
propertyDescriptions.put(canonicalKey(key), description);
return this;
}
/**
* Provide a default value for a property.
*
* <p>
* Invoking this method with the same key as a previous invocation will replace the default value for that key.
*
* @param key The configuration property key.
* @param value The default value for the property.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addDefault(String key, Object value) {
requireNonNull(key);
requireNonNull(value);
propertyDefaults.put(canonicalKey(key), value);
return this;
}
/**
* Add a property validation to this schema.
*
* <p>
* Multiple validators can be provided for the same key by invoking this method multiple times.
*
* @param key The configuration property key.
* @param validator A validator for the property.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder validateProperty(String key, PropertyValidator<Object> validator) {
requireNonNull(key);
requireNonNull(validator);
propertyValidators.put(canonicalKey(key), validator);
return this;
}
/**
* Add a configuration validator to the schema.
*
* <p>
* Multiple validators can be provided by invoking this method multiple times.
*
* @param validator A configuration validator.
* @return This builder.
*/
public SchemaBuilder validateConfiguration(ConfigurationValidator validator) {
requireNonNull(validator);
configurationValidators.add(validator);
return this;
}
/**
* Add a string property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a string.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addString(
String key,
@Nullable String defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super String> validator) {
requireNonNull(key);
return addScalar(String.class, "string", key, defaultValue, description, validator);
}
/**
* Add an integer property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains an integer.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addInteger(
String key,
@Nullable Integer defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super Integer> validator) {
requireNonNull(key);
if (defaultValue != null) {
addDefault(key, defaultValue);
}
if (description != null) {
documentProperty(key, description);
}
validateProperty(key, (canonicalKey, position, value) -> {
if (!(value == null || value instanceof Integer || value instanceof Long)) {
return Collections
.singletonList(new ConfigurationError(position, "Property at '" + canonicalKey + "' requires an integer"));
}
if (validator == null || (defaultValue != null && value == null)) {
return Collections.emptyList();
}
Integer intValue;
if (value instanceof Long) {
if (((Long) value) > Integer.MAX_VALUE) {
return Collections.singletonList(
new ConfigurationError(position, "Value of property '" + canonicalKey + "' is too large for an integer"));
}
intValue = ((Long) value).intValue();
} else {
intValue = (Integer) value;
}
return validator.validate(canonicalKey, position, intValue);
});
return this;
}
/**
* Add a long property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a long.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addLong(
String key,
@Nullable Long defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super Long> validator) {
requireNonNull(key);
if (defaultValue != null) {
addDefault(key, defaultValue);
}
if (description != null) {
documentProperty(key, description);
}
validateProperty(key, (vkey, position, value) -> {
if (!(value == null || value instanceof Long || value instanceof Integer)) {
return Collections
.singletonList(new ConfigurationError(position, "Property at '" + vkey + "' requires a long"));
}
if (validator == null || (defaultValue != null && value == null)) {
return Collections.emptyList();
}
Long longValue;
if (value instanceof Integer) {
longValue = ((Integer) value).longValue();
} else {
longValue = (Long) value;
}
return validator.validate(vkey, position, longValue);
});
return this;
}
/**
* Add a double property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a double.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addDouble(
String key,
@Nullable Double defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super Double> validator) {
requireNonNull(key);
return addScalar(Double.class, "double", key, defaultValue, description, validator);
}
/**
* Add a boolean property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a boolean.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addBoolean(
String key,
@Nullable Boolean defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super Boolean> validator) {
requireNonNull(key);
return addScalar(Boolean.class, "boolean", key, defaultValue, description, validator);
}
/**
* Add a list-of-strings property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a list of strings without any null values.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addListOfString(
String key,
@Nullable List<String> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<String>> validator) {
requireNonNull(key);
return addList(String.class, "string", key, defaultValue, description, validator);
}
/**
* Add a list-of-integers property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a list of integers without any null values.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
@SuppressWarnings("unchecked")
public SchemaBuilder addListOfInteger(
String key,
@Nullable List<Integer> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<Integer>> validator) {
requireNonNull(key);
if (defaultValue != null) {
if (defaultValue.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("default value list contains null value(s)");
}
addDefault(key, defaultValue);
}
if (description != null) {
documentProperty(key, description);
}
validateProperty(key, (vkey, position, value) -> {
if (value != null && !(value instanceof List)) {
return Collections
.singletonList(new ConfigurationError(position, "Property at '" + vkey + "' requires a list of integers"));
}
boolean containsLong = false;
if (value != null) {
List<Object> objs = (List<Object>) value;
for (int i = 0; i < objs.size(); ++i) {
Object obj = objs.get(i);
if (obj == null) {
return Collections.singletonList(
new ConfigurationError(position, "Value of property '" + vkey + "', index " + i + ", is null"));
}
if (!(obj instanceof Integer) && !(obj instanceof Long)) {
return Collections.singletonList(
new ConfigurationError(
position,
"Value of property '" + vkey + "', index " + i + ", is not an integer"));
}
if (obj instanceof Long) {
containsLong = true;
if (((Long) obj) > Integer.MAX_VALUE) {
return Collections.singletonList(
new ConfigurationError(
position,
"Value of property '" + vkey + "', index " + i + ", is too large for an integer"));
}
}
}
}
if (validator == null || (defaultValue != null && value == null)) {
return Collections.emptyList();
}
if (!containsLong) {
return validator.validate(vkey, position, (List<Integer>) value);
} else {
return validator.validate(vkey, position, ((List<Object>) value).stream().map(o -> {
if (o instanceof Integer) {
return (Integer) o;
}
assert (o instanceof Long);
return ((Long) o).intValue();
}).collect(Collectors.toList()));
}
});
return this;
}
/**
* Add a list-of-longs property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a list of longs without any null values.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
@SuppressWarnings("unchecked")
public SchemaBuilder addListOfLong(
String key,
@Nullable List<Long> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<Long>> validator) {
requireNonNull(key);
if (defaultValue != null) {
if (defaultValue.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("default value list contains null value(s)");
}
addDefault(key, defaultValue);
}
if (description != null) {
documentProperty(key, description);
}
validateProperty(key, (canonicalKey, position, value) -> {
if (value != null && !(value instanceof List)) {
return Collections.singletonList(
new ConfigurationError(position, "Property at '" + canonicalKey + "' requires a list of longs"));
}
boolean containsInteger = false;
if (value != null) {
List<Object> objs = (List<Object>) value;
for (int i = 0; i < objs.size(); ++i) {
Object obj = objs.get(i);
if (obj == null) {
return Collections.singletonList(
new ConfigurationError(position, "Value of property '" + canonicalKey + "', index " + i + ", is null"));
}
if (!(obj instanceof Long) && !(obj instanceof Integer)) {
return Collections.singletonList(
new ConfigurationError(
position,
"Value of property '" + canonicalKey + "', index " + i + ", is not a long"));
}
containsInteger |= (obj instanceof Integer);
}
}
if (validator == null || (defaultValue != null && value == null)) {
return Collections.emptyList();
}
if (!containsInteger) {
return validator.validate(canonicalKey, position, (List<Long>) value);
} else {
return validator.validate(canonicalKey, position, ((List<Object>) value).stream().map(o -> {
if (o instanceof Long) {
return (Long) o;
}
assert (o instanceof Integer);
return ((Integer) o).longValue();
}).collect(Collectors.toList()));
}
});
return this;
}
/**
* Add a list-of-doubles property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a list of doubles without any null values.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addListOfDouble(
String key,
@Nullable List<Double> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<Double>> validator) {
requireNonNull(key);
return addList(Double.class, "double", key, defaultValue, description, validator);
}
/**
* Add a list-of-booleans property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a list of booleans without any null values.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addListOfBoolean(
String key,
@Nullable List<Boolean> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<Boolean>> validator) {
requireNonNull(key);
return addList(Boolean.class, "boolean", key, defaultValue, description, validator);
}
/**
* Add a list-of-maps property to the schema.
*
* <p>
* Even if no {@code validator} is provided, the schema will validate that the configuration property, if present,
* contains a list of maps without any null values.
*
* <p>
* If a {@code defaultValue} is provided, then the provided validator, if any, will only be invoked if the value is
* present (i.e. it will not be provided a {@code null} value to validate).
*
* @param key The configuration property key.
* @param defaultValue A default value for the property or null if no default is provided.
* @param description The description to associate with this property, or null if no documentation is provided.
* @param validator A validator for the property, or null if no validator is provided.
* @return This builder.
* @throws IllegalArgumentException If the key cannot be parsed.
*/
public SchemaBuilder addListOfMap(
String key,
@Nullable List<Map<String, Object>> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<Map<String, Object>>> validator) {
requireNonNull(key);
return addList(Map.class, "map", key, defaultValue, description, validator);
}
/**
* Return the {@link Schema} constructed by this builder.
*
* @return A {@link Schema}.
*/
public Schema toSchema() {
return new Schema(propertyDescriptions, propertyDefaults, propertyValidators, configurationValidators);
}
private <T> SchemaBuilder addScalar(
Class<T> clazz,
String typeName,
String key,
@Nullable T defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super T> validator) {
if (defaultValue != null) {
addDefault(key, defaultValue);
}
if (description != null) {
documentProperty(key, description);
}
validateProperty(key, (canonicalKey, position, value) -> {
if (value != null && !clazz.isInstance(value)) {
return Collections.singletonList(
new ConfigurationError(position, "Property at '" + canonicalKey + "' requires a " + typeName));
}
if (validator == null || (defaultValue != null && value == null)) {
return Collections.emptyList();
}
return validator.validate(canonicalKey, position, clazz.cast(value));
});
return this;
}
@SuppressWarnings("unchecked")
private <T, U> SchemaBuilder addList(
Class<U> innerClass,
String typeName,
String key,
@Nullable List<T> defaultValue,
@Nullable String description,
@Nullable PropertyValidator<? super List<T>> validator) {
if (defaultValue != null) {
if (defaultValue.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("default value list contains null value(s)");
}
addDefault(key, defaultValue);
}
if (description != null) {
documentProperty(key, description);
}
validateProperty(key, (canonicalKey, position, value) -> {
if (value != null && !(value instanceof List)) {
return Collections.singletonList(
new ConfigurationError(
position,
"Property at '" + canonicalKey + "' requires a list of " + typeName + "s"));
}
if (value != null) {
List<Object> objs = (List<Object>) value;
for (int i = 0; i < objs.size(); ++i) {
Object obj = objs.get(i);
if (obj == null) {
return Collections.singletonList(
new ConfigurationError(position, "Value of property '" + canonicalKey + "', index " + i + ", is null"));
}
if (!innerClass.isInstance(obj)) {
return Collections.singletonList(
new ConfigurationError(
position,
"Value of property '" + canonicalKey + "', index " + i + ", is not a " + typeName));
}
}
}
if (validator == null || (defaultValue != null && value == null)) {
return Collections.emptyList();
}
return validator.validate(canonicalKey, position, (List<T>) value);
});
return this;
}
}