| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.deltaspike.core.api.config; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| import java.util.concurrent.TimeUnit; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import javax.enterprise.inject.Typed; |
| |
| import org.apache.deltaspike.core.api.projectstage.ProjectStage; |
| import org.apache.deltaspike.core.spi.config.ConfigFilter; |
| import org.apache.deltaspike.core.spi.config.ConfigSource; |
| import org.apache.deltaspike.core.spi.config.ConfigSourceProvider; |
| import org.apache.deltaspike.core.util.ClassUtils; |
| import org.apache.deltaspike.core.util.ExceptionUtils; |
| import org.apache.deltaspike.core.util.ProjectStageProducer; |
| import org.apache.deltaspike.core.util.ServiceUtils; |
| |
| /** |
| * The main entry point to the DeltaSpike configuration mechanism. |
| * |
| * <p> |
| * Resolves configured values of properties by going through the list of configured {@link ConfigSource}s and using the |
| * one with the highest ordinal. If multiple {@link ConfigSource}s have the same ordinal, their order is undefined.</p> |
| * |
| * <p> |
| * You can provide your own lookup paths by implementing and registering additional {@link PropertyFileConfig} or |
| * {@link ConfigSource} or {@link ConfigSourceProvider} implementations.</p> |
| * |
| * <p> |
| * The resolved configuration is also accessible by simple injection using the {@link ConfigProperty} qualifier.</p> |
| * |
| * @see <a href="http://deltaspike.apache.org/documentation/configuration.html">DeltaSpike Configuration Mechanism</a> |
| */ |
| @Typed() |
| public final class ConfigResolver |
| { |
| |
| private static final Logger LOG = Logger.getLogger(ConfigResolver.class.getName()); |
| |
| /** |
| * The content of this map will get lazily initiated and will hold the |
| * sorted List of ConfigSources for each WebApp/EAR, etc (thus the |
| * ClassLoader). |
| */ |
| private static Map<ClassLoader, ConfigSource[]> configSources |
| = new ConcurrentHashMap<ClassLoader, ConfigSource[]>(); |
| |
| /** |
| * The content of this map will hold the List of ConfigFilters |
| * for each WebApp/EAR, etc (thus the ClassLoader). |
| */ |
| private static Map<ClassLoader, List<ConfigFilter>> configFilters |
| = new ConcurrentHashMap<ClassLoader, List<ConfigFilter>>(); |
| |
| private static volatile ProjectStage projectStage = null; |
| |
| private ConfigResolver() |
| { |
| // this is a utility class which doesn't get instantiated. |
| } |
| |
| /** |
| * This method can be used for programmatically adding {@link ConfigSource}s. |
| * It is not needed for normal 'usage' by end users, but only for Extension Developers! |
| * |
| * @param configSourcesToAdd the ConfigSources to add |
| */ |
| public static synchronized void addConfigSources(List<ConfigSource> configSourcesToAdd) |
| { |
| // we first pickup all pre-configured ConfigSources... |
| getConfigSources(); |
| |
| // and now we can easily add our own |
| ClassLoader currentClassLoader = ClassUtils.getClassLoader(null); |
| ConfigSource[] configuredConfigSources = configSources.get(currentClassLoader); |
| |
| List<ConfigSource> allConfigSources = new ArrayList<ConfigSource>(); |
| allConfigSources.addAll(Arrays.asList(configuredConfigSources)); |
| allConfigSources.addAll(configSourcesToAdd); |
| |
| // finally put all the configSources back into the map |
| configSources.put(currentClassLoader, sortDescending(allConfigSources)); |
| } |
| |
| /** |
| * Clear all ConfigSources for the current ClassLoader. |
| * This will also clean up all ConfigFilters. |
| */ |
| public static synchronized void freeConfigSources() |
| { |
| ClassLoader classLoader = ClassUtils.getClassLoader(null); |
| configSources.remove(classLoader); |
| configFilters.remove(classLoader); |
| } |
| |
| /** |
| * Add a {@link ConfigFilter} to the ConfigResolver. This will only affect the current WebApp (or more precisely the |
| * current ClassLoader and it's children). |
| * |
| * @param configFilter |
| */ |
| public static void addConfigFilter(ConfigFilter configFilter) |
| { |
| List<ConfigFilter> currentConfigFilters = getInternalConfigFilters(); |
| currentConfigFilters.add(configFilter); |
| } |
| |
| /** |
| * @return the {@link ConfigFilter}s for the current application. |
| */ |
| public static List<ConfigFilter> getConfigFilters() |
| { |
| return Collections.unmodifiableList(getInternalConfigFilters()); |
| } |
| |
| /** |
| * @return the {@link ConfigFilter}s for the current application. |
| */ |
| private static List<ConfigFilter> getInternalConfigFilters() |
| { |
| ClassLoader cl = ClassUtils.getClassLoader(null); |
| List<ConfigFilter> currentConfigFilters = configFilters.get(cl); |
| if (currentConfigFilters == null) |
| { |
| currentConfigFilters = new CopyOnWriteArrayList<ConfigFilter>(); |
| configFilters.put(cl, currentConfigFilters); |
| } |
| |
| return currentConfigFilters; |
| } |
| |
| /** |
| * {@link #getPropertyValue(java.lang.String)} which returns the provided default value if no configured value can |
| * be found (<code>null</code> or empty). |
| * |
| * @param key the property key |
| * @param defaultValue fallback value |
| * |
| * @return the configured property value from the {@link ConfigSource} with the highest ordinal or the defaultValue |
| * if there is no value explicitly configured |
| */ |
| public static String getPropertyValue(String key, String defaultValue) |
| { |
| return getPropertyValue(key, defaultValue, true); |
| } |
| |
| |
| public static String getPropertyValue(String key, String defaultValue, boolean evaluateVariables) |
| { |
| String value = getPropertyValue( |
| key, |
| new ConfigResolverContext() |
| .setEvaluateVariables(evaluateVariables)); |
| |
| return fallbackToDefaultIfEmpty(key, value, defaultValue); |
| } |
| |
| /** |
| * Resolves the value configured for the given key. |
| * |
| * @param key the property key |
| * |
| * @return the configured property value from the {@link ConfigSource} with the highest ordinal or null if there is |
| * no configured value for it |
| */ |
| public static String getPropertyValue(String key) |
| { |
| return getPropertyValue( |
| key, |
| new ConfigResolverContext().setEvaluateVariables(true)); |
| } |
| |
| /** |
| * Resolves the value configured for the given key. |
| * |
| * @param key the property key |
| * @param evaluateVariables whether to evaluate any '${variablename}' variable expressions |
| * |
| * @return the configured property value from the {@link ConfigSource} with the highest ordinal or null if there is |
| * no configured value for it |
| */ |
| public static String getPropertyValue(String key, boolean evaluateVariables) |
| { |
| return getPropertyValue( |
| key, |
| new ConfigResolverContext() |
| .setEvaluateVariables(evaluateVariables)); |
| } |
| |
| /** |
| * Resolves the value configured for the given key in the current |
| * {@link org.apache.deltaspike.core.api.projectstage.ProjectStage}. |
| * |
| * <p> |
| * First, it will search for a value configured for the given key suffixed with the current ProjectStage (e.g. |
| * 'myproject.myconfig.Production'), and in case this value is not found (null or empty), it will look up the given |
| * key without any suffix.</p> |
| * |
| * <p> |
| * <b>Attention</b> This method must only be used after all ConfigSources got registered and it also must not be |
| * used to determine the ProjectStage itself.</p> |
| * |
| * @param key |
| * |
| * @return the value configured for {@code <given key>.<current project stage>}, or just the configured value of |
| * {@code <given key>} if the project-stage-specific value is not found (null or empty) |
| * |
| */ |
| public static String getProjectStageAwarePropertyValue(String key) |
| { |
| ConfigResolverContext configResolverContext = |
| new ConfigResolverContext() |
| .setProjectStageAware(true) |
| .setEvaluateVariables(true); |
| |
| ProjectStage ps = getProjectStage(); |
| |
| String value = getPropertyValue(key + '.' + ps, configResolverContext); |
| if (value == null) |
| { |
| configResolverContext.setProjectStageAware(false); |
| value = getPropertyValue(key, configResolverContext); |
| } |
| |
| return value; |
| } |
| /** |
| * {@link #getProjectStageAwarePropertyValue(String)} which returns the provided default value if no configured |
| * value can be found (<code>null</code> or empty). |
| * |
| * @param key |
| * @param defaultValue fallback value |
| * |
| * @return the configured value or if non found the defaultValue |
| * |
| */ |
| public static String getProjectStageAwarePropertyValue(String key, String defaultValue) |
| { |
| String value = getProjectStageAwarePropertyValue(key); |
| |
| return fallbackToDefaultIfEmpty(key, value, defaultValue); |
| } |
| |
| /** |
| * Resolves the value configured for the given key, parameterized by the current |
| * {@link org.apache.deltaspike.core.api.projectstage.ProjectStage} and by the value of a second property. |
| * |
| * <p> |
| * <b>Example:</b><br/> |
| * Suppose the current ProjectStage is {@code UnitTest} and we are looking for the value of {@code datasource} |
| * parameterized by the configured {@code dbvendor}. |
| * </p> |
| * <p> |
| * The first step is to resolve the value of the second property, {@code dbvendor}. This will also take the current |
| * ProjectStage into account. The following lookup is performed: |
| * <ul><li>dbvendor.UnitTest</li></ul> |
| * and if this value is not found then we will do a 2nd lookup for |
| * <ul><li>dbvendor</li></ul></p> |
| * |
| * <p> |
| * If a value was found for the second property (e.g. dbvendor = 'mysql') then we will use its value for the main |
| * lookup. If no value is found for the parameterized key {@code <key>.<second property value>.<project stage>}, we |
| * will do the {@code <key>.<second property value>}, then {@code <key>.<project stage>} and finally a {@code <key>} |
| * lookup: |
| * <ul> |
| * <li>datasource.mysql.UnitTest</li> |
| * <li>datasource.mysql</li> |
| * <li>datasource.UnitTest</li> |
| * <li>datasource</li> |
| * </ul> |
| * </p> |
| * |
| * <p> |
| * <b>Attention</b> This method must only be used after all ConfigSources got registered and it also must not be |
| * used to determine the ProjectStage itself.</p> |
| * |
| * @param key |
| * @param property the property to look up first and use as the parameter for the main lookup |
| * |
| * @return the configured value or null if no value is found for any of the key variants |
| * |
| */ |
| public static String getPropertyAwarePropertyValue(String key, String property) |
| { |
| String propertyValue = getProjectStageAwarePropertyValue(property); |
| |
| String value = null; |
| |
| if (propertyValue != null && propertyValue.length() > 0) |
| { |
| value = getProjectStageAwarePropertyValue(key + '.' + propertyValue); |
| } |
| |
| if (value == null) |
| { |
| value = getProjectStageAwarePropertyValue(key); |
| } |
| |
| return value; |
| } |
| |
| /** |
| * {@link #getPropertyAwarePropertyValue(java.lang.String, java.lang.String)} which returns the provided default |
| * value if no configured value can be found (<code>null</code> or empty). |
| * |
| * <p> |
| * <b>Attention</b> This method must only be used after all ConfigSources got registered and it also must not be |
| * used to determine the ProjectStage itself.</p> |
| * |
| * @param key |
| * @param property the property to look up first and use as the parameter for the main lookup |
| * @param defaultValue fallback value |
| * |
| * @return the configured value or if non found the defaultValue |
| * |
| */ |
| public static String getPropertyAwarePropertyValue(String key, String property, String defaultValue) |
| { |
| String value = getPropertyAwarePropertyValue(key, property); |
| |
| return fallbackToDefaultIfEmpty(key, value, defaultValue); |
| } |
| |
| private static String getPropertyValue(String key, ConfigResolverContext configResolverContext) |
| { |
| ConfigSource[] appConfigSources = getConfigSources(); |
| |
| String value; |
| for (ConfigSource configSource : appConfigSources) |
| { |
| value = configSource.getPropertyValue(key); |
| |
| if (value != null) |
| { |
| LOG.log(Level.FINE, "found value {0} for key {1} in ConfigSource {2}.", |
| new Object[]{filterConfigValueForLog(key, value), key, configSource.getConfigName()}); |
| |
| if (configResolverContext.isEvaluateVariables()) |
| { |
| value = resolveVariables(value, configResolverContext); |
| } |
| |
| return filterConfigValue(key, value); |
| } |
| |
| LOG.log(Level.FINER, "NO value found for key {0} in ConfigSource {1}.", |
| new Object[]{key, configSource.getConfigName()}); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * recursively resolve any ${varName} in the value |
| */ |
| private static String resolveVariables(String value, ConfigResolverContext configResolverContext) |
| { |
| int startVar = 0; |
| while ((startVar = value.indexOf("${", startVar)) >= 0) |
| { |
| int endVar = value.indexOf("}", startVar); |
| if (endVar <= 0) |
| { |
| break; |
| } |
| String varName = value.substring(startVar + 2, endVar); |
| if (varName.isEmpty()) |
| { |
| break; |
| } |
| |
| String variableValue; |
| if (configResolverContext.isProjectStageAware()) |
| { |
| variableValue = getProjectStageAwarePropertyValue(varName); |
| } |
| else |
| { |
| variableValue = getPropertyValue(varName, true); |
| } |
| |
| if (variableValue != null) |
| { |
| value = value.replace("${" + varName + "}", variableValue); |
| } |
| startVar++; |
| } |
| return value; |
| } |
| |
| /** |
| * Resolve all values for the given key. |
| * |
| * @param key |
| * |
| * @return a List of all found property values, sorted by their ordinal in ascending order |
| * |
| * @see org.apache.deltaspike.core.spi.config.ConfigSource#getOrdinal() |
| */ |
| public static List<String> getAllPropertyValues(String key) |
| { |
| // must use a new list because Arrays.asList() is resistant to sorting on some JVMs: |
| List<ConfigSource> appConfigSources = sortAscending(new ArrayList<ConfigSource>( |
| Arrays.<ConfigSource> asList(getConfigSources()))); |
| List<String> result = new ArrayList<String>(); |
| |
| for (ConfigSource configSource : appConfigSources) |
| { |
| String value = configSource.getPropertyValue(key); |
| |
| if (value != null) |
| { |
| value = filterConfigValue(key, value); |
| if (!result.contains(value)) |
| { |
| result.add(value); |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Returns a Map of all properties from all scannable config sources. The values of the properties reflect the |
| * values that would be obtained by a call to {@link #getPropertyValue(java.lang.String)}, that is, the value of the |
| * property from the ConfigSource with the highest ordinal. |
| * |
| * @see ConfigSource#isScannable() |
| */ |
| public static Map<String, String> getAllProperties() |
| { |
| // must use a new list because Arrays.asList() is resistant to sorting on some JVMs: |
| List<ConfigSource> appConfigSources = sortAscending(new ArrayList<ConfigSource>( |
| Arrays.<ConfigSource> asList(getConfigSources()))); |
| Map<String, String> result = new HashMap<String, String>(); |
| |
| for (ConfigSource configSource : appConfigSources) |
| { |
| if (configSource.isScannable()) |
| { |
| result.putAll(configSource.getProperties()); |
| } |
| } |
| |
| return Collections.unmodifiableMap(result); |
| } |
| |
| public static synchronized ConfigSource[] getConfigSources() |
| { |
| ClassLoader currentClassLoader = ClassUtils.getClassLoader(null); |
| |
| ConfigSource[] appConfigSources = configSources.get(currentClassLoader); |
| |
| if (appConfigSources == null) |
| { |
| appConfigSources = sortDescending(resolveConfigSources()); |
| |
| if (LOG.isLoggable(Level.FINE)) |
| { |
| for (ConfigSource cs : appConfigSources) |
| { |
| LOG.log(Level.FINE, "Adding ordinal {0} ConfigSource {1}", |
| new Object[]{cs.getOrdinal(), cs.getConfigName()}); |
| } |
| } |
| |
| configSources.put(currentClassLoader, appConfigSources); |
| } |
| |
| return appConfigSources; |
| } |
| |
| private static List<ConfigSource> resolveConfigSources() |
| { |
| List<ConfigSource> appConfigSources = ServiceUtils.loadServiceImplementations(ConfigSource.class); |
| |
| List<ConfigSourceProvider> configSourceProviderServiceLoader = |
| ServiceUtils.loadServiceImplementations(ConfigSourceProvider.class); |
| |
| for (ConfigSourceProvider configSourceProvider : configSourceProviderServiceLoader) |
| { |
| appConfigSources.addAll(configSourceProvider.getConfigSources()); |
| } |
| |
| List<? extends ConfigFilter> configFilters = ServiceUtils.loadServiceImplementations(ConfigFilter.class); |
| for (ConfigFilter configFilter : configFilters) |
| { |
| addConfigFilter(configFilter); |
| } |
| |
| return appConfigSources; |
| } |
| |
| private static ConfigSource[] sortDescending(List<ConfigSource> configSources) |
| { |
| Collections.sort(configSources, new Comparator<ConfigSource>() |
| { |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int compare(ConfigSource configSource1, ConfigSource configSource2) |
| { |
| return (configSource1.getOrdinal() > configSource2.getOrdinal()) ? -1 : 1; |
| } |
| }); |
| return configSources.toArray(new ConfigSource[configSources.size()]); |
| } |
| |
| private static List<ConfigSource> sortAscending(List<ConfigSource> configSources) |
| { |
| Collections.sort(configSources, new Comparator<ConfigSource>() |
| { |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int compare(ConfigSource configSource1, ConfigSource configSource2) |
| { |
| return (configSource1.getOrdinal() > configSource2.getOrdinal()) ? 1 : -1; |
| } |
| }); |
| return configSources; |
| } |
| |
| private static ProjectStage getProjectStage() |
| { |
| if (projectStage == null) |
| { |
| synchronized (ConfigResolver.class) |
| { |
| projectStage = ProjectStageProducer.getInstance().getProjectStage(); |
| } |
| } |
| |
| return projectStage; |
| } |
| |
| private static <T> T fallbackToDefaultIfEmpty(String key, T value, T defaultValue) |
| { |
| if (value == null || (value instanceof String && ((String)value).isEmpty())) |
| { |
| LOG.log(Level.FINE, "no configured value found for key {0}, using default value {1}.", |
| new Object[]{key, defaultValue}); |
| |
| return defaultValue; |
| } |
| |
| return value; |
| } |
| |
| /** |
| * Filter the configured value. |
| * This can e.g. be used for decryption. |
| * @return the filtered value |
| */ |
| public static String filterConfigValue(String key, String value) |
| { |
| List<ConfigFilter> currentConfigFilters = getInternalConfigFilters(); |
| |
| String filteredValue = value; |
| |
| for (ConfigFilter filter : currentConfigFilters) |
| { |
| filteredValue = filter.filterValue(key, filteredValue); |
| } |
| return filteredValue; |
| } |
| |
| /** |
| * Filter the configured value for logging. |
| * This can e.g. be used for displaying ***** instead of a real password. |
| * @return the filtered value |
| */ |
| public static String filterConfigValueForLog(String key, String value) |
| { |
| List<ConfigFilter> currentConfigFilters = getInternalConfigFilters(); |
| |
| String logValue = value; |
| |
| for (ConfigFilter filter : currentConfigFilters) |
| { |
| logValue = filter.filterValueForLog(key, logValue); |
| } |
| return logValue; |
| } |
| |
| /** |
| * A very simple interface for conversion of configuration values from String to any Java type. |
| * @param <T> The target type of the configuration entry |
| */ |
| public interface Converter<T> |
| { |
| |
| /** |
| * Returns the converted value of the configuration entry. |
| * @param value The String property value to convert |
| * @return Converted value |
| */ |
| T convert(String value); |
| |
| } |
| |
| /** |
| * A builder-based typed resolution mechanism for configuration values. |
| * @param <T> The target type of the configuration entry. |
| */ |
| public interface TypedResolver<T> |
| { |
| |
| /** |
| * Appends the resolved value of the given property to the key of this builder. This is described in more detail |
| * in {@link ConfigResolver#getPropertyAwarePropertyValue(String, String)}. |
| * @param propertyName The name of the parameter property |
| * @return This builder |
| */ |
| TypedResolver<T> parameterizedBy(String propertyName); |
| |
| /** |
| * Indicates whether to append the name of the current project stage to the key of this builder. This |
| * is described in more detail in {@link ConfigResolver#getProjectStageAwarePropertyValue(String)}. True by |
| * default. |
| * @param with |
| * @return This builder |
| */ |
| TypedResolver<T> withCurrentProjectStage(boolean with); |
| |
| /** |
| * Indicates whether the fallback resolution sequence should be performed, as described in |
| * {@link ConfigResolver#getPropertyAwarePropertyValue(String, String)}. This applies only when |
| * {@link #parameterizedBy(String)} or {@link #withCurrentProjectStage(boolean)} is used. |
| * @param strictly |
| * @return This builder |
| */ |
| TypedResolver<T> strictly(boolean strictly); |
| |
| /** |
| * Sets the default value to use in case the resolution returns null. |
| * @param value the default value |
| * @return This builder |
| */ |
| TypedResolver<T> withDefault(T value); |
| |
| /** |
| * Sets the default value to use in case the resolution returns null. Converts the given String to the type of |
| * this resolver using the same method as used for the configuration entries. |
| * @param value string value to be converted and used as default |
| * @return This builder |
| */ |
| TypedResolver<T> withStringDefault(String value); |
| |
| /** |
| * Specify that a resolved value will get cached for a certain amount of time. |
| * After the time expires the next {@link #getValue()} will again resolve the value |
| * from the underlying {@link ConfigResolver}. |
| * |
| * @param timeUnit the TimeUnit for the value |
| * @param value the amount of the TimeUnit to wait |
| * @return This builder |
| */ |
| TypedResolver<T> cacheFor(TimeUnit timeUnit, long value); |
| |
| /** |
| * Whether to evaluate variables in configured values. |
| * A variable starts with '${' and ends with '}', e.g. |
| * <pre> |
| * mycompany.some.url=${myserver.host}/some/path |
| * myserver.host=http://localhost:8081 |
| * </pre> |
| * If 'evaluateVariables' is enabled, the result for the above key |
| * {@code "mycompany.some.url"} would be: |
| * {@code "http://localhost:8081/some/path"} |
| * @param evaluateVariables whether to evaluate variables in values or not |
| * @return This builder |
| */ |
| TypedResolver<T> evaluateVariables(boolean evaluateVariables); |
| |
| /** |
| * Whether to log picking up any value changes as INFO. |
| * |
| * @return This builder |
| */ |
| TypedResolver<T> logChanges(boolean logChanges); |
| |
| /** |
| * Returns the converted resolved filtered value. |
| * @return the resolved value |
| */ |
| T getValue(); |
| |
| /** |
| * Returns the key given in {@link #resolve(String)}. |
| * @return the original key |
| */ |
| String getKey(); |
| |
| /** |
| * Returns the actual key which led to successful resolution and corresponds to the resolved value. This applies |
| * only when {@link #parameterizedBy(String)} or {@link #withCurrentProjectStage(boolean)} is used and |
| * {@link #strictly(boolean)} is not used, otherwise the resolved key should always be equal to the original |
| * key. This method is provided for cases, when projectStage-aware and/or parameterized resolution is |
| * requested but the value for such appended key is not found and some of the fallback keys is used, as |
| * described in {@link ConfigResolver#getPropertyAwarePropertyValue(String, String)}. |
| * This should be called only after calling {@link #getValue()} otherwise the value is undefined (but likely |
| * null). |
| * @return |
| */ |
| String getResolvedKey(); |
| |
| /** |
| * Returns the default value provided by {@link #withDefault(Object)} or {@link #withStringDefault(String)}. |
| * Returns null if no default was provided. |
| * @return the default value or null |
| */ |
| T getDefaultValue(); |
| |
| } |
| |
| /** |
| * A builder-based optionally typed resolution mechanism for configuration values. |
| * @param <T> This type variable should always be String for UntypedResolver. |
| */ |
| public interface UntypedResolver<T> extends TypedResolver<T> |
| { |
| /** |
| * Sets the type of the configuration entry to the given class and returns this builder as a TypedResolver. |
| * Only one of the supported types should be used which includes: Boolean, Class, Integer, Long, Float, Double. |
| * For custom types, see {@link #as(Class, Converter)}. |
| * @param clazz The target type |
| * @param <N> The target type |
| * @return This builder as a TypedResolver |
| */ |
| <N> TypedResolver<N> as(Class<N> clazz); |
| |
| /** |
| * Sets the type of the configuration entry to the given class, sets the converter to the one given and |
| * returns this builder as a TypedResolver. If a converter is provided for one of the types supported by |
| * default (see {@link #as(Class)} then the provided converter is used instead of the built-in one. |
| * @param clazz The target type |
| * @param converter The converter for the target type |
| * @param <N> The target type |
| * @return This builder as a TypedResolver |
| */ |
| <N> TypedResolver<N> as(Class<N> clazz, Converter<N> converter); |
| |
| } |
| |
| /** |
| * The entry point to the builder-based optionally typed configuration resolution mechanism. |
| * |
| * String is the default type for configuration entries and is not considered a 'type' by this resolver. Therefore |
| * an UntypedResolver is returned by this method. To convert the configuration value to another type, call |
| * {@link UntypedResolver#as(Class)}. |
| * |
| * @param name The property key to resolve |
| * @return A builder for configuration resolution. |
| */ |
| public static UntypedResolver<String> resolve(String name) |
| { |
| return new PropertyBuilder<String>(name); |
| } |
| |
| private static class PropertyBuilder<T> implements UntypedResolver<T> |
| { |
| |
| private String keyOriginal; |
| |
| private String keyResolved; |
| |
| private Class<?> configEntryType = String.class; |
| |
| private boolean withDefault = false; |
| private T defaultValue; |
| |
| private boolean projectStageAware = true; |
| |
| private String propertyParameter; |
| |
| private String parameterValue; |
| |
| private boolean strictly = false; |
| |
| private Converter<?> converter; |
| |
| private boolean evaluateVariables = false; |
| private boolean logChanges = false; |
| |
| private long cacheTimeMs = -1; |
| private volatile long reloadAfter = -1; |
| private T lastValue = null; |
| |
| |
| private PropertyBuilder() |
| { |
| } |
| |
| protected PropertyBuilder(String propertyName) |
| { |
| this.keyOriginal = propertyName; |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public <N> TypedResolver<N> as(Class<N> clazz) |
| { |
| configEntryType = clazz; |
| return (TypedResolver<N>) this; |
| } |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public <N> TypedResolver<N> as(Class<N> clazz, Converter<N> converter) |
| { |
| configEntryType = clazz; |
| this.converter = converter; |
| |
| return (TypedResolver<N>) this; |
| } |
| |
| @Override |
| public TypedResolver<T> withDefault(T value) |
| { |
| defaultValue = value; |
| withDefault = true; |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> withStringDefault(String value) |
| { |
| if (value == null || value.isEmpty()) |
| { |
| throw new RuntimeException("Empty String or null supplied as string-default value for property " |
| + keyOriginal); |
| } |
| |
| defaultValue = convert(value); |
| withDefault = true; |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> cacheFor(TimeUnit timeUnit, long value) |
| { |
| this.cacheTimeMs = timeUnit.toMillis(value); |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> parameterizedBy(String propertyName) |
| { |
| this.propertyParameter = propertyName; |
| |
| if (propertyParameter != null && !propertyParameter.isEmpty()) |
| { |
| String parameterValue = ConfigResolver |
| .resolve(propertyParameter) |
| .withCurrentProjectStage(projectStageAware) |
| .getValue(); |
| |
| if (parameterValue != null && !parameterValue.isEmpty()) |
| { |
| this.parameterValue = parameterValue; |
| } |
| } |
| |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> withCurrentProjectStage(boolean with) |
| { |
| this.projectStageAware = with; |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> strictly(boolean strictly) |
| { |
| this.strictly = strictly; |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> evaluateVariables(boolean evaluateVariables) |
| { |
| this.evaluateVariables = evaluateVariables; |
| return this; |
| } |
| |
| @Override |
| public TypedResolver<T> logChanges(boolean logChanges) |
| { |
| this.logChanges = logChanges; |
| return this; |
| } |
| |
| @Override |
| public T getValue() |
| { |
| long now = -1; |
| if (cacheTimeMs > 0) |
| { |
| now = System.currentTimeMillis(); |
| if (now <= reloadAfter) |
| { |
| return lastValue; |
| } |
| } |
| |
| String valueStr = resolveStringValue(); |
| T value = convert(valueStr); |
| |
| if (withDefault) |
| { |
| value = fallbackToDefaultIfEmpty(keyResolved, value, defaultValue); |
| } |
| |
| if (logChanges && (value != null && !value.equals(lastValue) || (value == null && lastValue != null)) ) |
| { |
| LOG.log(Level.INFO, "New value {0} for key {1}.", |
| new Object[]{filterConfigValueForLog(keyOriginal, valueStr), keyOriginal}); |
| } |
| |
| lastValue = value; |
| |
| if (cacheTimeMs > 0) |
| { |
| reloadAfter = now + cacheTimeMs; |
| } |
| |
| return value; |
| } |
| |
| @Override |
| public String getKey() |
| { |
| return keyOriginal; |
| } |
| |
| @Override |
| public String getResolvedKey() |
| { |
| return keyResolved; |
| } |
| |
| @Override |
| public T getDefaultValue() |
| { |
| return defaultValue; |
| } |
| |
| /** |
| * Performs the resolution cascade |
| */ |
| private String resolveStringValue() |
| { |
| ProjectStage ps = null; |
| String value = null; |
| keyResolved = keyOriginal; |
| int keySuffices = 0; |
| |
| // make the longest key |
| // first, try appending resolved parameter |
| if (propertyParameter != null && !propertyParameter.isEmpty()) |
| { |
| if (parameterValue != null && !parameterValue.isEmpty()) |
| { |
| keyResolved += "." + parameterValue; |
| keySuffices++; |
| } |
| // if parameter value can't be resolved and strictly |
| else if (strictly) |
| { |
| return null; |
| } |
| } |
| |
| // try appending projectstage |
| if (projectStageAware) |
| { |
| ps = getProjectStage(); |
| keyResolved += "." + ps; |
| keySuffices++; |
| } |
| |
| // make initial resolution of longest key |
| value = getPropertyValue(keyResolved, evaluateVariables); |
| |
| // try fallbacks if not strictly |
| if (value == null && !strictly) |
| { |
| |
| // by the length of the longest resolved key already tried |
| // breaks are left out intentionally |
| switch (keySuffices) |
| { |
| |
| case 2: |
| // try base.param |
| keyResolved = keyOriginal + "." + parameterValue; |
| value = getPropertyValue(keyResolved, null, evaluateVariables); |
| |
| if (value != null) |
| { |
| return value; |
| } |
| |
| // try base.ps |
| ps = getProjectStage(); |
| keyResolved = keyOriginal + "." + ps; |
| value = getPropertyValue(keyResolved, null, evaluateVariables); |
| |
| if (value != null) |
| { |
| return value; |
| } |
| |
| case 1: |
| // try base |
| keyResolved = keyOriginal; |
| value = getPropertyValue(keyResolved, null, evaluateVariables); |
| return value; |
| |
| default: |
| // the longest key was the base, no fallback |
| return null; |
| } |
| } |
| |
| return value; |
| } |
| |
| /** |
| * If a converter was provided for this builder, it takes precedence over the built-in converters. |
| */ |
| private T convert(String value) |
| { |
| |
| if (value == null) |
| { |
| return null; |
| } |
| |
| Object result = null; |
| |
| if (this.converter != null) |
| { |
| try |
| { |
| result = converter.convert(value); |
| } |
| catch (Exception e) |
| { |
| throw ExceptionUtils.throwAsRuntimeException(e); |
| } |
| } |
| else if (String.class.equals(configEntryType)) |
| { |
| result = value; |
| } |
| else if (Class.class.equals(configEntryType)) |
| { |
| result = ClassUtils.tryToLoadClassForName(value); |
| } |
| else if (Boolean.class.equals(configEntryType)) |
| { |
| Boolean isTrue = "TRUE".equalsIgnoreCase(value); |
| isTrue |= "1".equalsIgnoreCase(value); |
| isTrue |= "YES".equalsIgnoreCase(value); |
| isTrue |= "Y".equalsIgnoreCase(value); |
| isTrue |= "JA".equalsIgnoreCase(value); |
| isTrue |= "J".equalsIgnoreCase(value); |
| isTrue |= "OUI".equalsIgnoreCase(value); |
| |
| result = isTrue; |
| } |
| else if (Integer.class.equals(configEntryType)) |
| { |
| result = Integer.parseInt(value); |
| } |
| else if (Long.class.equals(configEntryType)) |
| { |
| result = Long.parseLong(value); |
| } |
| else if (Float.class.equals(configEntryType)) |
| { |
| result = Float.parseFloat(value); |
| } |
| else if (Double.class.equals(configEntryType)) |
| { |
| result = Double.parseDouble(value); |
| } |
| |
| return (T) result; |
| } |
| |
| } |
| |
| } |