/* * Copyright 2008-2017 the original author or authors. * * 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 org.codehaus.griffon.runtime.core; import griffon.core.GriffonApplication; import griffon.core.injection.Module; import griffon.core.injection.TestingModule; import griffon.core.test.TestCaseAware; import griffon.core.test.TestModuleAware; import griffon.core.test.TestModuleOverrides; import griffon.core.test.TestModules; import griffon.inject.BindTo; import griffon.util.AnnotationUtils; import org.codehaus.griffon.runtime.core.injection.AbstractTestingModule; import org.codehaus.griffon.runtime.core.injection.AnnotatedBindingBuilder; import org.codehaus.griffon.runtime.core.injection.LinkedBindingBuilder; import org.codehaus.griffon.runtime.core.injection.SingletonBindingBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.inject.Named; import javax.inject.Provider; import javax.inject.Qualifier; import javax.inject.Singleton; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static griffon.util.AnnotationUtils.named; import static griffon.util.GriffonNameUtils.getPropertyName; import static griffon.util.GriffonNameUtils.isBlank; /** * @author Andres Almiray * @since 2.0.0 */ public class TestApplicationBootstrapper extends DefaultApplicationBootstrapper implements TestCaseAware { private static final Logger LOG = LoggerFactory.getLogger(TestApplicationBootstrapper.class); private static final String METHOD_MODULES = "modules"; private static final String METHOD_MODULE_OVERRIDES = "moduleOverrides"; private Object testCase; public TestApplicationBootstrapper(@Nonnull GriffonApplication application) { super(application); } public void setTestCase(@Nonnull Object testCase) { this.testCase = testCase; } @Nonnull @Override protected List<Module> loadModules() { List<Module> modules = doCollectModulesFromMethod(); if (!modules.isEmpty()) { return modules; } modules = super.loadModules(); doCollectOverridingModules(modules); doCollectModulesFromInnerClasses(modules); doCollectModulesFromFields(modules); return modules; } @Nonnull @Override protected Map<String, Module> sortModules(@Nonnull List<Module> moduleInstances) { Map<String, Module> sortedModules = super.sortModules(moduleInstances); // move all `TestingModules` at the end // turns out the map is of type LinkedHashMap so insertion order is retained Map<String, Module> testingModules = new LinkedHashMap<>(); for (Map.Entry<String, Module> e : sortedModules.entrySet()) { if (e.getValue() instanceof TestingModule) { testingModules.put(e.getKey(), e.getValue()); } } for (String key : testingModules.keySet()) { sortedModules.remove(key); } sortedModules.putAll(testingModules); LOG.debug("computed {} order is {}", "Module", sortedModules.keySet()); return sortedModules; } @SuppressWarnings("unchecked") private List<Module> doCollectModulesFromMethod() { if (testCase == null) { return Collections.emptyList(); } if (testCase instanceof TestModuleAware) { return ((TestModuleAware) testCase).modules(); } else { Class<?> clazz = testCase.getClass(); List<Class<?>> classes = new ArrayList<>(); while (clazz != null) { classes.add(clazz); clazz = clazz.getSuperclass(); } Collections.reverse(classes); for (Class<?> c : classes) { List<Module> ms = harvestModulesFromMethod(c, METHOD_MODULES, TestModules.class); if (!ms.isEmpty()) { return ms; } } } return Collections.emptyList(); } @SuppressWarnings("unchecked") private void doCollectOverridingModules(@Nonnull final Collection<Module> modules) { if (testCase == null) { return; } if (testCase instanceof TestModuleAware) { List<Module> overrides = ((TestModuleAware) testCase).moduleOverrides(); modules.addAll(overrides); } else { Class<?> clazz = testCase.getClass(); List<Class<?>> classes = new ArrayList<>(); while (clazz != null) { classes.add(clazz); clazz = clazz.getSuperclass(); } Collections.reverse(classes); for (Class<?> c : classes) { List<Module> overrides = harvestModulesFromMethod(c, METHOD_MODULE_OVERRIDES, TestModuleOverrides.class); if (!overrides.isEmpty()) { modules.addAll(overrides); return; } } } } @Nonnull private List<Module> harvestModulesFromMethod(@Nonnull Class<?> clazz, @Nonnull String methodName, @Nonnull Class<? extends Annotation> annotationClass) { Method annotatedMethod = null; // check annotation first for (Method m : clazz.getDeclaredMethods()) { if (m.getAnnotation(annotationClass) != null) { annotatedMethod = m; break; } } // check using naming convention Method namedMethod = null; try { namedMethod = clazz.getDeclaredMethod(methodName); } catch (NoSuchMethodException e) { if (annotatedMethod == null) { return Collections.emptyList(); } } if (namedMethod != null && annotatedMethod != namedMethod) { System.err.println("Usage of method named '" + clazz.getName() + "." + methodName + "()' is discouraged. Rename the method and annotate it with @" + annotationClass.getSimpleName()); } Method method = annotatedMethod != null ? annotatedMethod : namedMethod; try { method.setAccessible(true); return (List<Module>) method.invoke(testCase); } catch (Exception e) { throw new IllegalArgumentException("An error occurred while initializing modules from " + clazz.getName() + "." + method.getName(), e); } } private void doCollectModulesFromInnerClasses(@Nonnull final Collection<Module> modules) { if (testCase != null) { modules.add(new InnerClassesModule()); modules.addAll(harvestInnerModules()); } } @Nonnull private Collection<? extends Module> harvestInnerModules() { Class<?> clazz = testCase.getClass(); List<Class<?>> classes = new ArrayList<>(); while (clazz != null) { classes.add(clazz); clazz = clazz.getSuperclass(); } Collections.reverse(classes); List<Module> modules = new ArrayList<>(); for (Class<?> c : classes) { modules.addAll(doHarvestInnerModules(c)); } return modules; } @Nonnull private Collection<? extends Module> doHarvestInnerModules(@Nonnull Class<?> rootClass) { List<Module> modules = new ArrayList<>(); for (Class<?> clazz : rootClass.getDeclaredClasses()) { if (!Module.class.isAssignableFrom(clazz) || !Modifier.isPublic(clazz.getModifiers())) { continue; } try { modules.add((Module) clazz.newInstance()); } catch (InstantiationException | IllegalAccessException e) { LOG.error("Can't instantiate module " + clazz.getName() + " . Make sure it's marked as public and provides a no-args constructor."); } } return modules; } private void doCollectModulesFromFields(@Nonnull final Collection<Module> modules) { if (testCase != null) { modules.add(new FieldsModule()); } } @Nonnull protected List<Annotation> harvestQualifiers(@Nonnull Class<?> clazz) { List<Annotation> list = new ArrayList<>(); Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) { if (BindTo.class.isAssignableFrom(annotation.getClass())) { continue; } // special case for @Named if (Named.class.isAssignableFrom(annotation.getClass())) { Named named = (Named) annotation; if (isBlank(named.value())) { list.add(named(getPropertyName(clazz))); continue; } } list.add(annotation); } } return list; } @Nonnull protected List<Annotation> harvestQualifiers(@Nonnull Field field) { List<Annotation> list = new ArrayList<>(); Annotation[] annotations = field.getAnnotations(); for (Annotation annotation : annotations) { if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) { if (BindTo.class.isAssignableFrom(annotation.getClass())) { continue; } // special case for @Named if (Named.class.isAssignableFrom(annotation.getClass())) { Named named = (Named) annotation; if (isBlank(named.value())) { list.add(named(getPropertyName(field.getName()))); continue; } } list.add(annotation); } } return list; } private class InnerClassesModule extends AbstractTestingModule { @Override @SuppressWarnings("unchecked") protected void doConfigure() { Class<?> clazz = testCase.getClass(); List<Class<?>> classes = new ArrayList<>(); while (clazz != null) { classes.add(clazz); clazz = clazz.getSuperclass(); } Collections.reverse(classes); for (Class<?> c : classes) { harvestBindings(c); } } protected void harvestBindings(@Nonnull Class<?> rootClass) { for (Class<?> clazz : rootClass.getDeclaredClasses()) { BindTo bindTo = clazz.getAnnotation(BindTo.class); if (bindTo == null) { continue; } List<Annotation> qualifiers = harvestQualifiers(clazz); Annotation classifier = qualifiers.isEmpty() ? null : qualifiers.get(0); boolean isSingleton = clazz.getAnnotation(Singleton.class) != null; AnnotatedBindingBuilder<?> abuilder = bind(bindTo.value()); if (classifier != null) { LinkedBindingBuilder<?> lbuilder = abuilder.withClassifier(classifier); if (Provider.class.isAssignableFrom(clazz)) { SingletonBindingBuilder<?> sbuilder = lbuilder.toProvider((Class) clazz); if (isSingleton) { sbuilder.asSingleton(); } } else { SingletonBindingBuilder<?> sbuilder = lbuilder.to((Class) clazz); if (isSingleton) { sbuilder.asSingleton(); } } } else { if (Provider.class.isAssignableFrom(clazz)) { SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Class) clazz); if (isSingleton) { sbuilder.asSingleton(); } } else { SingletonBindingBuilder<?> sbuilder = abuilder.to((Class) clazz); if (isSingleton) { sbuilder.asSingleton(); } } } } } } private class FieldsModule extends AbstractTestingModule { @Override @SuppressWarnings("unchecked") protected void doConfigure() { Class<?> clazz = testCase.getClass(); List<Class<?>> classes = new ArrayList<>(); while (clazz != null) { classes.add(clazz); clazz = clazz.getSuperclass(); } Collections.reverse(classes); for (Class<?> c : classes) { harvestBindings(c); } } protected void harvestBindings(@Nonnull Class<?> rootClass) { for (Field field : rootClass.getDeclaredFields()) { BindTo bindTo = field.getAnnotation(BindTo.class); if (bindTo == null) { continue; } List<Annotation> qualifiers = harvestQualifiers(field); Annotation classifier = qualifiers.isEmpty() ? null : qualifiers.get(0); boolean isSingleton = field.getAnnotation(Singleton.class) != null; field.setAccessible(true); Object instance = null; try { instance = field.get(testCase); } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } if (instance != null) { AnnotatedBindingBuilder<Object> abuilder = (AnnotatedBindingBuilder<Object>) bind(bindTo.value()); if (classifier != null) { if (Provider.class.isAssignableFrom(instance.getClass())) { SingletonBindingBuilder<?> sbuilder = abuilder .withClassifier(classifier) .toProvider((Provider<Object>) instance); if (isSingleton) { sbuilder.asSingleton(); } } else { abuilder.withClassifier(classifier).toInstance(instance); } } else if (Provider.class.isAssignableFrom(instance.getClass())) { SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Provider<Object>) instance); if (isSingleton) { sbuilder.asSingleton(); } } else { abuilder.toInstance(instance); } } else { AnnotatedBindingBuilder<?> abuilder = bind(bindTo.value()); if (classifier != null) { LinkedBindingBuilder<?> lbuilder = abuilder.withClassifier(classifier); if (Provider.class.isAssignableFrom(field.getType())) { SingletonBindingBuilder<?> sbuilder = lbuilder.toProvider((Class) field.getType()); if (isSingleton) { sbuilder.asSingleton(); } } else { SingletonBindingBuilder<?> sbuilder = lbuilder.to((Class) field.getType()); if (isSingleton) { sbuilder.asSingleton(); } } } else { if (Provider.class.isAssignableFrom(field.getType())) { SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Class) field.getType()); if (isSingleton) { sbuilder.asSingleton(); } } else { SingletonBindingBuilder<?> sbuilder = abuilder.to((Class) field.getType()); if (isSingleton) { sbuilder.asSingleton(); } } } } } } } }