blob: a63e165b2a2f06aa844c51a24e2a75e36c284b13 [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 java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
/**
* A schema for a configuration, providing default values and validation rules.
*/
public final class Schema {
static final Schema EMPTY =
new Schema(Collections.emptyMap(), Collections.emptyMap(), ArrayListMultimap.create(), Collections.emptyList());
private final Map<String, String> propertyDescriptions;
private final Map<String, Object> propertyDefaults;
private final ListMultimap<String, PropertyValidator<Object>> propertyValidators;
private final List<ConfigurationValidator> configurationValidators;
Schema(
Map<String, String> propertyDescriptions,
Map<String, Object> propertyDefaults,
ListMultimap<String, PropertyValidator<Object>> propertyValidators,
List<ConfigurationValidator> configurationValidators) {
this.propertyDescriptions = propertyDescriptions;
this.propertyDefaults = propertyDefaults;
this.propertyValidators = propertyValidators;
this.configurationValidators = configurationValidators;
}
/**
* The keys of all defaults provided by this schema.
*
* @return The keys for all defaults provided by this schema.
*/
public Set<String> defaultsKeySet() {
return propertyDefaults.keySet();
}
/**
* Get the description for a key.
*
* @param key A configuration key (e.g. {@code "server.address.hostname"}).
* @return A description associated with the key, or null if no description is available.
*/
@Nullable
public String description(String key) {
requireNonNull(key);
return propertyDescriptions.get(key);
}
/**
* Check if a key has a default provided by this schema.
*
* @param key A configuration key (e.g. {@code "server.address.hostname"}).
* @return {@code true} if this schema provides a default value for the key.
*/
public boolean hasDefault(String key) {
requireNonNull(key);
return propertyDefaults.containsKey(key);
}
/**
* Get a default value from this configuration.
*
* @param key A configuration key (e.g. {@code "server.address.hostname"}).
* @return The value, or null if no default was available.
*/
public Object getDefault(String key) {
requireNonNull(key);
return propertyDefaults.get(key);
}
/**
* Get a default value from this configuration as a string.
*
* @param key A configuration key (e.g. {@code "server.address.hostname"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a string.
*/
@Nullable
public String getDefaultString(String key) {
requireNonNull(key);
return getTypedDefault(key, String.class, "string");
}
/**
* Get a default value from this configuration as a integer.
*
* @param key A configuration key (e.g. {@code "server.address.port"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not an integer.
*/
@Nullable
public Integer getDefaultInteger(String key) {
requireNonNull(key);
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (obj instanceof Integer) {
return (Integer) obj;
}
if (obj instanceof Long) {
Long longValue = (Long) obj;
if (longValue > Integer.MAX_VALUE) {
throw new InvalidConfigurationPropertyTypeException(
null,
"Value of property '" + key + "' is too large for an integer");
}
return longValue.intValue();
}
throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not an integer");
}
/**
* Get a default value from this configuration as a long.
*
* @param key A configuration key (e.g. {@code "server.address.port"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a long.
*/
@Nullable
public Long getDefaultLong(String key) {
requireNonNull(key);
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (obj instanceof Long) {
return (Long) obj;
}
if (obj instanceof Integer) {
return ((Integer) obj).longValue();
}
throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a long");
}
/**
* Get a default value from this configuration as a double.
*
* @param key A configuration key (e.g. {@code "server.priority"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a double.
*/
@Nullable
public Double getDefaultDouble(String key) {
requireNonNull(key);
return getTypedDefault(key, Double.class, "double");
}
/**
* Get a default value from this configuration as a boolean.
*
* @param key A configuration key (e.g. {@code "server.active"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a boolean.
*/
@Nullable
public Boolean getDefaultBoolean(String key) {
requireNonNull(key);
return getTypedDefault(key, Boolean.class, "boolean");
}
/**
* Get a default value from this configuration as a map.
*
* @param key A configuration key (e.g. {@code "server.active"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a map.
*/
@SuppressWarnings("unchecked")
@Nullable
public Map<String, Object> getDefaultMap(String key) {
requireNonNull(key);
return getTypedDefault(key, Map.class, "map");
}
/**
* Get a default value from this configuration as a list.
*
* @param key A configuration key (e.g. {@code "server.common_names"}).
* @return The value, or null if no default was available.
* @throws IllegalArgumentException If the key cannot be parsed.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list.
*/
@SuppressWarnings("unchecked")
@Nullable
public List<Object> getDefaultList(String key) {
requireNonNull(key);
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (obj instanceof List) {
return (List<Object>) obj;
}
throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' is not a list");
}
/**
* Get a default value from this configuration as a list of strings.
*
* @param key A configuration key (e.g. {@code "server.common_names"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list of strings.
*/
@Nullable
public List<String> getDefaultListOfString(String key) {
requireNonNull(key);
return getListDefault(key, String.class, "strings");
}
/**
* Get a default value from this configuration as a list of integers.
*
* @param key A configuration key (e.g. {@code "server.address.ports"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list of integers.
*/
@SuppressWarnings("unchecked")
@Nullable
public List<Integer> getDefaultListOfInteger(String key) {
requireNonNull(key);
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (obj instanceof List) {
List<?> list = (List<?>) obj;
if (list.isEmpty() || list.get(0) instanceof Integer) {
return (List<Integer>) list;
}
if (list.get(0) instanceof Long) {
return IntStream.range(0, list.size()).mapToObj(i -> {
Long longValue = (Long) list.get(i);
if (longValue == null) {
return null;
}
if (longValue > Integer.MAX_VALUE) {
throw new InvalidConfigurationPropertyTypeException(
null,
"Value of property '" + key + "', index " + i + ", is too large for an integer");
}
return longValue.intValue();
}).collect(Collectors.toList());
}
}
throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a list of integers");
}
/**
* Get a default value from this configuration as a list of longs.
*
* @param key A configuration key (e.g. {@code "server.address.ports"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list of longs.
*/
@SuppressWarnings("unchecked")
@Nullable
public List<Long> getDefaultListOfLong(String key) {
requireNonNull(key);
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (obj instanceof List) {
List<?> list = (List<?>) obj;
if (list.isEmpty() || list.get(0) instanceof Long) {
return (List<Long>) list;
}
if (list.get(0) instanceof Integer) {
return ((List<Integer>) list).stream().map(i -> {
if (i == null) {
return null;
}
return i.longValue();
}).collect(Collectors.toList());
}
}
throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a list of longs");
}
/**
* Get a default value from this configuration as a list of doubles.
*
* @param key A configuration key (e.g. {@code "server.priorities"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list of doubles.
*/
@Nullable
public List<Double> getDefaultListOfDouble(String key) {
requireNonNull(key);
return getListDefault(key, Double.class, "doubles");
}
/**
* Get a default value from this configuration as a list of booleans.
*
* @param key A configuration key (e.g. {@code "server.flags"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list of booleans.
*/
@Nullable
public List<Boolean> getDefaultListOfBoolean(String key) {
requireNonNull(key);
return getListDefault(key, Boolean.class, "booleans");
}
/**
* Get a default value from this configuration as a list of maps.
*
* @param key A configuration key (e.g. {@code "mainnet.servers"}).
* @return The value, or null if no default was available.
* @throws InvalidConfigurationPropertyTypeException If the default value is not a list of maps.
*/
@Nullable
public List<Map<String, Object>> getDefaultListOfMap(String key) {
requireNonNull(key);
return getListDefault(key, Map.class, "maps");
}
@Nullable
private <T> T getTypedDefault(String key, Class<T> clazz, String typeName) {
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (clazz.isInstance(obj)) {
return clazz.cast(obj);
}
throw new InvalidConfigurationPropertyTypeException(null, "Property at '" + key + "' was not a " + typeName);
}
@SuppressWarnings("unchecked")
@Nullable
private <T> List<T> getListDefault(String key, Class<?> listType, String typeName) {
Object obj = propertyDefaults.get(key);
if (obj == null) {
return null;
}
if (obj instanceof List) {
List<?> list = (List<?>) obj;
if (list.isEmpty() || listType.isInstance(list.get(0))) {
return (List<T>) list;
}
}
throw new InvalidConfigurationPropertyTypeException(
null,
"Property at '" + key + "' was not a list of " + typeName);
}
/**
* Validate a configuration against this schema.
*
* <p>
* The validations are done incrementally as the stream is consumed. Use {@code .limit(...)} on the stream to control
* the maximum number of validation errors to receive.
*
* @param configuration The configuration to validate.
* @return A stream containing any errors encountered during validation.
*/
public Stream<ConfigurationError> validate(Configuration configuration) {
requireNonNull(configuration);
Stream<ConfigurationError> propertyErrors = propertyValidators.entries().stream().flatMap(e -> {
String key = e.getKey();
PropertyValidator<Object> validator = e.getValue();
Object value = configuration.get(key);
DocumentPosition position = configuration.inputPositionOf(key);
List<ConfigurationError> errors = validator.validate(key, position, value);
if (errors == null) {
return Stream.empty();
}
return errors.stream();
});
Stream<ConfigurationError> configErrors = configurationValidators.stream().flatMap(v -> {
List<ConfigurationError> errors = v.validate(configuration);
if (errors == null) {
return Stream.empty();
}
return errors.stream();
});
return Stream.concat(propertyErrors, configErrors);
}
}