/* * Copyright 2010 Proofpoint, Inc. * * 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. */ package io.airlift.configuration; import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; import com.google.inject.Binding; import com.google.inject.ConfigurationException; import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.spi.DefaultElementVisitor; import com.google.inject.spi.Element; import com.google.inject.spi.Elements; import com.google.inject.spi.InstanceBinding; import com.google.inject.spi.Message; import com.google.inject.spi.ProviderInstanceBinding; import io.airlift.configuration.ConfigurationMetadata.AttributeMetadata; import org.apache.bval.jsr.ApacheValidationProvider; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.inject.Provider; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; import java.util.function.Function; import static com.google.common.base.CaseFormat.LOWER_CAMEL; import static com.google.common.base.CaseFormat.UPPER_CAMEL; import static com.google.common.collect.Sets.newConcurrentHashSet; import static io.airlift.configuration.ConfigurationMetadata.getConfigurationMetadata; import static io.airlift.configuration.Problems.exceptionFor; import static java.lang.String.format; import static java.util.Objects.requireNonNull; public class ConfigurationFactory { @GuardedBy("VALIDATOR") private static final Validator VALIDATOR; static { // this prevents bval from using the thread context classloader ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(null); VALIDATOR = Validation.byProvider(ApacheValidationProvider.class).configure().buildValidatorFactory().getValidator(); } finally { Thread.currentThread().setContextClassLoader(currentClassLoader); } } private final Map<String, String> properties; private final WarningsMonitor warningsMonitor; private final Problems.Monitor monitor; private final ConcurrentMap<ConfigurationProvider<?>, Object> instanceCache = new ConcurrentHashMap<>(); private final Set<String> usedProperties = newConcurrentHashSet(); private final Set<ConfigurationProvider<?>> registeredProviders = newConcurrentHashSet(); @GuardedBy("this") private final List<Consumer<ConfigurationProvider<?>>> configurationBindingListeners = new ArrayList<>(); private final ListMultimap<Key<?>, ConfigDefaultsHolder<?>> registeredDefaultConfigs = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); private final LoadingCache<Class<?>, ConfigurationMetadata<?>> metadataCache = CacheBuilder.newBuilder() .build(new CacheLoader<Class<?>, ConfigurationMetadata<?>>() { @Override public ConfigurationMetadata<?> load(Class<?> configClass) { return getConfigurationMetadata(configClass, monitor); } }); public ConfigurationFactory(Map<String, String> properties) { this(properties, null, Problems.NULL_MONITOR); } public ConfigurationFactory(Map<String, String> properties, WarningsMonitor warningsMonitor) { this(properties, warningsMonitor, Problems.NULL_MONITOR); } @VisibleForTesting ConfigurationFactory(Map<String, String> properties, WarningsMonitor warningsMonitor, Problems.Monitor monitor) { this.properties = ImmutableMap.copyOf(properties); this.warningsMonitor = warningsMonitor; this.monitor = monitor; } public Map<String, String> getProperties() { return properties; } /** * Marks the specified property as consumed. */ @Beta public void consumeProperty(String property) { requireNonNull(property, "property is null"); usedProperties.add(property); } public Set<String> getUsedProperties() { return ImmutableSortedSet.copyOf(usedProperties); } /** * Registers all configuration classes in the module so they can be part of configuration inspection. */ @Beta public void registerConfigurationClasses(Module module) { registerConfigurationClasses(ImmutableList.of(module)); } public void registerConfigurationClasses(Collection<? extends Module> modules) { // some modules need access to configuration factory so they can lazy register additional config classes // initialize configuration factory modules.stream() .filter(ConfigurationAwareModule.class::isInstance) .map(ConfigurationAwareModule.class::cast) .forEach(module -> module.setConfigurationFactory(this)); for (Element element : Elements.getElements(modules)) { element.acceptVisitor(new DefaultElementVisitor<Void>() { @Override public <T> Void visit(Binding<T> binding) { if (binding instanceof InstanceBinding) { InstanceBinding<T> instanceBinding = (InstanceBinding<T>) binding; // configuration listener if (instanceBinding.getInstance() instanceof ConfigurationBindingListenerHolder) { addConfigurationBindingListener(((ConfigurationBindingListenerHolder) instanceBinding.getInstance()).getConfigurationBindingListener()); } // config defaults if (instanceBinding.getInstance() instanceof ConfigDefaultsHolder) { registerConfigDefaults((ConfigDefaultsHolder<?>) instanceBinding.getInstance()); } } // configuration provider if (binding instanceof ProviderInstanceBinding) { ProviderInstanceBinding<?> providerInstanceBinding = (ProviderInstanceBinding<?>) binding; Provider<?> provider = providerInstanceBinding.getUserSuppliedProvider(); if (provider instanceof ConfigurationProvider) { registerConfigurationProvider((ConfigurationProvider<?>) provider, Optional.of(binding.getSource())); } } return null; } }); } } void registerConfigurationProvider(ConfigurationProvider<?> configurationProvider, Optional<Object> bindingSource) { configurationProvider.setConfigurationFactory(this); configurationProvider.setBindingSource(bindingSource); ImmutableList<Consumer<ConfigurationProvider<?>>> listeners = ImmutableList.of(); synchronized (this) { if (registeredProviders.add(configurationProvider)) { listeners = ImmutableList.copyOf(configurationBindingListeners); } } listeners.forEach(listener -> listener.accept(configurationProvider)); } public void addConfigurationBindingListener(ConfigurationBindingListener listener) { ConfigurationProviderConsumer consumer = new ConfigurationProviderConsumer(listener); ImmutableSet<ConfigurationProvider<?>> currentProviders; synchronized (this) { configurationBindingListeners.add(consumer); currentProviders = ImmutableSet.copyOf(registeredProviders); } currentProviders.forEach(consumer); } public List<Message> validateRegisteredConfigurationProvider() { List<Message> messages = new ArrayList<>(); for (ConfigurationProvider<?> configurationProvider : ImmutableList.copyOf(registeredProviders)) { try { // call the getter which will cause object creation configurationProvider.get(); } catch (ConfigurationException e) { // if we got errors, add them to the errors list ImmutableList<Object> sources = configurationProvider.getBindingSource().map(ImmutableList::of).orElse(ImmutableList.of()); for (Message message : e.getErrorMessages()) { messages.add(new Message(sources, message.getMessage(), message.getCause())); } } } return messages; } Iterable<ConfigurationProvider<?>> getConfigurationProviders() { return ImmutableList.copyOf(registeredProviders); } <T> void registerConfigDefaults(ConfigDefaultsHolder<T> holder) { registeredDefaultConfigs.put(holder.getConfigKey(), holder); } private <T> ConfigDefaults<T> getConfigDefaults(Key<T> key) { ImmutableList.Builder<ConfigDefaults<T>> defaults = ImmutableList.builder(); Key<?> globalDefaults = Key.get(key.getTypeLiteral(), GlobalDefaults.class); registeredDefaultConfigs.get(globalDefaults).stream() .map(ConfigurationFactory.<T>castHolder()) .sorted() .map(ConfigDefaultsHolder::getConfigDefaults) .forEach(defaults::add); registeredDefaultConfigs.get(key).stream() .map(ConfigurationFactory.<T>castHolder()) .sorted() .map(ConfigDefaultsHolder::getConfigDefaults) .forEach(defaults::add); return ConfigDefaults.configDefaults(defaults.build()); } @SuppressWarnings("unchecked") private static <T> Function<ConfigDefaultsHolder<?>, ConfigDefaultsHolder<T>> castHolder() { return holder -> (ConfigDefaultsHolder<T>) holder; } <T> T getDefaultConfig(Key<T> key) { ConfigurationMetadata<T> configurationMetadata = getMetadata(key); configurationMetadata.getProblems().throwIfHasErrors(); T instance = newInstance(configurationMetadata); ConfigDefaults<T> configDefaults = getConfigDefaults(key); configDefaults.setDefaults(instance); return instance; } public <T> T build(Class<T> configClass) { return build(configClass, null); } public <T> T build(Class<T> configClass, @Nullable String prefix) { return build(configClass, Optional.ofNullable(prefix), ConfigDefaults.noDefaults()).getInstance(); } /** * This is used by the configuration provider */ <T> T build(ConfigurationProvider<T> configurationProvider) { requireNonNull(configurationProvider, "configurationProvider"); registerConfigurationProvider(configurationProvider, Optional.empty()); // check for a prebuilt instance T instance = getCachedInstance(configurationProvider); if (instance != null) { return instance; } ConfigurationBinding<T> configurationBinding = configurationProvider.getConfigurationBinding(); ConfigurationHolder<T> holder = build(configurationBinding.getConfigClass(), configurationBinding.getPrefix(), getConfigDefaults(configurationBinding.getKey())); instance = holder.getInstance(); // inform caller about warnings if (warningsMonitor != null) { for (Message message : holder.getProblems().getWarnings()) { warningsMonitor.onWarning(message.toString()); } } // add to instance cache T existingValue = putCachedInstance(configurationProvider, instance); // if key was already associated with a value, there was a // creation race and we lost. Just use the winners' instance; if (existingValue != null) { return existingValue; } return instance; } @SuppressWarnings("unchecked") private <T> T getCachedInstance(ConfigurationProvider<T> configurationProvider) { return (T) instanceCache.get(configurationProvider); } @SuppressWarnings("unchecked") private <T> T putCachedInstance(ConfigurationProvider<T> configurationProvider, T instance) { return (T) instanceCache.putIfAbsent(configurationProvider, instance); } private <T> ConfigurationHolder<T> build(Class<T> configClass, Optional<String> configPrefix, ConfigDefaults<T> configDefaults) { if (configClass == null) { throw new NullPointerException("configClass is null"); } String prefix = configPrefix .map(value -> value + ".") .orElse(""); ConfigurationMetadata<T> configurationMetadata = getMetadata(configClass); configurationMetadata.getProblems().throwIfHasErrors(); T instance = newInstance(configurationMetadata); configDefaults.setDefaults(instance); Problems problems = new Problems(monitor); for (AttributeMetadata attribute : configurationMetadata.getAttributes().values()) { try { setConfigProperty(instance, attribute, prefix, problems); } catch (InvalidConfigurationException e) { problems.addError(e.getCause(), e.getMessage()); } } // Check that none of the defunct properties are still in use if (configClass.isAnnotationPresent(DefunctConfig.class)) { for (String value : configClass.getAnnotation(DefunctConfig.class).value()) { if (!value.isEmpty() && properties.get(prefix + value) != null) { problems.addError("Defunct property '%s' (class [%s]) cannot be configured.", value, configClass.toString()); } } } for (ConstraintViolation<?> violation : validate(instance)) { String propertyFieldName = violation.getPropertyPath().toString(); // upper case first character to match config attribute name String attributeName = LOWER_CAMEL.to(UPPER_CAMEL, propertyFieldName); AttributeMetadata attribute = configurationMetadata.getAttributes().get(attributeName); if (attribute != null && attribute.getInjectionPoint() != null) { String propertyName = attribute.getInjectionPoint().getProperty(); if (!prefix.isEmpty()) { propertyName = prefix + "." + propertyName; } problems.addError("Invalid configuration property %s: %s (for class %s.%s)", propertyName, violation.getMessage(), configClass.getName(), violation.getPropertyPath()); } else { problems.addError("Invalid configuration property with prefix '%s': %s (for class %s.%s)", prefix, violation.getMessage(), configClass.getName(), violation.getPropertyPath()); } } problems.throwIfHasErrors(); return new ConfigurationHolder<>(instance, problems); } private static <T> Set<ConstraintViolation<T>> validate(T instance) { synchronized (VALIDATOR) { return VALIDATOR.validate(instance); } } @SuppressWarnings("unchecked") private <T> ConfigurationMetadata<T> getMetadata(Key<T> key) { return getMetadata((Class<T>) key.getTypeLiteral().getRawType()); } @SuppressWarnings("unchecked") private <T> ConfigurationMetadata<T> getMetadata(Class<T> configClass) { return (ConfigurationMetadata<T>) metadataCache.getUnchecked(configClass); } private static <T> T newInstance(ConfigurationMetadata<T> configurationMetadata) { try { return configurationMetadata.getConstructor().newInstance(); } catch (Throwable e) { if (e instanceof InvocationTargetException && e.getCause() != null) { e = e.getCause(); } throw exceptionFor(e, "Error creating instance of configuration class [%s]", configurationMetadata.getConfigClass().getName()); } } private <T> void setConfigProperty(T instance, AttributeMetadata attribute, String prefix, Problems problems) throws InvalidConfigurationException { // Get property value ConfigurationMetadata.InjectionPointMetaData injectionPoint = findOperativeInjectionPoint(attribute, prefix, problems); // If we did not get an injection point, do not call the setter if (injectionPoint == null) { return; } if (injectionPoint.getSetter().isAnnotationPresent(Deprecated.class)) { problems.addWarning("Configuration property '%s' is deprecated and should not be used", injectionPoint.getProperty()); } Object value = getInjectedValue(injectionPoint, prefix); try { injectionPoint.getSetter().invoke(instance, value); } catch (Throwable e) { if (e instanceof InvocationTargetException && e.getCause() != null) { e = e.getCause(); } throw new InvalidConfigurationException(e, "Error invoking configuration method [%s]", injectionPoint.getSetter().toGenericString()); } } private ConfigurationMetadata.InjectionPointMetaData findOperativeInjectionPoint(AttributeMetadata attribute, String prefix, Problems problems) throws ConfigurationException { ConfigurationMetadata.InjectionPointMetaData operativeInjectionPoint = attribute.getInjectionPoint(); String operativeName = null; String operativeValue = null; if (operativeInjectionPoint != null) { operativeName = prefix + operativeInjectionPoint.getProperty(); operativeValue = properties.get(operativeName); } for (ConfigurationMetadata.InjectionPointMetaData injectionPoint : attribute.getLegacyInjectionPoints()) { String fullName = prefix + injectionPoint.getProperty(); String value = properties.get(fullName); if (value != null) { String replacement = "deprecated."; if (attribute.getInjectionPoint() != null) { replacement = format("replaced. Use '%s' instead.", prefix + attribute.getInjectionPoint().getProperty()); } problems.addWarning("Configuration property '%s' has been " + replacement, fullName); if (operativeValue == null) { operativeInjectionPoint = injectionPoint; operativeValue = value; operativeName = fullName; } else if (!value.equals(operativeValue)) { problems.addError("Value for property '%s' (=%s) conflicts with property '%s' (=%s)", fullName, value, operativeName, operativeValue); } } } problems.throwIfHasErrors(); if (operativeValue == null) { // No injection from configuration return null; } return operativeInjectionPoint; } private Object getInjectedValue(ConfigurationMetadata.InjectionPointMetaData injectionPoint, String prefix) throws InvalidConfigurationException { // Get the property value String name = prefix + injectionPoint.getProperty(); String value = properties.get(name); if (value == null) { return null; } // coerce the property value to the final type Class<?> propertyType = injectionPoint.getSetter().getParameterTypes()[0]; Object finalValue = coerce(propertyType, value); if (finalValue == null) { throw new InvalidConfigurationException(format("Could not coerce value '%s' to %s (property '%s') in order to call [%s]", value, propertyType.getName(), injectionPoint.getProperty(), injectionPoint.getSetter().toGenericString())); } usedProperties.add(name); return finalValue; } private static Object coerce(Class<?> type, String value) { if (type.isPrimitive() && value == null) { return null; } try { if (String.class.isAssignableFrom(type)) { return value; } else if (Boolean.class.isAssignableFrom(type) || Boolean.TYPE.isAssignableFrom(type)) { return Boolean.valueOf(value); } else if (Byte.class.isAssignableFrom(type) || Byte.TYPE.isAssignableFrom(type)) { return Byte.valueOf(value); } else if (Short.class.isAssignableFrom(type) || Short.TYPE.isAssignableFrom(type)) { return Short.valueOf(value); } else if (Integer.class.isAssignableFrom(type) || Integer.TYPE.isAssignableFrom(type)) { return Integer.valueOf(value); } else if (Long.class.isAssignableFrom(type) || Long.TYPE.isAssignableFrom(type)) { return Long.valueOf(value); } else if (Float.class.isAssignableFrom(type) || Float.TYPE.isAssignableFrom(type)) { return Float.valueOf(value); } else if (Double.class.isAssignableFrom(type) || Double.TYPE.isAssignableFrom(type)) { return Double.valueOf(value); } } catch (Exception ignored) { // ignore the random exceptions from the built in types return null; } // Look for a static fromString(String) method try { Method fromString = type.getMethod("fromString", String.class); if (fromString.getReturnType().isAssignableFrom(type)) { return fromString.invoke(null, value); } } catch (Throwable ignored) { } // Look for a static valueOf(String) method try { Method valueOf = type.getMethod("valueOf", String.class); if (valueOf.getReturnType().isAssignableFrom(type)) { return valueOf.invoke(null, value); } } catch (Throwable ignored) { } // Look for a constructor taking a string try { Constructor<?> constructor = type.getConstructor(String.class); return constructor.newInstance(value); } catch (Throwable ignored) { } return null; } private static class ConfigurationHolder<T> { private final T instance; private final Problems problems; private ConfigurationHolder(T instance, Problems problems) { this.instance = instance; this.problems = problems; } public T getInstance() { return instance; } public Problems getProblems() { return problems; } } private class ConfigurationProviderConsumer implements Consumer<ConfigurationProvider<?>> { private final ConfigurationBindingListener listener; private final ConfigBinder configBinder; public ConfigurationProviderConsumer(ConfigurationBindingListener listener) { this.listener = listener; this.configBinder = ConfigBinder.configBinder(ConfigurationFactory.this, Optional.of(listener)); } @Override public void accept(ConfigurationProvider<?> configurationProvider) { listener.configurationBound(configurationProvider.getConfigurationBinding(), configBinder); } } }