io.debezium.config.Configuration Maven / Gradle / Ivy
/*
* Copyright Debezium Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.config;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.time.Duration;
import java.time.temporal.TemporalUnit;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.kafka.common.config.ConfigValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.debezium.annotation.Immutable;
import io.debezium.config.Field.ValidationOutput;
import io.debezium.util.Collect;
import io.debezium.util.IoUtil;
import io.debezium.util.Strings;
/**
* An immutable representation of a Debezium configuration. A {@link Configuration} instance can be obtained
* {@link #from(Properties) from Properties} or loaded from a {@link #load(File) file}, {@link #load(InputStream) stream},
* {@link #load(Reader) reader}, {@link #load(URL) URL}, or {@link #load(String, ClassLoader) classpath resource}. They can
* also be built by first {@link #create() creating a builder} and then using that builder to populate and
* {@link Builder#build() return} the immutable Configuration instance.
*
* A Configuration object is basically a decorator around a {@link Properties} object. It has methods to get and convert
* individual property values to numeric, boolean and String types, optionally using a default value if the given property value
* does not exist. However, it is {@link Immutable immutable}, so it does not have any methods to set property values, allowing
* it to be passed around and reused without concern that other components might change the underlying property values.
*
* @author Randall Hauch
*/
@Immutable
public interface Configuration {
Logger CONFIGURATION_LOGGER = LoggerFactory.getLogger(Configuration.class);
Pattern PASSWORD_PATTERN = Pattern.compile(".*password$|.*sasl\\.jaas\\.config$", Pattern.CASE_INSENSITIVE);
/**
* The basic interface for configuration builders.
*
* @param the type of configuration
* @param the type of builder
*/
public static interface ConfigBuilder> {
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
B with(String key, String value);
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, Object value) {
return with(key, value != null ? value.toString() : null);
}
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, EnumeratedValue value) {
return with(key, value != null ? value.getValue() : null);
}
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, int value) {
return with(key, Integer.toString(value));
}
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, float value) {
return with(key, Float.toString(value));
}
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, double value) {
return with(key, Double.toString(value));
}
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, long value) {
return with(key, Long.toString(value));
}
/**
* Associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, boolean value) {
return with(key, Boolean.toString(value));
}
/**
* Associate the given class name value with the specified key.
*
* @param key the key
* @param value the Class value
* @return this builder object so methods can be chained together; never null
*/
default B with(String key, Class value) {
return with(key, value != null ? value.getName() : null);
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
B withDefault(String key, String value);
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, Object value) {
return withDefault(key, value != null ? value.toString() : null);
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, EnumeratedValue value) {
return withDefault(key, value != null ? value.getValue() : null);
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, int value) {
return withDefault(key, Integer.toString(value));
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, float value) {
return withDefault(key, Float.toString(value));
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, double value) {
return withDefault(key, Double.toString(value));
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, long value) {
return withDefault(key, Long.toString(value));
}
/**
* If there is no field with the specified key, then associate the given value with the specified key.
*
* @param key the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, boolean value) {
return withDefault(key, Boolean.toString(value));
}
/**
* If there is no field with the specified key, then associate the given class name value with the specified key.
*
* @param key the key
* @param value the Class value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(String key, Class value) {
return withDefault(key, value != null ? value.getName() : null);
}
/**
* If any of the fields in the supplied Configuration object do not exist, then add them.
*
* @param other the configuration whose fields should be added; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Configuration other) {
return apply(builder -> other.forEach(builder::withDefault));
}
/**
* Add all of the fields in the supplied Configuration object.
*
* @param other the configuration whose fields should be added; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B with(Configuration other) {
return apply(builder -> other.forEach(builder::with));
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, String value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, Object value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, EnumeratedValue value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, int value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, float value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, double value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, long value) {
return with(field.name(), value);
}
/**
* Associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, boolean value) {
return with(field.name(), value);
}
/**
* Associate the given class name value with the specified field.
*
* @param field the predefined field for the key
* @param value the Class value
* @return this builder object so methods can be chained together; never null
*/
default B with(Field field, Class value) {
return with(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, String value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, Object value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, int value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, float value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, double value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, long value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the default value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, boolean value) {
return withDefault(field.name(), value);
}
/**
* If the field does not have a value, then associate the given value with the key of the specified field.
*
* @param field the predefined field for the key
* @param value the default value
* @return this builder object so methods can be chained together; never null
*/
default B withDefault(Field field, Class value) {
return withDefault(field.name(), value != null ? value.getName() : null);
}
/**
* Apply the function to this builder.
*
* @param function the predefined field for the key
* @return this builder object so methods can be chained together; never null
*/
B apply(Consumer function);
/**
* Apply the function to this builder to change a potentially existing boolean field.
*
* @param key the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
* @throws NumberFormatException if the existing value is not a boolean
*/
default B changeBoolean(String key, Function function) {
Function strFunction = (existingStr) -> {
Boolean result = function.apply(existingStr != null ? Boolean.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(key, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing string field.
*
* @param key the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
B changeString(String key, Function function);
/**
* Apply the function to this builder to change a potentially existing double field.
*
* @param key the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
* @throws NumberFormatException if the existing value is not a double
*/
default B changeDouble(String key, Function function) {
Function strFunction = (existingStr) -> {
Double result = function.apply(existingStr != null ? Double.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(key, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing float field.
*
* @param key the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
* @throws NumberFormatException if the existing value is not a float
*/
default B changeFloat(String key, Function function) {
Function strFunction = (existingStr) -> {
Float result = function.apply(existingStr != null ? Float.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(key, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing long field.
*
* @param key the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
* @throws NumberFormatException if the existing value is not a long
*/
default B changeLong(String key, Function function) {
Function strFunction = (existingStr) -> {
Long result = function.apply(existingStr != null ? Long.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(key, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing integer field.
*
* @param key the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
* @throws NumberFormatException if the existing value is not an integer
*/
default B changeInteger(String key, Function function) {
Function strFunction = (existingStr) -> {
Integer result = function.apply(existingStr != null ? Integer.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(key, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing boolean field.
*
* @param field the predefined field for the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B changeBoolean(Field field, Function function) {
Function strFunction = (existingStr) -> {
Boolean result = function.apply(existingStr != null ? Boolean.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(field, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing string field.
*
* @param field the predefined field for the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
B changeString(Field field, Function function);
/**
* Apply the function to this builder to change a potentially existing double field.
*
* @param field the predefined field for the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B changeDouble(Field field, Function function) {
Function strFunction = (existingStr) -> {
Double result = function.apply(existingStr != null ? Double.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(field, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing float field.
*
* @param field the predefined field for the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B changeFloat(Field field, Function function) {
Function strFunction = (existingStr) -> {
Float result = function.apply(existingStr != null ? Float.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(field, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing long field.
*
* @param field the predefined field for the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B changeLong(Field field, Function function) {
Function strFunction = (existingStr) -> {
Long result = function.apply(existingStr != null ? Long.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(field, strFunction);
}
/**
* Apply the function to this builder to change a potentially existing integer field.
*
* @param field the predefined field for the key
* @param function the function that computes the new value given a possibly-existing value; may not be null
* @return this builder object so methods can be chained together; never null
*/
default B changeInteger(Field field, Function function) {
Function strFunction = (existingStr) -> {
Integer result = function.apply(existingStr != null ? Integer.valueOf(existingStr) : null);
return result != null ? result.toString() : null;
};
return changeString(field, strFunction);
}
/**
* Build and return the immutable configuration.
*
* @return the immutable configuration; never null
*/
C build();
}
/**
* A builder of Configuration objects.
*/
public static class Builder implements ConfigBuilder {
private final Properties props = new Properties();
protected Builder() {
}
protected Builder(Properties props) {
this.props.putAll(props);
}
@Override
public Builder with(String key, String value) {
props.setProperty(key, value);
return this;
}
@Override
public Builder withDefault(String key, String value) {
if (!props.containsKey(key)) {
props.setProperty(key, value);
}
return this;
}
@Override
public Builder apply(Consumer function) {
function.accept(this);
return this;
}
@Override
public Builder changeString(String key, Function function) {
return changeString(key, null, function);
}
@Override
public Builder changeString(Field field, Function function) {
return changeString(field.name(), field.defaultValueAsString(), function);
}
protected Builder changeString(String key, String defaultValue, Function function) {
String existing = props.getProperty(key);
if (existing == null) {
existing = defaultValue;
}
String newValue = function.apply(existing);
return with(key, newValue);
}
@Override
public Configuration build() {
return Configuration.from(props);
}
}
/**
* Create a new {@link Builder configuration builder}.
*
* @return the configuration builder
*/
public static Builder create() {
return new Builder();
}
/**
* Create a new {@link Builder configuration builder} that starts with a copy of the supplied configuration.
*
* @param config the configuration to copy; may be null
* @return the configuration builder
*/
public static Builder copy(Configuration config) {
return config != null ? new Builder(config.asProperties()) : new Builder();
}
/**
* Create a Configuration object that is populated by system properties, per {@link #withSystemProperties(String)}.
*
* @param prefix the required prefix for the system properties; may not be null but may be empty
* @return the configuration
*/
public static Configuration fromSystemProperties(String prefix) {
return empty().withSystemProperties(prefix);
}
/**
* Obtain an empty configuration.
*
* @return an empty configuration; never null
*/
public static Configuration empty() {
return new Configuration() {
@Override
public Set keys() {
return Collections.emptySet();
}
@Override
public String getString(String key) {
return null;
}
@Override
public String toString() {
return "{}";
}
};
}
/**
* Obtain a configuration instance by copying the supplied Properties object. The supplied {@link Properties} object is
* copied so that the resulting Configuration cannot be modified.
*
* @param properties the properties; may be null or empty
* @return the configuration; never null
*/
public static Configuration from(Properties properties) {
Properties props = new Properties();
if (properties != null) {
props.putAll(properties);
}
return new Configuration() {
@Override
public String getString(String key) {
return props.getProperty(key);
}
@Override
public Set keys() {
return props.stringPropertyNames();
}
@Override
public String toString() {
return withMaskedPasswords().asProperties().toString();
}
};
}
/**
* Obtain a configuration instance by copying the supplied map of string keys and object values. The entries within the map
* are copied so that the resulting Configuration cannot be modified.
*
* @param properties the properties; may be null or empty
* @return the configuration; never null
*/
public static Configuration from(Map properties) {
return from(properties, value -> {
if (value == null) {
return null;
}
if (value instanceof Collection) {
return Strings.join(",", (List) value);
}
return value.toString();
});
}
/**
* Obtain a configuration instance by copying the supplied map of string keys and object values. The entries within the map
* are copied so that the resulting Configuration cannot be modified.
*
* @param properties the properties; may be null or empty
* @param conversion the function that converts the supplied values into strings, or returns {@code null} if the value
* is to be excluded
* @return the configuration; never null
*/
public static Configuration from(Map properties, Function conversion) {
Map props = new HashMap<>();
if (properties != null) {
props.putAll(properties);
}
return new Configuration() {
@Override
public String getString(String key) {
return conversion.apply((T) props.get(key));
}
@Override
public Set keys() {
return props.keySet();
}
@Override
public String toString() {
return withMaskedPasswords().asProperties().toString();
}
};
}
/**
* Obtain a configuration instance by loading the Properties from the supplied URL.
*
* @param url the URL to the stream containing the configuration properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(URL url) throws IOException {
try (InputStream stream = url.openStream()) {
return load(stream);
}
}
/**
* Obtain a configuration instance by loading the Properties from the supplied file.
*
* @param file the file containing the configuration properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(File file) throws IOException {
try (InputStream stream = new FileInputStream(file)) {
return load(stream);
}
}
/**
* Obtain a configuration instance by loading the Properties from the supplied stream.
*
* @param stream the stream containing the properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(InputStream stream) throws IOException {
try {
Properties properties = new Properties();
properties.load(stream);
return from(properties);
}
finally {
stream.close();
}
}
/**
* Obtain a configuration instance by loading the Properties from the supplied reader.
*
* @param reader the reader containing the properties; may not be null
* @return the configuration; never null
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(Reader reader) throws IOException {
try {
Properties properties = new Properties();
properties.load(reader);
return from(properties);
}
finally {
reader.close();
}
}
/**
* Obtain a configuration instance by loading the Properties from a file on the file system or classpath given by the supplied
* path.
*
* @param path the path to the file containing the configuration properties; may not be null
* @param clazz the class whose classpath is to be used to find the file; may be null
* @return the configuration; never null but possibly empty
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(String path, Class clazz) throws IOException {
return load(path, clazz.getClassLoader());
}
/**
* Obtain a configuration instance by loading the Properties from a file on the file system or classpath given by the supplied
* path.
*
* @param path the path to the file containing the configuration properties; may not be null
* @param classLoader the class loader to use; may be null
* @return the configuration; never null but possibly empty
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(String path, ClassLoader classLoader) throws IOException {
Logger logger = LoggerFactory.getLogger(Configuration.class);
return load(path, classLoader, logger::debug);
}
/**
* Obtain a configuration instance by loading the Properties from a file on the file system or classpath given by the supplied
* path.
*
* @param path the path to the file containing the configuration properties; may not be null
* @param classLoader the class loader to use; may be null
* @param logger the function that will be called with status updates; may be null
* @return the configuration; never null but possibly empty
* @throws IOException if there is an error reading the stream
*/
public static Configuration load(String path, ClassLoader classLoader, Consumer logger) throws IOException {
try (InputStream stream = IoUtil.getResourceAsStream(path, classLoader, null, null, logger)) {
Properties props = new Properties();
if (stream != null) {
props.load(stream);
}
return from(props);
}
}
/**
* Obtain an editor for a copy of this configuration.
*
* @return a builder that is populated with this configuration's key-value pairs; never null
*/
default Builder edit() {
return copy(this);
}
/**
* Determine whether this configuration contains a key-value pair with the given key and the value is non-null
*
* @param key the key
* @return true if the configuration contains the key, or false otherwise
*/
default boolean hasKey(String key) {
return getString(key) != null;
}
/**
* Get the set of keys in this configuration.
*
* @return the set of keys; never null but possibly empty
*/
public Set keys();
/**
* Get the string value associated with the given key.
*
* @param key the key for the configuration property
* @return the value, or null if the key is null or there is no such key-value pair in the configuration
*/
public String getString(String key);
/**
* Get the string value associated with the given key, returning the default value if there is no such key-value pair.
*
* @param key the key for the configuration property
* @param defaultValue the value that should be returned by default if there is no such key-value pair in the configuration;
* may be null
* @return the configuration value, or the {@code defaultValue} if there is no such key-value pair in the configuration
*/
default String getString(String key, String defaultValue) {
return getString(key, () -> defaultValue);
}
/**
* Get the string value associated with the given key, returning the default value if there is no such key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier of value that should be returned by default if there is no such key-value pair in
* the configuration; may be null and may return null
* @return the configuration value, or the {@code defaultValue} if there is no such key-value pair in the configuration
*/
default String getString(String key, Supplier defaultValueSupplier) {
String value = getString(key);
return value != null ? value : (defaultValueSupplier != null ? defaultValueSupplier.get() : null);
}
/**
* Get the string value associated with the given field, returning the field's default value if there is no such key-value
* pair in this configuration.
*
* @param field the field; may not be null
* @return the configuration's value for the field, or the field's {@link Field#defaultValue() default value} if there is no
* such key-value pair in the configuration
*/
default String getString(Field field) {
return getString(field.name(), field.defaultValueAsString());
}
/**
* Get the string value associated with the given field, returning the field's default value if there is no such key-value
* pair in this configuration.
*
* @param field the field; may not be null
* @param defaultValue the default value
* @return the configuration's value for the field, or the field's {@link Field#defaultValue() default value} if there is no
* such key-value pair in the configuration
*/
default String getString(Field field, String defaultValue) {
return getString(field.name(), () -> {
String value = field.defaultValueAsString();
return value != null ? value : defaultValue;
});
}
/**
* Get the string value(s) associated with the given key, where the supplied regular expression is used to parse the single
* string value into multiple values.
*
* @param field the field; may not be null
* @param regex the delimiting regular expression
* @return the list of string values; null only if there is no such key-value pair in the configuration
* @see String#split(String)
*/
default List getStrings(Field field, String regex) {
return getStrings(field.name(), regex);
}
/**
* Get the string value(s) associated with the given key, where the supplied regular expression is used to parse the single
* string value into multiple values.
*
* @param key the key for the configuration property
* @param regex the delimiting regular expression
* @return the list of string values; null only if there is no such key-value pair in the configuration
* @see String#split(String)
*/
default List getStrings(String key, String regex) {
String value = getString(key);
if (value == null) {
return null;
}
return Collect.arrayListOf(value.split(regex));
}
/**
* Get the string value(s) associated with the given key, where the supplied regular expression is used to parse the single
* string value into multiple values. In addition, all values will be trimmed.
*
* @param field the field for the configuration property
* @param regex the delimiting regular expression
* @return the list of string values; null only if there is no such key-value pair in the configuration
* @see String#split(String)
*/
default List getTrimmedStrings(Field field, String regex) {
String value = getString(field);
if (value == null) {
return null;
}
return Arrays.stream(value.split(regex))
.map(String::trim)
.collect(Collectors.toList());
}
/**
* Get the integer value associated with the given key.
*
* @param key the key for the configuration property
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as an integer
*/
default Integer getInteger(String key) {
return getInteger(key, null);
}
/**
* Get the long value associated with the given key.
*
* @param key the key for the configuration property
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as an integer
*/
default Long getLong(String key) {
return getLong(key, null);
}
/**
* Get the boolean value associated with the given key.
*
* @param key the key for the configuration property
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as a boolean value
*/
default Boolean getBoolean(String key) {
return getBoolean(key, null);
}
/**
* Get the integer value associated with the given key, returning the default value if there is no such key-value pair or
* if the value could not be {@link Integer#parseInt(String) parsed} as an integer.
*
* @param key the key for the configuration property
* @param defaultValue the default value
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as an integer
*/
default int getInteger(String key, int defaultValue) {
return getInteger(key, () -> defaultValue).intValue();
}
/**
* Get the long value associated with the given key, returning the default value if there is no such key-value pair or
* if the value could not be {@link Long#parseLong(String) parsed} as a long.
*
* @param key the key for the configuration property
* @param defaultValue the default value
* @return the long value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as a long
*/
default long getLong(String key, long defaultValue) {
return getLong(key, () -> defaultValue).longValue();
}
/**
* Get the boolean value associated with the given key, returning the default value if there is no such key-value pair or
* if the value could not be {@link Boolean#parseBoolean(String) parsed} as a boolean value.
*
* @param key the key for the configuration property
* @param defaultValue the default value
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, or the value
* could not be parsed as a boolean value
*/
default boolean getBoolean(String key, boolean defaultValue) {
return getBoolean(key, () -> defaultValue).booleanValue();
}
/**
* Get the integer value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as an integer
*/
default Number getNumber(String key, Supplier defaultValueSupplier) {
String value = getString(key);
return Strings.asNumber(value, defaultValueSupplier);
}
/**
* Get the integer value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as an integer
*/
default Integer getInteger(String key, IntSupplier defaultValueSupplier) {
String value = getString(key);
if (value != null) {
try {
return Integer.valueOf(value);
}
catch (NumberFormatException e) {
}
}
return defaultValueSupplier != null ? defaultValueSupplier.getAsInt() : null;
}
/**
* Get the long value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the long value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as a long
*/
default Long getLong(String key, LongSupplier defaultValueSupplier) {
String value = getString(key);
if (value != null) {
try {
return Long.valueOf(value);
}
catch (NumberFormatException e) {
}
}
return defaultValueSupplier != null ? defaultValueSupplier.getAsLong() : null;
}
/**
* Get the boolean value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param key the key for the configuration property
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as a boolean value
*/
default Boolean getBoolean(String key, BooleanSupplier defaultValueSupplier) {
String value = getString(key);
if (value != null) {
value = value.trim().toLowerCase();
if (Boolean.valueOf(value)) {
return Boolean.TRUE;
}
if (value.equals("false")) {
return false;
}
}
return defaultValueSupplier != null ? defaultValueSupplier.getAsBoolean() : null;
}
/**
* Get the numeric value associated with the given field, returning the field's default value if there is no such
* key-value pair.
*
* @param field the field
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as an integer value
* @throws NumberFormatException if there is no name-value pair and the field has no default value
*/
default Number getNumber(Field field) {
return getNumber(field.name(), () -> Strings.asNumber(field.defaultValueAsString()));
}
/**
* Get the integer value associated with the given field, returning the field's default value if there is no such
* key-value pair.
*
* @param field the field
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as an integer value
* @throws NumberFormatException if there is no name-value pair and the field has no default value
*/
default int getInteger(Field field) {
return getInteger(field.name(), () -> Integer.valueOf(field.defaultValueAsString())).intValue();
}
/**
* Get the long value associated with the given field, returning the field's default value if there is no such
* key-value pair.
*
* @param field the field
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as a long value
* @throws NumberFormatException if there is no name-value pair and the field has no default value
*/
default long getLong(Field field) {
return getLong(field.name(), () -> Long.valueOf(field.defaultValueAsString())).longValue();
}
/**
* Get the boolean value associated with the given field when that field has a default value. If the configuration does
* not have a name-value pair with the same name as the field, then the field's default value.
*
* @param field the field
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as a boolean value
* @throws NumberFormatException if there is no name-value pair and the field has no default value
*/
default boolean getBoolean(Field field) {
return getBoolean(field.name(), () -> Boolean.valueOf(field.defaultValueAsString())).booleanValue();
}
/**
* Get the integer value associated with the given field, returning the field's default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValue the default value
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as an integer value
*/
default int getInteger(Field field, int defaultValue) {
return getInteger(field.name(), defaultValue);
}
/**
* Get the long value associated with the given field, returning the field's default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValue the default value
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as a long value
*/
default long getLong(Field field, long defaultValue) {
return getLong(field.name(), defaultValue);
}
/**
* Get the boolean value associated with the given field, returning the field's default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValue the default value
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration and there is
* no default value in the field or the default value could not be parsed as a long, or there is a key-value pair in
* the configuration but the value could not be parsed as a boolean value
*/
default boolean getBoolean(Field field, boolean defaultValue) {
return getBoolean(field.name(), defaultValue);
}
/**
* Get the integer value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the integer value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as an integer
*/
default Integer getInteger(Field field, IntSupplier defaultValueSupplier) {
return getInteger(field.name(), defaultValueSupplier);
}
/**
* Get the long value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the long value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as a long
*/
default Long getLong(Field field, LongSupplier defaultValueSupplier) {
return getLong(field.name(), defaultValueSupplier);
}
/**
* Get the boolean value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValueSupplier the supplier for the default value; may be null
* @return the boolean value, or null if the key is null, there is no such key-value pair in the configuration, the
* {@code defaultValueSupplier} reference is null, or there is a key-value pair in the configuration but the value
* could not be parsed as a boolean value
*/
default Boolean getBoolean(Field field, BooleanSupplier defaultValueSupplier) {
return getBoolean(field.name(), defaultValueSupplier);
}
/**
* Get the boolean value associated with the given key, using the given supplier to obtain a default value if there is no such
* key-value pair.
*
* @param field the field
* @param defaultValueSupplier the supplier of value that should be returned by default if there is no such key-value pair in
* the configuration; may be null and may return null
* @return the configuration value, or the {@code defaultValue} if there is no such key-value pair in the configuration
*/
default String getString(Field field, Supplier defaultValueSupplier) {
return getString(field.name(), defaultValueSupplier);
}
/**
*
* Gets the duration value associated with the given key.
*
* @param field the field
* @param unit the temporal unit of the duration value
* @return the duration value associated with the given key
*/
default Duration getDuration(Field field, TemporalUnit unit) {
return Duration.of(getLong(field), unit);
}
/**
* Get an instance of the class given by the value in the configuration associated with the given key.
*
* @param key the key for the configuration property
* @param type the Class of which the resulting object is expected to be an instance of; may not be null
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default T getInstance(String key, Class type) {
return getInstance(key, type, () -> getClass().getClassLoader());
}
/**
* Get an instance of the class given by the value in the configuration associated with the given key.
*
* @param key the key for the configuration property
* @param type the Class of which the resulting object is expected to be an instance of; may not be null
* @param classloaderSupplier the supplier of the ClassLoader to be used to load the resulting class; may be null if this
* class' ClassLoader should be used
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default T getInstance(String key, Class type, Supplier classloaderSupplier) {
return Instantiator.getInstance(getString(key), classloaderSupplier, null);
}
/**
* Get an instance of the class given by the value in the configuration associated with the given key.
* The instance is created using {@code Instance(Configuration)} constructor.
*
* @param key the key for the configuration property
* @param clazz the Class of which the resulting object is expected to be an instance of; may not be null
* @param configuration {@link Configuration} object that is passed as a parameter to the constructor
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default T getInstance(String key, Class clazz, Configuration configuration) {
return Instantiator.getInstance(getString(key), () -> getClass().getClassLoader(), configuration);
}
/**
* Get an instance of the class given by the value in the configuration associated with the given field.
*
* @param field the field for the configuration property
* @param clazz the Class of which the resulting object is expected to be an instance of; may not be null
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default T getInstance(Field field, Class clazz) {
return getInstance(field, clazz, () -> getClass().getClassLoader());
}
/**
* Get an instance of the class given by the value in the configuration associated with the given field.
*
* @param field the field for the configuration property
* @param type the Class of which the resulting object is expected to be an instance of; may not be null
* @param classloaderSupplier the supplier of the ClassLoader to be used to load the resulting class; may be null if this
* class' ClassLoader should be used
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default T getInstance(Field field, Class type, Supplier classloaderSupplier) {
return Instantiator.getInstance(getString(field), classloaderSupplier, null);
}
/**
* Get an instance of the class given by the value in the configuration associated with the given field.
* The instance is created using {@code Instance(Configuration)} constructor.
*
* @param field the field for the configuration property
* @param clazz the Class of which the resulting object is expected to be an instance of; may not be null
* @param configuration the {@link Configuration} object that is passed as a parameter to the constructor
* @return the new instance, or null if there is no such key-value pair in the configuration or if there is a key-value
* configuration but the value could not be converted to an existing class with a zero-argument constructor
*/
default T getInstance(Field field, Class clazz, Configuration configuration) {
return Instantiator.getInstance(getString(field), () -> getClass().getClassLoader(), configuration);
}
/**
* Return a new {@link Configuration} that contains only the subset of keys that match the given prefix.
* If desired, the keys in the resulting Configuration will have the prefix (plus any terminating "{@code .}" character if
* needed) removed.
*
* This method returns this Configuration instance if the supplied {@code prefix} is null or empty.
*
* @param prefix the prefix
* @param removePrefix true if the prefix (and any subsequent "{@code .}" character) should be removed from the keys in the
* resulting Configuration, or false if the keys in this Configuration should be used as-is in the resulting
* Configuration
* @return the subset of this Configuration; never null
*/
default Configuration subset(String prefix, boolean removePrefix) {
if (prefix == null) {
return this;
}
prefix = prefix.trim();
if (prefix.isEmpty()) {
return this;
}
String prefixWithSeparator = prefix.endsWith(".") ? prefix : prefix + ".";
int minLength = prefixWithSeparator.length();
Function prefixRemover = removePrefix ? key -> key.substring(minLength) : key -> key;
return filter(key -> key != null && key.startsWith(prefixWithSeparator)).map(prefixRemover);
}
/**
* Return a new {@link Configuration} that contains only the subset of keys that satisfy the given predicate.
*
* @param mapper that function that transforms keys
* @return the subset Configuration; never null
*/
default Configuration map(Function mapper) {
if (mapper == null) {
return this;
}
Map newToOld = new HashMap<>();
keys().stream().filter(k -> k != null).forEach(oldKey -> {
String newKey = mapper.apply(oldKey);
if (newKey != null) {
newToOld.put(newKey, oldKey);
}
});
return new Configuration() {
@Override
public Set keys() {
return Collect.unmodifiableSet(newToOld.keySet());
}
@Override
public String getString(String key) {
String oldKey = newToOld.get(key);
return Configuration.this.getString(oldKey);
}
@Override
public String toString() {
return withMaskedPasswords().asProperties().toString();
}
};
}
/**
* Return a new {@link Configuration} that contains only the subset of keys that satisfy the given predicate.
*
* @param matcher the function that determines whether a key should be included in the subset
* @return the subset Configuration; never null
*/
default Configuration filter(Predicate matcher) {
if (matcher == null) {
return this;
}
return new Configuration() {
@Override
public Set keys() {
return Collect.unmodifiableSet(Configuration.this.keys().stream()
.filter(k -> k != null)
.filter(matcher)
.collect(Collectors.toSet()));
}
@Override
public String getString(String key) {
return matcher.test(key) ? Configuration.this.getString(key) : null;
}
@Override
public String toString() {
return withMaskedPasswords().asProperties().toString();
}
};
}
/**
* Return a new {@link Configuration} that contains the mapped values.
*
* @param mapper the function that takes a key and value and returns the new mapped value
* @return the Configuration with mapped values; never null
*/
default Configuration mapped(BiFunction mapper) {
if (mapper == null) {
return this;
}
return new Configuration() {
@Override
public Set keys() {
return Configuration.this.keys();
}
@Override
public String getString(String key) {
return mapper.apply(key, Configuration.this.getString(key));
}
@Override
public String toString() {
return withMaskedPasswords().asProperties().toString();
}
};
}
/**
* Return a new {@link Configuration} that contains all of the same fields as this configuration, except with all
* variables in the fields replaced with values from the supplied function. Variables found in fields will be left as-is
* if the supplied function returns no value for the variable name(s).
*
* Variables may appear anywhere within a field value, and multiple variables can be used within the same field. Variables
* take the form:
*
*
* variable := '${' variableNames [ ':' defaultValue ] '}'
* variableNames := variableName [ ',' variableNames ]
* variableName := // any characters except ',' and ':' and '}'
* defaultValue := // any characters except '}'
*
*
* and examples of variables include:
*
* ${var1}
* ${var1:defaultValue}
* ${var1,var2}
* ${var1,var2:defaultValue}
*
*
* The variableName literal is the name used to look up a the property.
*
* This syntax supports multiple variables. The logic will process the variables from let to right,
* until an existing property is found. And at that point, it will stop and will not attempt to find values for the other
* variables.
*
*
*
* @param valuesByVariableName the function that returns a variable value for a variable name; may not be null but may
* return null if the variable name is not known
* @return the Configuration with masked values for matching keys; never null
*/
default Configuration withReplacedVariables(Function valuesByVariableName) {
return mapped((key, value) -> {
return Strings.replaceVariables(value, valuesByVariableName);
});
}
/**
* Return a new {@link Configuration} that contains all of the same fields as this configuration, except with masked values
* for all keys that end in "password".
*
* @return the Configuration with masked values for matching keys; never null
*/
default Configuration withMaskedPasswords() {
return withMasked(PASSWORD_PATTERN);
}
/**
* Return a new {@link Configuration} that contains all of the same fields as this configuration, except with masked values
* for all keys that match the specified pattern.
*
* @param keyRegex the regular expression to match against the keys
* @return the Configuration with masked values for matching keys; never null
*/
default Configuration withMasked(String keyRegex) {
if (keyRegex == null) {
return this;
}
return withMasked(Pattern.compile(keyRegex));
}
/**
* Return a new {@link Configuration} that contains all of the same fields as this configuration, except with masked values
* for all keys that match the specified pattern.
*
* @param keyRegex the regular expression to match against the keys
* @return the Configuration with masked values for matching keys; never null
*/
default Configuration withMasked(Pattern keyRegex) {
if (keyRegex == null) {
return this;
}
return new Configuration() {
@Override
public Set keys() {
return Configuration.this.keys();
}
@Override
public String getString(String key) {
boolean matches = keyRegex.matcher(key).matches();
return matches ? "********" : Configuration.this.getString(key);
}
@Override
public String toString() {
return withMaskedPasswords().asProperties().toString();
}
};
}
/**
* Determine if this configuration is empty and has no properties.
*
* @return {@code true} if empty, or {@code false} otherwise
*/
default boolean isEmpty() {
return keys().isEmpty();
}
/**
* Get a copy of these configuration properties as a Properties object.
*
* @return the properties object; never null
*/
default Properties asProperties() {
return asProperties(null);
}
/**
* Get a copy of these configuration properties as a Properties object.
*
* @param fields the fields defining the defaults; may be null
* @return the properties object; never null
*/
default Properties asProperties(Field.Set fields) {
Properties props = new Properties();
// Add all values as-is ...
keys().forEach(key -> {
String value = getString(key);
if (key != null && value != null) {
props.setProperty(key, value);
}
});
if (fields != null) {
// Add the default values ...
fields.forEach(field -> {
props.put(field.name(), getString(field));
});
}
return props;
}
/**
* Get a copy of these configuration properties as a Properties object.
*
* @return the properties object; never null
*/
default Map asMap() {
return asMap(null);
}
/**
* Get a copy of these configuration properties with defaults as a Map.
*
* @param fields the fields defining the defaults; may be null
* @return the properties object; never null
*/
default Map asMap(Field.Set fields) {
Map props = new HashMap<>();
// Add all values as-is ...
keys().forEach(key -> {
String value = getString(key);
if (key != null && value != null) {
props.put(key, value);
}
});
if (fields != null) {
// Add the default values ...
fields.forEach(field -> {
props.put(field.name(), getString(field));
});
}
return props;
}
/**
* Return a copy of this configuration except where acceptable system properties are used to overwrite properties copied from
* this configuration. All system properties whose name has the given prefix are added, where the prefix is removed from the
* system property name, it is converted to lower case, and each underscore character ('{@code _}') are replaced with a
* period ('{@code .}').
*
* @param prefix the required prefix for the system properties
* @return the resulting properties converted from the system properties; never null, but possibly empty
*/
default Configuration withSystemProperties(String prefix) {
int prefixLength = prefix.length();
return withSystemProperties(input -> {
if (input.startsWith(prefix)) {
String withoutPrefix = input.substring(prefixLength).trim();
if (withoutPrefix.length() > 0) {
// Convert to a properties format ...
return withoutPrefix.toLowerCase().replaceAll("[_]", ".");
}
}
return null;
});
}
/**
* Return a copy of this configuration except where acceptable system properties are used to overwrite properties copied from
* this configuration. Each of the system properties is examined and passed to the supplied function; if the result of the
* function is a non-null string, then a property with that string as the name and the system property value are added to the
* returned configuration.
*
* @param propertyNameConverter the function that will convert the name of each system property to an applicable property name
* (or null if the system property name does not apply); may not be null
* @return the resulting properties filtered from the input properties; never null, but possibly empty
*/
default Configuration withSystemProperties(Function propertyNameConverter) {
Properties props = asProperties();
Properties systemProperties = System.getProperties();
for (String key : systemProperties.stringPropertyNames()) {
String propName = propertyNameConverter.apply(key);
if (propName != null && propName.length() > 0) {
String value = systemProperties.getProperty(key);
props.setProperty(propName, value);
}
}
return from(props);
}
/**
* Validate the supplied fields in this configuration. Extra fields not described by the supplied {@code fields} parameter
* are not validated.
*
* @param fields the fields
* @param problems the consumer to be called with each problem; never null
* @return {@code true} if the value is considered valid, or {@code false} if it is not valid
*/
default boolean validate(Iterable fields, ValidationOutput problems) {
boolean valid = true;
for (Field field : fields) {
if (!field.validate(this, problems)) {
valid = false;
}
}
return valid;
}
/**
* Validate the supplied fields in this configuration. Extra fields not described by the supplied {@code fields} parameter
* are not validated.
*
* @param fields the fields
* @param problems the consumer to be called with each problem; never null
* @return {@code true} if the value is considered valid, or {@code false} if it is not valid
*/
default boolean validateAndRecord(Iterable fields, Consumer problems) {
return validate(fields, (f, v, problem) -> {
if (v == null) {
problems.accept("The '" + f.name() + "' value is invalid: " + problem);
}
else {
String valueStr = v.toString();
if (v instanceof CharSequence) {
if (PASSWORD_PATTERN.matcher((CharSequence) v).matches()) {
valueStr = "********"; // mask any fields that we know are passwords
}
else {
valueStr = "'" + valueStr + "'";
}
}
problems.accept("The '" + f.name() + "' value " + valueStr + " is invalid: " + problem);
}
});
}
/**
* Validate the supplied fields in this configuration. Extra fields not described by the supplied {@code fields} parameter
* are not validated.
*
* @param fields the fields
* @return the {@link ConfigValue} for each of the fields; never null
*/
default Map validate(Field.Set fields) {
// Create a map of configuration values for each field ...
Map configValuesByFieldName = new HashMap<>();
fields.forEach(field -> {
configValuesByFieldName.put(field.name(), new ConfigValue(field.name()));
});
// If any dependents don't exist ...
fields.forEachMissingDependent(missingDepedent -> {
ConfigValue undefinedConfigValue = new ConfigValue(missingDepedent);
undefinedConfigValue.addErrorMessage(missingDepedent + " is referred in the dependents, but not defined.");
undefinedConfigValue.visible(false);
configValuesByFieldName.put(missingDepedent, undefinedConfigValue);
});
// Now validate each top-level field ...
fields.forEachTopLevelField(field -> {
field.validate(this, fields::fieldWithName, configValuesByFieldName);
});
return configValuesByFieldName;
}
/**
* Apply the given function to all fields whose names match the given regular expression.
*
* @param regex the regular expression string; may not be null
* @param function the consumer that takes the name and value of matching fields; may not be null
*/
default void forEachMatchingFieldName(String regex, BiConsumer function) {
forEachMatchingFieldName(Pattern.compile(regex), function);
}
/**
* Apply the given function to all fields whose names match the given regular expression.
*
* @param regex the regular expression string; may not be null
* @param function the consumer that takes the name and value of matching fields; may not be null
*/
default void forEachMatchingFieldName(Pattern regex, BiConsumer function) {
this.asMap().forEach((fieldName, fieldValue) -> {
if (regex.matcher(fieldName).matches()) {
function.accept(fieldName, fieldValue);
}
});
}
/**
* For all fields whose names match the given regular expression, extract an integer from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param function the consumer that takes the value of matching field and the integer extracted from the field name; may not
* be null
*/
default void forEachMatchingFieldNameWithInteger(String regex, BiConsumer function) {
forEachMatchingFieldNameWithInteger(Pattern.compile(regex), 1, function);
}
/**
* For all fields whose names match the given regular expression, extract an integer from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the integer to be extracted; must be positive
* @param function the consumer that takes the value of matching field and the integer extracted from the field name; may not
* be null
*/
default void forEachMatchingFieldNameWithInteger(String regex, int groupNumber, BiConsumer function) {
forEachMatchingFieldNameWithInteger(Pattern.compile(regex), groupNumber, function);
}
/**
* For all fields whose names match the given regular expression, extract an integer from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the integer to be extracted; must be positive
* @param function the consumer that takes the value of matching field and the integer extracted from the field name; may not
* be null
*/
default void forEachMatchingFieldNameWithInteger(Pattern regex, int groupNumber, BiConsumer function) {
BiFunction extractor = (fieldName, strValue) -> {
try {
return Integer.valueOf(strValue);
}
catch (NumberFormatException e) {
LoggerFactory.getLogger(getClass()).error("Unexpected value {} extracted from configuration field '{}' using regex '{}'",
strValue, fieldName, regex);
return null;
}
};
forEachMatchingFieldName(regex, groupNumber, extractor, function);
}
/**
* For all fields whose names match the given regular expression, extract a boolean value from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param function the consumer that takes the value of matching field and the boolean extracted from the field name; may not
* be null
*/
default void forEachMatchingFieldNameWithBoolean(String regex, BiConsumer function) {
forEachMatchingFieldNameWithBoolean(Pattern.compile(regex), 1, function);
}
/**
* For all fields whose names match the given regular expression, extract a boolean value from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the boolean to be extracted; must be positive
* @param function the consumer that takes the value of matching field and the boolean extracted from the field name; may not
* be null
*/
default void forEachMatchingFieldNameWithBoolean(String regex, int groupNumber, BiConsumer function) {
forEachMatchingFieldNameWithBoolean(Pattern.compile(regex), groupNumber, function);
}
/**
* For all fields whose names match the given regular expression, extract a boolean value from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the boolean to be extracted; must be positive
* @param function the consumer that takes the value of matching field and the boolean extracted from the field name; may not
* be null
*/
default void forEachMatchingFieldNameWithBoolean(Pattern regex, int groupNumber, BiConsumer function) {
BiFunction extractor = (fieldName, strValue) -> {
try {
return Boolean.parseBoolean(strValue);
}
catch (NumberFormatException e) {
LoggerFactory.getLogger(getClass()).error("Unexpected value {} extracted from configuration field '{}' using regex '{}'",
strValue, fieldName, regex);
return null;
}
};
forEachMatchingFieldName(regex, groupNumber, extractor, function);
}
/**
* For all fields whose names match the given regular expression, extract a string value from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param function the consumer that takes the value of matching field and the string value extracted from the field name; may
* not be null
*/
default void forEachMatchingFieldNameWithString(String regex, BiConsumer function) {
forEachMatchingFieldNameWithString(Pattern.compile(regex), 1, function);
}
/**
* For all fields whose names match the given regular expression, extract a string value from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the string value to be extracted; must be positive
* @param function the consumer that takes the value of matching field and the string value extracted from the field name; may
* not be null
*/
default void forEachMatchingFieldNameWithString(String regex, int groupNumber, BiConsumer function) {
forEachMatchingFieldNameWithString(Pattern.compile(regex), groupNumber, function);
}
/**
* For all fields whose names match the given regular expression, extract a string value from the first group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the string value to be extracted; must be positive
* @param function the consumer that takes the value of matching field and the string value extracted from the field name; may
* not be null
*/
default void forEachMatchingFieldNameWithString(Pattern regex, int groupNumber, BiConsumer function) {
forEachMatchingFieldName(regex, groupNumber, (fieldName, value) -> value, function);
}
/**
* For all fields whose names match the given regular expression, extract a value from the specified group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the string value to be extracted; must be positive
* @param groupExtractor the function that extracts the value from the group
* @param function the consumer that takes the value of matching field and the value extracted from the field name; may
* not be null
*/
default void forEachMatchingFieldName(String regex, int groupNumber, BiFunction groupExtractor,
BiConsumer function) {
forEachMatchingFieldName(Pattern.compile(regex), groupNumber, groupExtractor, function);
}
/**
* For all fields whose names match the given regular expression, extract a value from the specified group in the regular
* expression and call the supplied function.
*
* @param regex the regular expression string; may not be null
* @param groupNumber the number of the regular expression group containing the string value to be extracted; must be positive
* @param groupExtractor the function that extracts the value from the group
* @param function the consumer that takes the value of matching field and the value extracted from the field name; may
* not be null
*/
default void forEachMatchingFieldName(Pattern regex, int groupNumber, BiFunction groupExtractor,
BiConsumer function) {
this.asMap().forEach((fieldName, fieldValue) -> {
Matcher matcher = regex.matcher(fieldName);
if (matcher.matches()) {
String groupValue = matcher.group(groupNumber);
T extractedValue = groupExtractor.apply(fieldName, groupValue);
if (extractedValue != null) {
function.accept(fieldValue, extractedValue);
}
}
});
}
/**
* Call the supplied function for each of the fields.
*
* @param function the consumer that takes the field name and the string value extracted from the field; may
* not be null
*/
default void forEach(BiConsumer function) {
this.asMap().forEach(function);
}
/**
* Returns the string config value from newProperty config field if it's set or its default value when it's not
* set/null.If both are null it returns the value of the oldProperty config field, or its default value when it's
* null.
* This fallback only works for newProperty fields that have a null / not-set default value!
*
* @param newProperty the new property config field
* @param oldProperty the old / fallback property config field
* @return the evaluated value
*/
default String getFallbackStringProperty(Field newProperty, Field oldProperty) {
return getString(newProperty, () -> getString(oldProperty));
}
/**
* Returns the string config value from newProperty config field with a warning if it's set or its default value when it's not
* set/null. If both are null it returns the value of the oldProperty config field with a warning, or its default value when
* it's null.
*/
default String getFallbackStringPropertyWithWarning(Field newProperty, Field oldProperty) {
if (hasKey(oldProperty.name()) && hasKey(newProperty.name())) { // both are set
CONFIGURATION_LOGGER.warn("Provided configuration has deprecated property \"" + oldProperty.name()
+ "\" and new property \"" + newProperty.name() + "\" set. Using value from \"" + newProperty.name() + "\"!");
}
return getString(
newProperty,
() -> {
String oldValue = getString(oldProperty);
if (oldValue != null && !oldValue.equals(oldProperty.defaultValueAsString())) {
CONFIGURATION_LOGGER.warn("Using configuration property \"" + oldProperty.name()
+ "\" is deprecated and will be removed in future versions. Please use \"" + newProperty.name()
+ "\" instead.");
}
return oldValue;
});
}
}