package io.eguan.configuration; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import io.eguan.configuration.ValidationError.ErrorType; import java.util.regex.Pattern; import javax.annotation.Nonnull; /** * Abstract implementation of the {@link ConfigKey} interface. * * This class provides additional protected methods for internal usage. * * @author oodrive * @author pwehrle * */ public abstract class AbstractConfigKey implements ConfigKey<MetaConfiguration> { /** * The regular expression against which to match the {@link #name}. */ private static final String NAME_REGEX = "[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)*"; /** * The {@link Pattern} to use for {@link #name} validation. */ protected static final Pattern NAME_PATTERN = Pattern.compile(NAME_REGEX); /** * Validates a name against the {@link AbstractConfigKey}'s constraints on names. * * @param name * the name to check against the mandatory syntax (i.e. {@value #NAME_REGEX}) * @return {@code true} if the name is valid, {@code false} if the name is empty, {@code null} or otherwise invalid */ protected static boolean isNameValid(final String name) { if (name == null) { return false; } return NAME_PATTERN.matcher(name).matches(); } /** * The unique name returned provided to {@link #AbstractConfigKey(String)} and returned by {@link #getName()}. */ private final String name; /** * Abstract constructor taking the mandatory name as argument. * * @param name * the unique name identifying this key */ protected AbstractConfigKey(final String name) { this.name = name; } @Override public final boolean hasDefaultValue() { // returns definitive value according to constraints return isRequired() ? false : getDefaultValue() != null; } @Override public boolean isRequired() { // defaults to required if there is no default value return getDefaultValue() == null; } /** * Gets the unique name to be used in .properties files for this key. * * The returned name uniquely identifies a configuration key within this context, following a syntax matching * {@value #NAME_REGEX}. * * @return the non-{@code null} unique name within the context */ @Nonnull protected final String getName() { return name; } /** * Parses the given {@link String} value defined for the {@link ConfigKey}. * * This method returns a result of the accurate type if it can be parsed from the provided {@link String} value. * * If the method is passed an empty {@link String}, it can return either <code>null</code> or an empty String * * @param value * the non-{@code null} value to parse as read from a .properties file. * @return the typed value, or, when the <code>value</code> is empty, the default value or <code>null</code> if * there is no default value. * @throws IllegalArgumentException * if no value can be parsed and/or conversion to the target type fails * @throws NullPointerException * if the passed value is {@code null} */ protected abstract Object parseValue(@Nonnull String value) throws IllegalArgumentException, NullPointerException; /** * Provides the inverse of {@link #parseValue(String)}, i.e. produces a {@link String} from the provided value that * can be parsed to the same (content) value by {@link #parseValue(String)}. Therefore, a <code>null</code> * parameter will be converted to an empty {@link String}. * * @param value * the value of the same type as the one produced by {@link #parseValue(String)} or <code>null</code> * @return the {@link String} containing all the necessary information to be parsed back to the equivalent value * @throws IllegalArgumentException * if the argument is of the wrong type or cannot be converted to the target type */ protected abstract String valueToString(Object value) throws IllegalArgumentException; /** * Checks the given value for validity regarding constraints defined by this {@link ConfigKey}. * * * The value parameter must be of the type obtained from {@link #parseValue(String)}. * <ul> * <li>allows {@code null} as a valid value for undefined values</li> * <li>performs basic sanity checks (not negative, correct syntax, ...) and</li> * <li>performs advanced range and domain checks (min and max limits, valid enum constant, ...).</li> * </ul> * * @param value * the value to check, possibly {@code null} * @return a specific {@link ValidationError} if the check produced an error, {@link ValidationError#NO_ERROR} * otherwise */ protected abstract ValidationError checkValue(Object value); /** * Gets the default value for this key. * * Keys marked as required cannot have a non-{@code null} default value. * * @return the default value or {@code null} if there is none */ protected abstract Object getDefaultValue(); /** * Utility method that checks if the given configuration key is managed by the configuration provided and throw an * {@link IllegalStateException} if this is not the case. * * @param config * the {@link MetaConfiguration} to check * @throws IllegalStateException * if the provided configuration does not contain this key */ protected final void checkConfigForKey(@Nonnull final MetaConfiguration config) throws IllegalStateException { if (!config.getConfigKeys().contains(this)) { throw new IllegalStateException(String.format("Configuration does not include key; config='%s', key='%s'", config, this)); } } /** * Checks if the given value is {@code null} and/or required. * * @param value * the value to check * @return <ul> * <li>a {@link ValidationError} with {@link ValidationError.ErrorType#NONE} if value is neither * {@code null} nor required</li> * <li>a {@link ValidationError} with {@link ValidationError.ErrorType#VALUE_NULL} if value is {@code null} * but not required</li> * <li>a specific {@link ValidationError} with {@link ValidationError.ErrorType#VALUE_REQUIRED} if value is * both {@code null} and required</li> * </ul> */ protected final ValidationError checkForNullAndRequired(final Object value) { ValidationError result = ValidationError.NO_ERROR; if (value == null) { result = new ValidationError(ErrorType.VALUE_NULL, null, this, value, "value is null"); if (isRequired()) { result = new ValidationError(ErrorType.VALUE_REQUIRED, null, this, value, "is required and not set"); } } return result; } /** * Checks if the given {@link Object} is of the exact {@link Class} provided as second argument. * * As member classes pass this test, enums constants pass. * * @param value * the Object to check * @param clazz * @return a {@link ValidationError} with {@link ValidationError.ErrorType#VALUE_INVALID} if the provided value is * not of the exact same type, {@link ValidationError.NO_ERROR} otherwise * @throws NullPointerException * if either argument is <code>null</code> */ protected final ValidationError checkSameClass(final Object value, final Class<?> clazz) throws NullPointerException { try { clazz.cast(value); } catch (final ClassCastException ce) { return new ValidationError(ErrorType.VALUE_INVALID, null, this, value, ce.getMessage()); } final Class<?> valueClass = value.getClass(); // checks if clazz is the same or a superclass of value's class if (valueClass.isAssignableFrom(clazz)) { return ValidationError.NO_ERROR; } // treats the special case where an enum constant appears as member instance of the enclosing enum class if (valueClass.getEnclosingClass() == clazz) { return ValidationError.NO_ERROR; } return new ValidationError(ErrorType.VALUE_INVALID, null, this, value, "is a subclass of " + clazz.getCanonicalName()); } /** * Return the {@link String} representation of this object, i.e. it's {@link Class#getCanonicalName() canonical * name}. * * Classes extending {@link AbstractConfigKey} are expected to be singletons, so this method is just for prettily * printing the class name. */ @Override public final String toString() { return getClass().getCanonicalName(); } }