/* * 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.base.Preconditions; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.inject.ConfigurationException; import io.airlift.configuration.Problems.Monitor; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.google.common.base.MoreObjects.toStringHelper; import static java.util.Objects.requireNonNull; public class ConfigurationMetadata<T> { public static <T> ConfigurationMetadata<T> getValidConfigurationMetadata(Class<T> configClass) throws ConfigurationException { return getValidConfigurationMetadata(configClass, Problems.NULL_MONITOR); } static <T> ConfigurationMetadata<T> getValidConfigurationMetadata(Class<T> configClass, Problems.Monitor monitor) throws ConfigurationException { ConfigurationMetadata<T> metadata = getConfigurationMetadata(configClass, monitor); metadata.getProblems().throwIfHasErrors(); return metadata; } public static <T> ConfigurationMetadata<T> getConfigurationMetadata(Class<T> configClass) { return getConfigurationMetadata(configClass, Problems.NULL_MONITOR); } static <T> ConfigurationMetadata<T> getConfigurationMetadata(Class<T> configClass, Problems.Monitor monitor) { return new ConfigurationMetadata<>(configClass, monitor); } private final Class<T> configClass; private final Problems problems; private final Constructor<T> constructor; private final Map<String, AttributeMetadata> attributes; private final Set<String> defunctConfig; private ConfigurationMetadata(Class<T> configClass, Monitor monitor) { if (configClass == null) { throw new NullPointerException("configClass is null"); } this.problems = new Problems(monitor); this.configClass = configClass; if (Modifier.isAbstract(configClass.getModifiers())) { problems.addError("Config class [%s] is abstract", configClass.getName()); } if (!Modifier.isPublic(configClass.getModifiers())) { problems.addError("Config class [%s] is not public", configClass.getName()); } this.defunctConfig = new HashSet<>(); if (configClass.isAnnotationPresent(DefunctConfig.class)) { DefunctConfig defunctConfig = configClass.getAnnotation(DefunctConfig.class); if (defunctConfig.value().length < 1) { problems.addError("@DefunctConfig annotation on class [%s] is empty", configClass.getName()); } for (String defunct : configClass.getAnnotation(DefunctConfig.class).value()) { if (defunct.isEmpty()) { problems.addError("@DefunctConfig annotation on class [%s] contains empty values", configClass.getName()); } else if (!this.defunctConfig.add(defunct)) { problems.addError("Defunct property '%s' is listed more than once in @DefunctConfig for class [%s]", defunct, configClass.getName()); } } } // verify there is a public no-arg constructor Constructor<T> constructor = null; try { constructor = configClass.getDeclaredConstructor(); if (!Modifier.isPublic(constructor.getModifiers())) { problems.addError("Constructor [%s] is not public", constructor.toGenericString()); } } catch (Exception e) { problems.addError("Configuration class [%s] does not have a public no-arg constructor", configClass.getName()); } this.constructor = constructor; this.attributes = ImmutableSortedMap.copyOf(buildAttributeMetadata(configClass)); // find invalid config methods not skipped by findConfigMethods() for (Class<?> clazz = configClass; (clazz != null) && !clazz.equals(Object.class); clazz = clazz.getSuperclass()) { for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(Config.class)) { if (!Modifier.isPublic(method.getModifiers())) { problems.addError("@Config method [%s] is not public", method.toGenericString()); } if (Modifier.isStatic(method.getModifiers())) { problems.addError("@Config method [%s] is static", method.toGenericString()); } } } } if (problems.getErrors().isEmpty() && this.attributes.isEmpty()) { problems.addError("Configuration class [%s] does not have any @Config annotations", configClass.getName()); } } public Class<T> getConfigClass() { return configClass; } public Constructor<T> getConstructor() { return constructor; } public Map<String, AttributeMetadata> getAttributes() { return attributes; } Problems getProblems() { return problems; } private boolean validateAnnotations(Method configMethod) { Config config = configMethod.getAnnotation(Config.class); LegacyConfig legacyConfig = configMethod.getAnnotation(LegacyConfig.class); if (config == null) { problems.addError("Method [%s] must have @Config annotation", configMethod.toGenericString()); return false; } boolean isValid = true; if (config.value().isEmpty()) { problems.addError("@Config method [%s] annotation has an empty value", configMethod.toGenericString()); isValid = false; } if (legacyConfig != null) { if (legacyConfig.value().length == 0) { problems.addError("@LegacyConfig method [%s] annotation has an empty list", configMethod.toGenericString()); isValid = false; } if (!legacyConfig.replacedBy().isEmpty()) { problems.addError("@Config method [%s] has annotation claiming to be replaced by another property ('%s')", configMethod.toGenericString(), legacyConfig.replacedBy()); isValid = false; } for (String arrayEntry : legacyConfig.value()) { if (arrayEntry == null || arrayEntry.isEmpty()) { problems.addError("@LegacyConfig method [%s] annotation contains null or empty value", configMethod.toGenericString()); isValid = false; } else if (arrayEntry.equals(config.value())) { problems.addError("@Config property name '%s' appears in @LegacyConfig annotation for method [%s]", config.value(), configMethod.toGenericString()); isValid = false; } } } return isValid; } private boolean validateSetter(Method method) { if (method == null) { return false; } if (!method.getName().startsWith("set")) { problems.addError("Method [%s] is not a valid setter (e.g. setFoo) for configuration annotation", method.toGenericString()); return false; } if (method.getParameterTypes().length != 1) { problems.addError("Configuration setter method [%s] does not have exactly one parameter", method.toGenericString()); return false; } return true; } private Map<String, AttributeMetadata> buildAttributeMetadata(Class<T> configClass) { Map<String, AttributeMetadata> attributes = new HashMap<>(); for (Method configMethod : findConfigMethods(configClass)) { AttributeMetadata attribute = buildAttributeMetadata(configClass, configMethod); if (attribute != null) { if (attributes.containsKey(attribute.getName())) { problems.addError("Configuration class [%s] Multiple methods are annotated for @Config attribute [%s]", configClass.getName(), attribute.getName()); } attributes.put(attribute.getName(), attribute); } } // Find orphan @LegacyConfig methods, in order to report errors Collection<Method> legacyMethods = findLegacyConfigMethods(configClass); for (AttributeMetadata attribute : attributes.values()) { for (InjectionPointMetaData injectionPoint : attribute.getLegacyInjectionPoints()) { if (legacyMethods.contains(injectionPoint.getSetter())) { // Don't care about legacy methods which are related to current attributes legacyMethods.remove(injectionPoint.getSetter()); } } } for (Method method : legacyMethods) { if (!method.isAnnotationPresent(Config.class)) { validateSetter(method); problems.addError("@LegacyConfig method [%s] is not associated with any valid @Config attribute.", method.toGenericString()); } } return attributes; } private AttributeMetadata buildAttributeMetadata(Class<T> configClass, Method configMethod) { Preconditions.checkArgument(configMethod.isAnnotationPresent(Config.class)); if (!validateAnnotations(configMethod)) { return null; } String propertyName = configMethod.getAnnotation(Config.class).value(); // verify parameters if (!validateSetter(configMethod)) { return null; } // determine the attribute name String attributeName = configMethod.getName().substring(3); AttributeMetaDataBuilder builder = new AttributeMetaDataBuilder(configClass, attributeName); if (configMethod.isAnnotationPresent(ConfigDescription.class)) { builder.setDescription(configMethod.getAnnotation(ConfigDescription.class).value()); } // find the getter Method getter = findGetter(configClass, configMethod, attributeName); if (getter != null) { builder.setGetter(getter); if (configMethod.isAnnotationPresent(Deprecated.class) != getter.isAnnotationPresent(Deprecated.class)) { problems.addError("Methods [%s] and [%s] must be @Deprecated together", configMethod, getter); } } if (defunctConfig.contains(propertyName)) { problems.addError("@Config property '%s' on method [%s] is defunct on class [%s]", propertyName, configMethod, configClass); } // Add the injection point for the current setter/property builder.addInjectionPoint(InjectionPointMetaData.newCurrent(configClass, propertyName, configMethod)); // Add injection points for legacy setters/properties for (InjectionPointMetaData injectionPoint : findLegacySetters(configClass, propertyName, attributeName)) { if (!injectionPoint.getSetter().isAnnotationPresent(Config.class) && !injectionPoint.getSetter().isAnnotationPresent(Deprecated.class)) { problems.addWarning("Replaced @LegacyConfig method [%s] should be @Deprecated", injectionPoint.getSetter().toGenericString()); } builder.addInjectionPoint(injectionPoint); } return builder.build(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ConfigurationMetadata<?> that = (ConfigurationMetadata<?>) o; if (!configClass.equals(that.configClass)) { return false; } return true; } @Override public int hashCode() { return configClass.hashCode(); } @Override public String toString() { return toStringHelper(this) .add("configClass", configClass) .toString(); } public static class InjectionPointMetaData { private final Class<?> configClass; private final String property; private final Method setter; private final boolean current; public static InjectionPointMetaData newCurrent(Class<?> configClass, String property, Method setter) { return new InjectionPointMetaData(configClass, property, setter, true); } public static InjectionPointMetaData newLegacy(Class<?> configClass, String property, Method setter) { return new InjectionPointMetaData(configClass, property, setter, false); } private InjectionPointMetaData(Class<?> configClass, String property, Method setter, boolean current) { requireNonNull(configClass); requireNonNull(property); requireNonNull(setter); Preconditions.checkArgument(!property.isEmpty()); this.configClass = configClass; this.property = property; this.setter = setter; this.current = current; } public Class<?> getConfigClass() { return this.configClass; } public String getProperty() { return this.property; } public Method getSetter() { return this.setter; } public boolean isLegacy() { return !this.current; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } InjectionPointMetaData that = (InjectionPointMetaData) o; if (!configClass.equals(that.configClass)) { return false; } if (!property.equals(that.property)) { return false; } return true; } @Override public int hashCode() { int result = configClass.hashCode(); result = 31 * result + property.hashCode(); return result; } } public static class AttributeMetadata { private final Class<?> configClass; private final String name; private final String description; private final Method getter; private final InjectionPointMetaData injectionPoint; private final Set<InjectionPointMetaData> legacyInjectionPoints; public AttributeMetadata(Class<?> configClass, String name, String description, Method getter, InjectionPointMetaData injectionPoint, Set<InjectionPointMetaData> legacyInjectionPoints) { requireNonNull(configClass); requireNonNull(name); requireNonNull(getter); requireNonNull(injectionPoint); requireNonNull(legacyInjectionPoints); this.configClass = configClass; this.name = name; this.description = description; this.getter = getter; this.injectionPoint = injectionPoint; this.legacyInjectionPoints = ImmutableSet.copyOf(legacyInjectionPoints); } public Class<?> getConfigClass() { return configClass; } public String getName() { return name; } public String getDescription() { return description; } public Method getGetter() { return getter; } public InjectionPointMetaData getInjectionPoint() { return this.injectionPoint; } public Set<InjectionPointMetaData> getLegacyInjectionPoints() { return this.legacyInjectionPoints; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } AttributeMetadata that = (AttributeMetadata) o; if (!configClass.equals(that.configClass)) { return false; } if (!name.equals(that.name)) { return false; } return true; } @Override public int hashCode() { int result = configClass.hashCode(); result = 31 * result + name.hashCode(); return result; } @Override public String toString() { return toStringHelper(this) .add("name", name) .toString(); } } public static class AttributeMetaDataBuilder { private final Class<?> configClass; private final String name; private String description; private Method getter; private InjectionPointMetaData injectionPoint; private final Set<InjectionPointMetaData> legacyInjectionPoints = new HashSet<>(); public AttributeMetaDataBuilder(Class<?> configClass, String name) { requireNonNull(configClass); requireNonNull(name); Preconditions.checkArgument(!name.isEmpty()); this.configClass = configClass; this.name = name; } public void setDescription(String description) { requireNonNull(description); this.description = description; } public void setGetter(Method getter) { requireNonNull(getter); this.getter = getter; } public void addInjectionPoint(InjectionPointMetaData injectionPointMetaData) { requireNonNull(injectionPointMetaData); if (injectionPointMetaData.isLegacy()) { this.legacyInjectionPoints.add(injectionPointMetaData); return; } if (this.injectionPoint != null) { throw Problems.exceptionFor("Trying to set current property twice: '%s' on method [%s] and '%s' on method [%s]", this.injectionPoint.getProperty(), this.injectionPoint.getSetter().toGenericString(), injectionPointMetaData.getProperty(), injectionPointMetaData.getSetter().toGenericString()); } this.injectionPoint = injectionPointMetaData; } public AttributeMetadata build() { // todo fix validation if (getter == null) { return null; } return new AttributeMetadata(configClass, name, description, getter, injectionPoint, legacyInjectionPoints); } } private static Collection<Method> findConfigMethods(Class<?> configClass) { return findAnnotatedMethods(configClass, Config.class); } private static Collection<Method> findLegacyConfigMethods(Class<?> configClass) { return findAnnotatedMethods(configClass, LegacyConfig.class); } /** * Find methods that are tagged with a given annotation somewhere in the hierarchy * * @param configClass the class to analyze * @return a map that associates a concrete method to the actual method tagged * (which may belong to a different class in class hierarchy) */ private static Collection<Method> findAnnotatedMethods(Class<?> configClass, Class<? extends java.lang.annotation.Annotation> annotation) { List<Method> result = new ArrayList<>(); // gather all publicly available methods // this returns everything, even if it's declared in a parent for (Method method : configClass.getMethods()) { // skip methods that are used internally by the vm for implementing covariance, etc if (method.isSynthetic() || method.isBridge() || Modifier.isStatic(method.getModifiers())) { continue; } // look for annotations recursively in super-classes or interfaces Method managedMethod = findAnnotatedMethod(configClass, annotation, method.getName(), method.getParameterTypes()); if (managedMethod != null) { result.add(managedMethod); } } return result; } public static Method findAnnotatedMethod(Class<?> configClass, Class<? extends java.lang.annotation.Annotation> annotation, String methodName, Class<?>... paramTypes) { try { Method method = configClass.getDeclaredMethod(methodName, paramTypes); if (method != null && method.isAnnotationPresent(annotation)) { return method; } } catch (NoSuchMethodException e) { // ignore } if (configClass.getSuperclass() != null) { Method managedMethod = findAnnotatedMethod(configClass.getSuperclass(), annotation, methodName, paramTypes); if (managedMethod != null) { return managedMethod; } } for (Class<?> iface : configClass.getInterfaces()) { Method managedMethod = findAnnotatedMethod(iface, annotation, methodName, paramTypes); if (managedMethod != null) { return managedMethod; } } return null; } private Set<InjectionPointMetaData> findLegacySetters(Class<?> configClass, String propertyName, String attributeName) { Set<InjectionPointMetaData> setters = new HashSet<>(); String setterName = "set" + attributeName; for (Class<?> clazz = configClass; (clazz != null) && !clazz.equals(Object.class); clazz = clazz.getSuperclass()) { for (Method method : clazz.getDeclaredMethods()) { if (isUsableMethod(method)) { if (method.getName().equals(setterName) && method.isAnnotationPresent(LegacyConfig.class)) { // Found @LegacyConfig setter with matching attribute name if (validateSetter(method)) { for (String property : method.getAnnotation(LegacyConfig.class).value()) { if (defunctConfig.contains(property)) { problems.addError("@LegacyConfig property '%s' on method [%s] is defunct on class [%s]", property, method, configClass); } if (!property.equals(propertyName)) { setters.add(InjectionPointMetaData.newLegacy(configClass, property, method)); } else { problems.addError("@LegacyConfig property '%s' on method [%s] is replaced by @Config property of same name on method [%s]", property, method.toGenericString(), setterName); } } } } else if (method.isAnnotationPresent(LegacyConfig.class) && method.getAnnotation(LegacyConfig.class).replacedBy().equals(propertyName)) { // Found @LegacyConfig setter linked by replacedBy() property if (validateSetter(method)) { for (String property : method.getAnnotation(LegacyConfig.class).value()) { if (defunctConfig.contains(property)) { problems.addError("@LegacyConfig property '%s' on method [%s] is defunct on class [%s]", property, method, configClass); } if (!property.equals(propertyName)) { setters.add(InjectionPointMetaData.newLegacy(configClass, property, method)); } else { problems.addError("@LegacyConfig property '%s' on method [%s] is replaced by @Config property of same name on method [%s]", property, method.toGenericString(), setterName); } } } } } } } return setters; } private Method findGetter(Class<?> configClass, Method configMethod, String attributeName) { // find the getter or is function String getterName = "get" + attributeName; String isName = "is" + attributeName; List<Method> getters = new ArrayList<>(); List<Method> unusableGetters = new ArrayList<>(); for (Class<?> clazz = configClass; (clazz != null) && !clazz.equals(Object.class); clazz = clazz.getSuperclass()) { for (Method method : clazz.getDeclaredMethods()) { if (method.getName().equals(getterName) || method.getName().equals(isName)) { if (isUsableMethod(method) && !method.getReturnType().equals(Void.TYPE) && method.getParameterTypes().length == 0) { getters.add(method); } else { unusableGetters.add(method); } } } } // too small if (getters.isEmpty()) { String unusable = ""; if (!unusableGetters.isEmpty()) { StringBuilder builder = new StringBuilder(" The following methods are unusable: "); for (Method method : unusableGetters) { builder.append('[').append(method.toGenericString()).append(']'); } unusable = builder.toString(); } problems.addError("No getter for @Config method [%s].%s", configMethod.toGenericString(), unusable); return null; } // too big if (getters.size() > 1) { // To many getters problems.addError("Multiple getters found for @Config setter [%s]", configMethod.toGenericString()); return null; } // just right return getters.get(0); } private static boolean isUsableMethod(Method method) { return !method.isSynthetic() && !method.isBridge() && !Modifier.isStatic(method.getModifiers()) && Modifier.isPublic(method.getModifiers()); } }