All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.debezium.config.Field Maven / Gradle / Ivy

There is a newer version: 1.13.0
Show newest version
/*
 * 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.time.DateTimeException;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.Supplier;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;

import javax.lang.model.SourceVersion;

import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.common.config.ConfigDef.Importance;
import org.apache.kafka.common.config.ConfigDef.Type;
import org.apache.kafka.common.config.ConfigDef.Width;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.config.ConfigValue;

import io.debezium.annotation.Immutable;
import io.debezium.function.Predicates;
import io.debezium.util.Strings;

/**
 * An immutable definition of a field that make appear within a {@link Configuration} instance.
 * @author Randall Hauch
 */
@Immutable
public final class Field {

    public static final String INTERNAL_PREFIX = "internal.";

    /**
     * Create a set of fields.
     * @param fields the fields to include
     * @return the field set; never null
     */
    public static Set setOf(Field... fields) {
        return new Set().with(fields);
    }

    /**
     * Create a set of fields.
     * @param fields the fields to include
     * @return the field set; never null
     */
    public static Set setOf(Iterable fields) {
        return new Set().with(fields);
    }

    /**
     * A set of fields.
     */
    @Immutable
    public static final class Set implements Iterable {
        private final Map fieldsByName;

        private Set() {
            this.fieldsByName = Collections.emptyMap();
        }

        private Set(Collection fields) {
            Map all = new LinkedHashMap<>();
            fields.forEach(field -> {
                if (field != null) {
                    all.put(field.name(), field);
                }
            });
            this.fieldsByName = Collections.unmodifiableMap(all);
        }

        /**
         * Get the field with the given {Field#name() name}.
         * @param name the name of the field
         * @return the field, or {@code null} if there is no field with the given name
         */
        public Field fieldWithName(String name) {
            return fieldsByName.get(name);
        }

        @Override
        public Iterator iterator() {
            return fieldsByName.values().iterator();
        }

        /**
         * Get the fields in this set as an array.
         * @return the array of fields; never null
         */
        public Field[] asArray() {
            return fieldsByName.values().toArray(new Field[fieldsByName.size()]);
        }

        /**
         * Call the supplied function for each of this set's fields that have non-existent dependents.
         * @param consumer the function; may not be null
         */
        public void forEachMissingDependent(Consumer consumer) {
            fieldsByName.values().stream()
                    .map(Field::dependents)
                    .flatMap(Collection::stream)
                    .filter(Predicates.not(fieldsByName::containsKey))
                    .forEach(consumer);
        }

        /**
         * Call the supplied function for each of this set's fields that are not included as dependents in other
         * fields.
         * @param consumer the function; may not be null
         */
        public void forEachTopLevelField(Consumer consumer) {
            Collection namesOfDependents = fieldsByName.values().stream()
                    .map(Field::dependents)
                    .flatMap(Collection::stream)
                    .collect(Collectors.toSet());
            fieldsByName.values().stream().filter(f -> !namesOfDependents.contains(f.name())).forEach(consumer);
        }

        /**
         * Get a new set that contains the fields in this set and those supplied.
         * @param fields the fields to include with this set's fields
         * @return the new set; never null
         */
        public Set with(Field... fields) {
            if (fields.length == 0) {
                return this;
            }
            LinkedHashSet all = new LinkedHashSet<>(this.fieldsByName.values());
            for (Field f : fields) {
                if (f != null) {
                    all.add(f);
                }
            }
            return new Set(all);
        }

        /**
         * Get a new set that contains the fields in this set and those supplied.
         * @param fields the fields to include with this set's fields
         * @return the new set; never null
         */
        public Set with(Iterable fields) {
            LinkedHashSet all = new LinkedHashSet<>(this.fieldsByName.values());
            fields.forEach(field -> {
                if (field != null) {
                    all.add(field);
                }
            });
            return new Set(all);
        }
    }

    /**
     * A functional interface that accepts validation results.
     */
    @FunctionalInterface
    public static interface ValidationOutput {
        /**
         * Accept a problem with the given value for the field.
         * @param field the field with the value; may not be null
         * @param value the value that is not valid
         * @param problemMessage the message describing the problem; may not be null
         */
        void accept(Field field, Object value, String problemMessage);
    }

