package com.github.racc.tscg; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import com.github.racc.tscg.reflectors.fastclasspathscanner.FastClasspathScanningReflector; import com.github.racc.tscg.reflectors.reflections.ReflectionsReflector; import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner; import org.reflections.Reflections; import org.reflections.scanners.FieldAnnotationsScanner; import org.reflections.scanners.MethodAnnotationsScanner; import org.reflections.scanners.MethodParameterScanner; import org.reflections.scanners.TypeAnnotationsScanner; import org.reflections.util.ClasspathHelper; import org.reflections.util.ConfigurationBuilder; import org.reflections.util.FilterBuilder; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Key; import com.google.inject.Module; import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValueType; /** * Include this {@link Module} in your {@link Guice} bootstrapping to Automagically * get bindings to your {@link TypesafeConfig} annotated configuration parameters. * * @author jason */ public class TypesafeConfigModule extends AbstractModule { private final Config config; private final Reflector reflections; private final Set<TypesafeConfig> boundAnnotations; private TypesafeConfigModule(Config config, Reflector reflections) { this.config = config; this.reflections = reflections; this.boundAnnotations = new HashSet<TypesafeConfig>(); } /** * Essentially calls fromConfigWithPackage with a package name of "" to construct a TypesafeConfigModule. * * @param config the config to use. * @return The constructed TypesafeConfigModule. * @deprecated this is deprecated as there is a known issue with scanning the classpath for annotations * in this mode, when running a packaged jar file. * use {@link #fromConfigWithPackage(Config, String)} instead and limit the packages to scan. */ @Deprecated public static TypesafeConfigModule fromConfig(Config config) { return fromConfigWithPackage(config, ""); } /** * Scans the specified packages for annotated classes, and applies Config values to them. * * @param config the Config to derive values from * @param packageNamePrefix the prefix to limit scanning to - e.g. "com.github" * @return The constructed TypesafeConfigModule. */ public static TypesafeConfigModule fromConfigWithPackage(Config config, String packageNamePrefix) { Reflections reflections = createPackageScanningReflections(packageNamePrefix); return fromConfigWithReflections(config, reflections); } /** * Scans the specified packages for annotated classes, and applies Config values to them. * * @param config the Config to derive values from * @param reflections the reflections object to use * @return The constructed TypesafeConfigModule. */ public static TypesafeConfigModule fromConfigWithReflections(Config config, Reflections reflections) { return new TypesafeConfigModule(config, new ReflectionsReflector(reflections)); } /** * Scans the specified packages for annotated classes, and applies Config values to them. * * @param config the Config to derive values from * @return The constructed TypesafeConfigModule. */ public static TypesafeConfigModule fromConfigUsingClasspathScanner(Config config, String ...scannerSpec) { return new TypesafeConfigModule(config, new FastClasspathScanningReflector(scannerSpec)); } public static Reflections createPackageScanningReflections(String packageNamePrefix){ ConfigurationBuilder configBuilder = new ConfigurationBuilder() .filterInputsBy(new FilterBuilder().includePackage(packageNamePrefix)) .setUrls(ClasspathHelper.forPackage(packageNamePrefix)) .setScanners( new TypeAnnotationsScanner(), new MethodParameterScanner(), new MethodAnnotationsScanner(), new FieldAnnotationsScanner() ); return new Reflections(configBuilder); } @SuppressWarnings({ "rawtypes"}) @Override protected void configure() { Set<Constructor> annotatedConstructors = reflections.getConstructorsWithAnyParamAnnotated(TypesafeConfig.class); for (Constructor c : annotatedConstructors) { Parameter[] params = c.getParameters(); bindParameters(params); } Set<Method> annotatedMethods = reflections.getMethodsWithAnyParamAnnotated(TypesafeConfig.class); for (Method m : annotatedMethods) { Parameter[] params = m.getParameters(); bindParameters(params); } Set<Field> annotatedFields = reflections.getFieldsAnnotatedWith(TypesafeConfig.class); for (Field f : annotatedFields) { TypesafeConfig annotation = f.getAnnotation(TypesafeConfig.class); bindValue(f.getType(), f.getAnnotatedType().getType(), annotation); } } private void bindParameters(Parameter[] params) { for (Parameter p : params) { if (p.isAnnotationPresent(TypesafeConfig.class)) { TypesafeConfig annotation = p.getAnnotation(TypesafeConfig.class); bindValue(p.getType(), p.getAnnotatedType().getType(), annotation); } } } private void bindValue(Class<?> paramClass, Type paramType, TypesafeConfig annotation) { // Prevents multiple bindings on the same annotation if (!boundAnnotations.contains(annotation)) { @SuppressWarnings("unchecked") Key<Object> key = (Key<Object>) Key.get(paramType, annotation); String configPath = annotation.value(); Object configValue = getConfigValue(paramClass, paramType, configPath); bind(key).toInstance(configValue); boundAnnotations.add(annotation); } } @SuppressWarnings({ "unchecked", "rawtypes" }) private Object getConfigValue(Class<?> paramClass, Type paramType, String path) { Optional<Object> extractedValue = ConfigExtractors.extractConfigValue(config, paramClass, path); if (extractedValue.isPresent()) { return extractedValue.get(); } ConfigValue configValue = config.getValue(path); ConfigValueType valueType = configValue.valueType(); if (valueType.equals(ConfigValueType.OBJECT) && Map.class.isAssignableFrom(paramClass)) { ConfigObject object = config.getObject(path); return object.unwrapped(); } else if (valueType.equals(ConfigValueType.OBJECT)) { Object bean = ConfigBeanFactory.create(config.getConfig(path), paramClass); return bean; } else if (valueType.equals(ConfigValueType.LIST) && List.class.isAssignableFrom(paramClass)) { Type listType = ((ParameterizedType) paramType).getActualTypeArguments()[0]; Optional<List<?>> extractedListValue = ListExtractors.extractConfigListValue(config, listType, path); if (extractedListValue.isPresent()) { return extractedListValue.get(); } else { List<? extends Config> configList = config.getConfigList(path); return configList.stream() .map(cfg -> { Object created = ConfigBeanFactory.create(cfg, (Class) listType); return created; }) .collect(Collectors.toList()); } } throw new RuntimeException("Cannot obtain config value for " + paramType + " at path: " + path); } }