| /* |
| * 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; |
| } |
| } |