    /**
     * A functional interface that can be used to validate field values.
     */
    @FunctionalInterface
    public static interface Validator {

        /**
         * Validate the supplied value for the field, and report any problems to the designated consumer.
         *
         * @param config the configuration containing the field to be validated; may not be null
         * @param field the {@link Field} being validated; never null
         * @param problems the consumer to be called with each problem; never null
         * @return the number of problems that were found, or 0 if the value is valid
         */
        int validate(Configuration config, Field field, ValidationOutput problems);

        /**
         * Obtain a new {@link Validator} object that validates using this validator and the supplied validator.
         *
         * @param other the validation function to call after this
         * @return the new validator, or this validator if {@code other} is {@code null} or equal to {@code this}
         */
        default Validator and(Validator other) {
            if (other == null || other == this) {
                return this;
            }
            return (config, field, problems) -> {
                return validate(config, field, problems) + other.validate(config, field, problems);
            };
        }
    }

    /**
     * A component that is able to provide recommended values for a field given a configuration.
     * In case that there are {@link Field#dependents() dependencies} between fields, the valid values and visibility
     * for a field may change given the values of other fields.
     */
    public static interface Recommender {
        /**
         * Return a set of recommended (and valid) values for the field given the current configuration values.
         * @param field the field for which the recommended values are to be found; may not be null
         * @param config the configuration; may not be null
         * @return the list of valid values
         */
        public List validValues(Field field, Configuration config);

        /**
         * Set the visibility of the field given the current configuration values.
         * @param field the field; may not be null
         * @param config the configuration; may not be null
         * @return {@code true} if the field is to be visible, or {@code false} otherwise
         */
        public boolean visible(Field field, Configuration config);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name.
     * @param name the name of the field; may not be null
     * @return the field; never null
     */
    public static Field create(String name) {
        return new Field(name, null, null, null, null, null, null, null);
    }

    /**
     * Create an immutable internal {@link Field} instance with the given property name.
     * The name will be prefixed with {@code internal.} prefix.
     *
     * @param name the name of the field; may not be null
     * @return the field; never null
     */
    public static Field createInternal(String name) {
        return new Field(INTERNAL_PREFIX + name, null, null, null, null, null, null, null, null, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @return the field; never null
     */
    public static Field create(String name, String displayName) {
        return new Field(name, displayName, null, null, null, null, null, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name and description.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description) {
        return new Field(name, displayName, null, null, description, null, null, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValue the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, String defaultValue) {
        return new Field(name, displayName, Type.STRING, null, description, null, () -> defaultValue, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValue the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, int defaultValue) {
        return new Field(name, displayName, Type.INT, null, description, null, () -> Integer.toString(defaultValue), null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValue the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, long defaultValue) {
        return new Field(name, displayName, Type.LONG, null, description, null, () -> Long.toString(defaultValue), null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValue the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, boolean defaultValue) {
        return new Field(name, displayName, Type.BOOLEAN, null, description, null, () -> Boolean.toString(defaultValue), null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValueGenerator the generator for the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, Supplier defaultValueGenerator) {
        return new Field(name, displayName, Type.STRING, null, description, null, defaultValueGenerator, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     *
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValueGenerator the generator for the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, BooleanSupplier defaultValueGenerator) {
        return new Field(name, displayName, Type.BOOLEAN, null, description, null,
                defaultValueGenerator::getAsBoolean, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValueGenerator the generator for the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, IntSupplier defaultValueGenerator) {
        return new Field(name, displayName, Type.INT, null, description, null,
                defaultValueGenerator::getAsInt, null);
    }

    /**
     * Create an immutable {@link Field} instance with the given property name, description, and default value.
     * @param name the name of the field; may not be null
     * @param displayName the display name of the field; may not be null
     * @param description the description
     * @param defaultValueGenerator the generator for the default value for the field
     * @return the field; never null
     */
    public static Field create(String name, String displayName, String description, LongSupplier defaultValueGenerator) {
        return new Field(name, displayName, Type.LONG, null, description, null,
                defaultValueGenerator::getAsLong, null);
    }

    /**
     * Add this field to the given configuration definition.
     * @param configDef the definition of the configuration; may be null if none of the fields are to be added
     * @param groupName the name of the group; may be null
     * @param fields the fields to be added as a group to the definition of the configuration
     * @return the updated configuration; never null
     */
    public static ConfigDef group(ConfigDef configDef, String groupName, Field... fields) {
        if (configDef != null) {
            if (groupName != null) {
                for (int i = 0; i != fields.length; ++i) {
                    Field f = fields[i];
                    configDef.define(f.name(), f.type(), f.defaultValue(), null, f.importance(), f.description(),
                            groupName, i + 1, f.width(), f.displayName(), f.dependents(), null);
                }
            }
            else {
                for (int i = 0; i != fields.length; ++i) {
                    Field f = fields[i];
                    configDef.define(f.name(), f.type(), f.defaultValue(), null, f.importance(), f.description(),
                            null, 1, f.width(), f.displayName(), f.dependents(), null);
                }
            }
        }
        return configDef;
    }

    private final String name;
    private final String displayName;
    private final String desc;
    private final Supplier defaultValueGenerator;
    private final Validator validator;
    private final Width width;
    private final Type type;
    private final Importance importance;
    private final List dependents;
    private final Recommender recommender;

    protected Field(String name, String displayName, Type type, Width width, String description, Importance importance,
                    Supplier defaultValueGenerator, Validator validator) {
        this(name, displayName, type, width, description, importance, null, defaultValueGenerator, validator, null);
    }

    protected Field(String name, String displayName, Type type, Width width, String description, Importance importance,
                    List dependents, Supplier defaultValueGenerator, Validator validator,
                    Recommender recommender) {
        Objects.requireNonNull(name, "The field name is required");
        this.name = name;
        this.displayName = displayName;
        this.desc = description;
        this.defaultValueGenerator = defaultValueGenerator != null ? defaultValueGenerator : () -> null;
        this.validator = validator;
        this.type = type != null ? type : Type.STRING;
        this.width = width != null ? width : Width.NONE;
        this.importance = importance != null ? importance : Importance.MEDIUM;
        this.dependents = dependents != null ? dependents : Collections.emptyList();
        this.recommender = recommender;
        assert this.name != null;
    }

    /**
     * Get the name of the field.
     * @return the name; never null
     */
    public String name() {
        return name;
    }

    /**
     * Get the default value of the field.
     * @return the default value, or {@code null} if there is no default value
     */
    public Object defaultValue() {
        return defaultValueGenerator.get();
    }

    /**
     * Get the string representation of the default value of the field.
     * @return the default value, or {@code null} if there is no default value
     */
    public String defaultValueAsString() {
        Object defaultValue = defaultValue();
        return defaultValue != null ? defaultValue.toString() : null;
    }

    /**
     * Get the description of the field.
     * @return the description; never null
     */
    public String description() {
        return desc;
    }

    /**
     * Get the display name of the field.
     * @return the display name; never null
     */
    public String displayName() {
        return displayName;
    }

    /**
     * Get the width of this field.
     * @return the width; never null
     */
    public Width width() {
        return width;
    }

    /**
     * Get the type of this field.
     * @return the type; never null
     */
    public Type type() {
        return type;
    }

    /**
     * Get the importance of this field.
     * @return the importance; never null
     */
    public Importance importance() {
        return importance;
    }

    /**
     * Get the names of the fields that are or may be dependent upon this field.
     * @return the list of dependents; never null but possibly empty
     */
    public List dependents() {
        return dependents;
    }

    /**
     * Get the validator for this field.
     * @return the validator; may be null if there is no validator
     */
    public Validator validator() {
        return validator;
    }

    /**
     * Get the {@link Recommender} for this field.
     * @return the recommender; may be null if there is no recommender
     */
    public Recommender recommender() {
        return recommender;
    }

    /**
     * Validate the supplied value for this field, and report any problems to the designated consumer.
     * @param config the field values keyed by their name; may not be null
     * @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
     */
    public boolean validate(Configuration config, ValidationOutput problems) {
        Validator typeValidator = validatorForType(type);
        int errors = 0;
        if (typeValidator != null) {
            errors += typeValidator.validate(config, this, problems);
        }
        if (validator != null) {
            errors += validator.validate(config, this, problems);
        }
        return errors == 0;
    }

    /**
     * Validate this field in the supplied configuration, updating the {@link ConfigValue} for the field with the results.
     * @param config the configuration to be validated; may not be null
     * @param fieldSupplier the supplier for dependent fields by name; may not be null
     * @param results the set of configuration results keyed by field name; may not be null
     */
    protected void validate(Configuration config, Function fieldSupplier, Map results) {
        // First, merge any new recommended values ...
        ConfigValue value = results.computeIfAbsent(this.name(), n -> new ConfigValue(n));

        // Apply the validator ...
        validate(config, (f, v, problem) -> {
            value.addErrorMessage(problem);
        });

        // Apply the recommender ..
        if (recommender != null) {
            try {
                // Set the visibility ...
                value.visible(recommender.visible(this, config));
                // Compute and set the new recommendations ...
                List newRecommendations = recommender.validValues(this, config);
                List previousRecommendations = value.recommendedValues();
                if (!previousRecommendations.isEmpty()) {
                    // Don't use any newly recommended values if they were not previously recommended ...
                    newRecommendations.retainAll(previousRecommendations);
                }
                value.recommendedValues(newRecommendations);
            }
            catch (ConfigException e) {
                value.addErrorMessage(e.getMessage());
            }
        }

        // Do the same for any dependents ...
        dependents.forEach(name -> {
            Field dependentField = fieldSupplier.apply(name);
            if (dependentField != null) {
                dependentField.validate(config, fieldSupplier, results);
            }
        });
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given description.
     * @param description the new description for the new field
     * @return the new field; never null
     */
    public Field withDescription(String description) {
        return new Field(name(), displayName, type(), width, description, importance(), dependents,
                defaultValueGenerator, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given display name.
     *
     * @param displayName the new display name for the field
     * @return the new field; never null
     */
    public Field withDisplayName(String displayName) {
        return new Field(name(), displayName, type(), width, description(), importance(), dependents,
                defaultValueGenerator, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given width.
     * @param width the new width for the field
     * @return the new field; never null
     */
    public Field withWidth(Width width) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                defaultValueGenerator, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given type.
     * @param type the new type for the field
     * @return the new field; never null
     */
    public Field withType(Type type) {
        return new Field(name(), displayName(), type, width(), description(), importance(), dependents,
                defaultValueGenerator, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but has a {@link #withType(Type) type} of
     * {@link org.apache.kafka.connect.data.Schema.Type#STRING}, a {@link #withRecommender(Recommender) recommender}
     * that returns a list of {@link Enum#name() Enum names} as valid values, and a validator that verifies values are valid
     * enumeration names.
     * @param enumType the enumeration type for the field
     * @return the new field; never null
     */
    public > Field withEnum(Class enumType) {
        return withEnum(enumType, null);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but has a {@link #withType(Type) type} of
     * {@link org.apache.kafka.connect.data.Schema.Type#STRING}, a {@link #withRecommender(Recommender) recommender}
     * that returns a list of {@link Enum#name() Enum names} as valid values, and a validator that verifies values are valid
     * enumeration names.
     * @param enumType the enumeration type for the field
     * @param defaultOption the default enumeration value; may be null
     * @return the new field; never null
     */
    public > Field withEnum(Class enumType, T defaultOption) {
        EnumRecommender recommendator = new EnumRecommender<>(enumType);
        Field result = withType(Type.STRING).withRecommender(recommendator).withValidation(recommendator);
        // Not all enums support EnumeratedValue yet
        if (defaultOption != null) {
            if (defaultOption instanceof EnumeratedValue) {
                result = result.withDefault(((EnumeratedValue) defaultOption).getValue());
            }
            else {
                result = result.withDefault(defaultOption.name().toLowerCase());
            }
        }
        return result;
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given importance.
     * @param importance the new importance for the field
     * @return the new field; never null
     */
    public Field withImportance(Importance importance) {
        return new Field(name(), displayName(), type(), width(), description(), importance, dependents,
                defaultValueGenerator, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given display name.
     * @param dependents the names of the fields that depend on this field
     * @return the new field; never null
     */
    public Field withDependents(String... dependents) {
        return new Field(name(), displayName(), type(), width, description(), importance(),
                Arrays.asList(dependents), defaultValueGenerator, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     * @param defaultValue the new default value for the new field
     * @return the new field; never null
     */
    public Field withDefault(String defaultValue) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                () -> defaultValue, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     *
     * @param defaultValue the new default value for the new field
     * @return the new field; never null
     */
    public Field withDefault(boolean defaultValue) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                () -> Boolean.valueOf(defaultValue), validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     * @param defaultValue the new default value for the new field
     * @return the new field; never null
     */
    public Field withDefault(int defaultValue) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                () -> defaultValue, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     *
     * @param defaultValue the new default value for the new field
     * @return the new field; never null
     */
    public Field withDefault(long defaultValue) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                () -> defaultValue, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     *
     * @param defaultValueGenerator the supplier for the new default value for the new field, called whenever a default value
     *            is needed
     * @return the new field; never null
     */
    public Field withDefault(BooleanSupplier defaultValueGenerator) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                defaultValueGenerator::getAsBoolean, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     *
     * @param defaultValueGenerator the supplier for the new default value for the new field, called whenever a default value
     *            is needed
     * @return the new field; never null
     */
    public Field withDefault(IntSupplier defaultValueGenerator) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                defaultValueGenerator::getAsInt, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given default value.
     *
     * @param defaultValueGenerator the supplier for the new default value for the new field, called whenever a default value
     *            is needed
     * @return the new field; never null
     */
    public Field withDefault(LongSupplier defaultValueGenerator) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                defaultValueGenerator::getAsLong, validator, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but with the given recommender.
     *
     * @param recommender the recommender; may be null
     * @return the new field; never null
     */
    public Field withRecommender(Recommender recommender) {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                defaultValueGenerator, validator, recommender);
    }

    public Field withInvisibleRecommender() {
        return withRecommender(new InvisibleRecommender());
    }

    /**
     * Create and return a new Field instance that is a copy of this field but that uses no validation.
     *
     * @return the new field; never null
     */
    public Field withNoValidation() {
        return new Field(name(), displayName(), type(), width, description(), importance(), dependents,
                defaultValueGenerator, null, recommender);
    }

    /**
     * Create and return a new Field instance that is a copy of this field but that in addition to {@link #validator() existing
     * validation} the supplied validation function(s) are also used.
     *
     * @param validators the additional validation function(s); may be null
     * @return the new field; never null
     */
    public Field withValidation(Validator... validators) {
        Validator actualValidator = validator;
        for (Validator validator : validators) {
            if (validator != null) {
                actualValidator = validator.and(actualValidator);
            }
        }
        return new Field(name(), displayName(), type(), width(), description(), importance(), dependents,
                defaultValueGenerator, actualValidator, recommender);
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof Field) {
            Field that = (Field) obj;
            return this.name().equals(that.name());
        }
        return false;
    }

    @Override
    public String toString() {
        return name();
    }

    /**
     * Validation logic for numeric ranges
     */
    public static class RangeValidator implements Validator {
        private final Number min;
        private final Number max;

        private RangeValidator(Number min, Number max) {
            this.min = min;
            this.max = max;
        }

        /**
         * A validator that checks only the lower numerical bound.
         *
         * @param min the minimum acceptable value; may not be null
         * @return the validator; never null
         */
        public static RangeValidator atLeast(Number min) {
            return new RangeValidator(min, null);
        }

        /**
         * A validator that checks both the upper and lower bound.
         *
         * @param min the minimum acceptable value; may not be null
         * @param max the maximum acceptable value; may not be null
         * @return the validator; never null
         */
        public static RangeValidator between(Number min, Number max) {
            return new RangeValidator(min, max);
        }

        @Override
        public int validate(Configuration config, Field field, ValidationOutput problems) {
            Number value = config.getNumber(field);
            if (value == null) {
                problems.accept(field, value, "A value must be provided");
                return 1;
            }
            if (min != null && value.doubleValue() < min.doubleValue()) {
                problems.accept(field, value, "Value must be at least " + min);
                return 1;
            }
            if (max != null && value.doubleValue() > max.doubleValue()) {
                problems.accept(field, value, "Value must be no more than " + max);
                return 1;
            }
            return 0;
        }

        public void ensureValid(String name, Object o) {
            if (o == null) {
                throw new ConfigException(name, o, "Value must be non-null");
            }
            Number n = (Number) o;
            if (min != null && n.doubleValue() < min.doubleValue()) {
                throw new ConfigException(name, o, "Value must be at least " + min);
            }
            if (max != null && n.doubleValue() > max.doubleValue()) {
                throw new ConfigException(name, o, "Value must be no more than " + max);
            }
        }

        @Override
        public String toString() {
            if (min == null) {
                return "[...," + max + "]";
            }
            else {
                if (max == null) {
                    return "[" + min + ",...]";
                }
                else {
                    return "[" + min + ",...," + max + "]";
                }
            }
        }
    }

    /**
     * A {@link Recommender} that will look at several fields that are deemed to be exclusive, such that when the first of
     * them has a value the others are made invisible.
     */
    public static class OneOfRecommender implements Recommender {

        protected final List possibleNames;

        public OneOfRecommender(String... possibleNames) {
            this(Arrays.asList(possibleNames));
        }

        public OneOfRecommender(List possibleNames) {
            this.possibleNames = possibleNames;
        }

        @Override
        public List validValues(Field field, Configuration config) {
            return new LinkedList<>();
        }

        @Override
        public boolean visible(Field field, Configuration config) {
            for (String possibleName : possibleNames) {
                Object value = config.getString(possibleName);
                if (value != null) {
                    // There is a value for this possible name, the name is visible if it has a value ...
                    return possibleName.equals(field.name());
                }
            }
            // There are no values for any of the possibles, so they all are visible ...
            return true;
        }
    }

    public static class EnumRecommender> implements Recommender, Validator {

        private final List validValues;
        private final java.util.Set literals;
        private final String literalsStr;

        public EnumRecommender(Class enumType) {
            // Not all enums support EnumeratedValue yet
            if (Arrays.asList(enumType.getInterfaces()).contains(EnumeratedValue.class)) {
                this.literals = Arrays.stream(enumType.getEnumConstants())
                        .map(x -> ((EnumeratedValue) x).getValue())
                        .map(String::toLowerCase)
                        .collect(Collectors.toSet());
            }
            else {
                this.literals = Arrays.stream(enumType.getEnumConstants())
                        .map(Enum::name)
                        .map(String::toLowerCase)
                        .collect(Collectors.toSet());
            }
            this.validValues = Collections.unmodifiableList(new ArrayList<>(this.literals));
            this.literalsStr = Strings.join(", ", validValues);
        }

        @Override
        public List validValues(Field field, Configuration config) {
            return validValues;
        }

        @Override
        public boolean visible(Field field, Configuration config) {
            return true;
        }

        @Override
        public int validate(Configuration config, Field field, ValidationOutput problems) {
            String value = config.getString(field);
            if (value == null) {
                problems.accept(field, value, "Value must be one of " + literalsStr);
                return 1;
            }
            String trimmed = value.trim().toLowerCase();
            if (!literals.contains(trimmed)) {
                problems.accept(field, value, "Value must be one of " + literalsStr);
                return 1;
            }
            return 0;
        }
    }

    /**
     * A {@link Recommender} that will look at several fields that are deemed to be exclusive, such that when the first of
     * them has a value the others are made invisible.
     */
    public static class InvisibleRecommender implements Recommender {

        public InvisibleRecommender() {
        }

        @Override
        public List validValues(Field field, Configuration config) {
            return new LinkedList<>();
        }

        @Override
        public boolean visible(Field field, Configuration config) {
            return false;
        }
    }

    public static Validator validatorForType(Type type) {
        switch (type) {
            case BOOLEAN:
                return Field::isBoolean;
            case CLASS:
                return Field::isClassName;
            case DOUBLE:
                return Field::isDouble;
            case INT:
                return Field::isInteger;
            case SHORT:
                return Field::isShort;
            case LONG:
                return Field::isLong;
            case STRING:
            case LIST:
            case PASSWORD:
                break;
        }
        return null;
    }

    public static int isListOfRegex(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        int errors = 0;
        if (value != null) {
            try {
                Strings.setOfRegex(value, Pattern.CASE_INSENSITIVE);
            }
            catch (PatternSyntaxException e) {
                problems.accept(field, value, "A comma-separated list of valid regular expressions is expected, but " + e.getMessage());
                ++errors;
            }
        }
        return errors;
    }

    public static int isRegex(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        int errors = 0;
        if (value != null) {
            try {
                Pattern.compile(value, Pattern.CASE_INSENSITIVE);
            }
            catch (PatternSyntaxException e) {
                problems.accept(field, value, "A valid regular expressions is expected, but " + e.getMessage());
                ++errors;
            }
        }
        return errors;
    }

    public static int isClassName(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null || SourceVersion.isName(value)) {
            return 0;
        }
        problems.accept(field, value, "A Java class name is expected");
        return 1;
    }

    public static int isRequired(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value != null && value.trim().length() > 0) {
            return 0;
        }
        problems.accept(field, value, "A value is required");
        return 1;
    }

    public static int isOptional(Configuration config, Field field, ValidationOutput problems) {
        // optional fields are valid whether or not there is a value
        return 0;
    }

    public static int isBoolean(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null ||
                value.trim().equalsIgnoreCase(Boolean.TRUE.toString()) ||
                value.trim().equalsIgnoreCase(Boolean.FALSE.toString())) {
            return 0;
        }
        problems.accept(field, value, "Either 'true' or 'false' is expected");
        return 1;
    }

    public static int isInteger(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            Integer.parseInt(value);
        }
        catch (NumberFormatException e) {
            problems.accept(field, value, "An integer is expected");
            return 1;
        }
        return 0;
    }

    public static int isPositiveInteger(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            if (Integer.parseInt(value) > 0) {
                return 0;
            }
        }
        catch (Throwable e) {
        }
        problems.accept(field, value, "A positive integer is expected");
        return 1;
    }

    public static int isNonNegativeInteger(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            if (Integer.parseInt(value) >= 0) {
                return 0;
            }
        }
        catch (Throwable e) {
        }
        problems.accept(field, value, "An non-negative integer is expected");
        return 1;
    }

    public static int isLong(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            Long.parseLong(value);
        }
        catch (NumberFormatException e) {
            problems.accept(field, value, "A long value is expected");
            return 1;
        }
        return 0;
    }

    public static int isPositiveLong(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            if (Long.parseLong(value) > 0) {
                return 0;
            }
        }
        catch (Throwable e) {
        }
        problems.accept(field, value, "A positive long value is expected");
        return 1;
    }

    public static int isNonNegativeLong(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            if (Long.parseLong(value) >= 0) {
                return 0;
            }
        }
        catch (Throwable e) {
        }
        problems.accept(field, value, "A non-negative long value is expected");
        return 1;
    }

    public static int isShort(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            Short.parseShort(value);
        }
        catch (NumberFormatException e) {
            problems.accept(field, value, "A short value is expected");
            return 1;
        }
        return 0;
    }

    public static int isDouble(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            Double.parseDouble(value);
        }
        catch (NumberFormatException e) {
            problems.accept(field, value, "A double value is expected");
            return 1;
        }
        return 0;
    }

    public static int isZoneOffset(Configuration config, Field field, ValidationOutput problems) {
        String value = config.getString(field);
        if (value == null) {
            return 0;
        }
        try {
            ZoneOffset.of(value);
        }
        catch (DateTimeException e) {
            problems.accept(field, value, "A zone offset string representation is expected");
            return 1;
        }
        return 0;
    }
}