TestApplicationBootstrapper.java
001 /*
002  * Copyright 2008-2017 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package org.codehaus.griffon.runtime.core;
017 
018 import griffon.core.GriffonApplication;
019 import griffon.core.injection.Module;
020 import griffon.core.injection.TestingModule;
021 import griffon.core.test.TestCaseAware;
022 import griffon.core.test.TestModuleAware;
023 import griffon.core.test.TestModuleOverrides;
024 import griffon.core.test.TestModules;
025 import griffon.inject.BindTo;
026 import griffon.util.AnnotationUtils;
027 import org.codehaus.griffon.runtime.core.injection.AbstractTestingModule;
028 import org.codehaus.griffon.runtime.core.injection.AnnotatedBindingBuilder;
029 import org.codehaus.griffon.runtime.core.injection.LinkedBindingBuilder;
030 import org.codehaus.griffon.runtime.core.injection.SingletonBindingBuilder;
031 import org.slf4j.Logger;
032 import org.slf4j.LoggerFactory;
033 
034 import javax.annotation.Nonnull;
035 import javax.inject.Named;
036 import javax.inject.Provider;
037 import javax.inject.Qualifier;
038 import javax.inject.Singleton;
039 import java.lang.annotation.Annotation;
040 import java.lang.reflect.Field;
041 import java.lang.reflect.Method;
042 import java.lang.reflect.Modifier;
043 import java.util.ArrayList;
044 import java.util.Collection;
045 import java.util.Collections;
046 import java.util.LinkedHashMap;
047 import java.util.List;
048 import java.util.Map;
049 
050 import static griffon.util.AnnotationUtils.named;
051 import static griffon.util.GriffonNameUtils.getPropertyName;
052 import static griffon.util.GriffonNameUtils.isBlank;
053 
054 /**
055  @author Andres Almiray
056  @since 2.0.0
057  */
058 public class TestApplicationBootstrapper extends DefaultApplicationBootstrapper implements TestCaseAware {
059     private static final Logger LOG = LoggerFactory.getLogger(TestApplicationBootstrapper.class);
060 
061     private static final String METHOD_MODULES = "modules";
062     private static final String METHOD_MODULE_OVERRIDES = "moduleOverrides";
063     private Object testCase;
064 
065     public TestApplicationBootstrapper(@Nonnull GriffonApplication application) {
066         super(application);
067     }
068 
069     public void setTestCase(@Nonnull Object testCase) {
070         this.testCase = testCase;
071     }
072 
073     @Nonnull
074     @Override
075     protected List<Module> loadModules() {
076         List<Module> modules = doCollectModulesFromMethod();
077         if (!modules.isEmpty()) {
078             return modules;
079         }
080         modules = super.loadModules();
081         doCollectOverridingModules(modules);
082         doCollectModulesFromInnerClasses(modules);
083         doCollectModulesFromFields(modules);
084         return modules;
085     }
086 
087     @Nonnull
088     @Override
089     protected Map<String, Module> sortModules(@Nonnull List<Module> moduleInstances) {
090         Map<String, Module> sortedModules = super.sortModules(moduleInstances);
091         // move all `TestingModules` at the end
092         // turns out the map is of type LinkedHashMap so insertion order is retained
093         Map<String, Module> testingModules = new LinkedHashMap<>();
094         for (Map.Entry<String, Module> e : sortedModules.entrySet()) {
095             if (e.getValue() instanceof TestingModule) {
096                 testingModules.put(e.getKey(), e.getValue());
097             }
098         }
099         for (String key : testingModules.keySet()) {
100             sortedModules.remove(key);
101         }
102         sortedModules.putAll(testingModules);
103 
104         LOG.debug("computed {} order is {}""Module", sortedModules.keySet());
105 
106         return sortedModules;
107     }
108 
109     @SuppressWarnings("unchecked")
110     private List<Module> doCollectModulesFromMethod() {
111         if (testCase == null) {
112             return Collections.emptyList();
113         }
114 
115         if (testCase instanceof TestModuleAware) {
116             return ((TestModuleAwaretestCase).modules();
117         else {
118             Class<?> clazz = testCase.getClass();
119             List<Class<?>> classes = new ArrayList<>();
120             while (clazz != null) {
121                 classes.add(clazz);
122                 clazz = clazz.getSuperclass();
123             }
124 
125             Collections.reverse(classes);
126             for (Class<?> c : classes) {
127                 List<Module> ms = harvestModulesFromMethod(c, METHOD_MODULES, TestModules.class);
128                 if (!ms.isEmpty()) { return ms; }
129             }
130         }
131 
132         return Collections.emptyList();
133     }
134 
135     @SuppressWarnings("unchecked")
136     private void doCollectOverridingModules(@Nonnull final Collection<Module> modules) {
137         if (testCase == null) {
138             return;
139         }
140 
141         if (testCase instanceof TestModuleAware) {
142             List<Module> overrides = ((TestModuleAwaretestCase).moduleOverrides();
143             modules.addAll(overrides);
144         else {
145             Class<?> clazz = testCase.getClass();
146             List<Class<?>> classes = new ArrayList<>();
147             while (clazz != null) {
148                 classes.add(clazz);
149                 clazz = clazz.getSuperclass();
150             }
151 
152             Collections.reverse(classes);
153             for (Class<?> c : classes) {
154                 List<Module> overrides = harvestModulesFromMethod(c, METHOD_MODULE_OVERRIDES, TestModuleOverrides.class);
155                 if (!overrides.isEmpty()) {
156                     modules.addAll(overrides);
157                     return;
158                 }
159             }
160         }
161     }
162 
163     @Nonnull
164     @SuppressWarnings("unchecked")
165     private List<Module> harvestModulesFromMethod(@Nonnull Class<?> clazz, @Nonnull String methodName, @Nonnull Class<? extends Annotation> annotationClass) {
166         Method annotatedMethod = null;
167 
168         // check annotation first
169         for (Method m : clazz.getDeclaredMethods()) {
170             if (m.getAnnotation(annotationClass!= null) {
171                 annotatedMethod = m;
172                 break;
173             }
174         }
175 
176         // check using naming convention
177         Method namedMethod = null;
178         try {
179             namedMethod = clazz.getDeclaredMethod(methodName);
180         catch (NoSuchMethodException e) {
181             if (annotatedMethod == null) {
182                 return Collections.emptyList();
183             }
184         }
185 
186         if (namedMethod != null && annotatedMethod != namedMethod) {
187             System.err.println("Usage of method named '" + clazz.getName() "." + methodName + "()' is discouraged. Rename the method and annotate it with @" + annotationClass.getSimpleName());
188         }
189 
190         Method method = annotatedMethod != null ? annotatedMethod : namedMethod;
191         try {
192             method.setAccessible(true);
193             return (List<Module>method.invoke(testCase);
194         catch (Exception e) {
195             throw new IllegalArgumentException("An error occurred while initializing modules from " + clazz.getName() "." + method.getName(), e);
196         }
197     }
198 
199     private void doCollectModulesFromInnerClasses(@Nonnull final Collection<Module> modules) {
200         if (testCase != null) {
201             modules.add(new InnerClassesModule());
202             modules.addAll(harvestInnerModules());
203         }
204     }
205 
206     @Nonnull
207     private Collection<? extends Module> harvestInnerModules() {
208         Class<?> clazz = testCase.getClass();
209         List<Class<?>> classes = new ArrayList<>();
210         while (clazz != null) {
211             classes.add(clazz);
212             clazz = clazz.getSuperclass();
213         }
214 
215         Collections.reverse(classes);
216 
217         List<Module> modules = new ArrayList<>();
218         for (Class<?> c : classes) {
219             modules.addAll(doHarvestInnerModules(c));
220         }
221 
222         return modules;
223     }
224 
225     @Nonnull
226     private Collection<? extends Module> doHarvestInnerModules(@Nonnull Class<?> rootClass) {
227         List<Module> modules = new ArrayList<>();
228         for (Class<?> clazz : rootClass.getDeclaredClasses()) {
229             if (!Module.class.isAssignableFrom(clazz|| !Modifier.isPublic(clazz.getModifiers())) {
230                 continue;
231             }
232 
233             try {
234                 modules.add((Moduleclazz.newInstance());
235             catch (InstantiationException | IllegalAccessException e) {
236                 LOG.error("Can't instantiate module " + clazz.getName() " . Make sure it's marked as public and provides a no-args constructor.");
237             }
238         }
239         return modules;
240     }
241 
242     private void doCollectModulesFromFields(@Nonnull final Collection<Module> modules) {
243         if (testCase != null) {
244             modules.add(new FieldsModule());
245         }
246     }
247 
248     @Nonnull
249     protected List<Annotation> harvestQualifiers(@Nonnull Class<?> clazz) {
250         List<Annotation> list = new ArrayList<>();
251         Annotation[] annotations = clazz.getAnnotations();
252         for (Annotation annotation : annotations) {
253             if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) {
254                 if (BindTo.class.isAssignableFrom(annotation.getClass())) {
255                     continue;
256                 }
257 
258                 // special case for @Named
259                 if (Named.class.isAssignableFrom(annotation.getClass())) {
260                     Named named = (Namedannotation;
261                     if (isBlank(named.value())) {
262                         list.add(named(getPropertyName(clazz)));
263                         continue;
264                     }
265                 }
266                 list.add(annotation);
267             }
268         }
269         return list;
270     }
271 
272     @Nonnull
273     protected List<Annotation> harvestQualifiers(@Nonnull Field field) {
274         List<Annotation> list = new ArrayList<>();
275         Annotation[] annotations = field.getAnnotations();
276         for (Annotation annotation : annotations) {
277             if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) {
278                 if (BindTo.class.isAssignableFrom(annotation.getClass())) {
279                     continue;
280                 }
281 
282                 // special case for @Named
283                 if (Named.class.isAssignableFrom(annotation.getClass())) {
284                     Named named = (Namedannotation;
285                     if (isBlank(named.value())) {
286                         list.add(named(getPropertyName(field.getName())));
287                         continue;
288                     }
289                 }
290                 list.add(annotation);
291             }
292         }
293         return list;
294     }
295 
296     private class InnerClassesModule extends AbstractTestingModule {
297         @Override
298         @SuppressWarnings("unchecked")
299         protected void doConfigure() {
300             Class<?> clazz = testCase.getClass();
301             List<Class<?>> classes = new ArrayList<>();
302             while (clazz != null) {
303                 classes.add(clazz);
304                 clazz = clazz.getSuperclass();
305             }
306 
307             Collections.reverse(classes);
308             for (Class<?> c : classes) {
309                 harvestBindings(c);
310             }
311         }
312 
313         @SuppressWarnings("unchecked")
314         protected void harvestBindings(@Nonnull Class<?> rootClass) {
315             for (Class<?> clazz : rootClass.getDeclaredClasses()) {
316                 BindTo bindTo = clazz.getAnnotation(BindTo.class);
317                 if (bindTo == null) { continue}
318                 List<Annotation> qualifiers = harvestQualifiers(clazz);
319                 Annotation classifier = qualifiers.isEmpty() null : qualifiers.get(0);
320                 boolean isSingleton = clazz.getAnnotation(Singleton.class!= null;
321 
322                 AnnotatedBindingBuilder<?> abuilder = bind(bindTo.value());
323                 if (classifier != null) {
324                     LinkedBindingBuilder<?> lbuilder = abuilder.withClassifier(classifier);
325                     if (Provider.class.isAssignableFrom(clazz)) {
326                         SingletonBindingBuilder<?> sbuilder = lbuilder.toProvider((Classclazz);
327                         if (isSingleton) { sbuilder.asSingleton()}
328                     else {
329                         SingletonBindingBuilder<?> sbuilder = lbuilder.to((Classclazz);
330                         if (isSingleton) { sbuilder.asSingleton()}
331                     }
332                 else {
333                     if (Provider.class.isAssignableFrom(clazz)) {
334                         SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Classclazz);
335                         if (isSingleton) { sbuilder.asSingleton()}
336                     else {
337                         SingletonBindingBuilder<?> sbuilder = abuilder.to((Classclazz);
338                         if (isSingleton) { sbuilder.asSingleton()}
339                     }
340                 }
341             }
342         }
343     }
344 
345     private class FieldsModule extends AbstractTestingModule {
346         @Override
347         @SuppressWarnings("unchecked")
348         protected void doConfigure() {
349             Class<?> clazz = testCase.getClass();
350             List<Class<?>> classes = new ArrayList<>();
351             while (clazz != null) {
352                 classes.add(clazz);
353                 clazz = clazz.getSuperclass();
354             }
355 
356             Collections.reverse(classes);
357             for (Class<?> c : classes) {
358                 harvestBindings(c);
359             }
360         }
361 
362         @SuppressWarnings("unchecked")
363         protected void harvestBindings(@Nonnull Class<?> rootClass) {
364             for (Field field : rootClass.getDeclaredFields()) {
365                 BindTo bindTo = field.getAnnotation(BindTo.class);
366                 if (bindTo == null) { continue}
367                 List<Annotation> qualifiers = harvestQualifiers(field);
368                 Annotation classifier = qualifiers.isEmpty() null : qualifiers.get(0);
369                 boolean isSingleton = field.getAnnotation(Singleton.class!= null;
370 
371                 field.setAccessible(true);
372                 Object instance = null;
373                 try {
374                     instance = field.get(testCase);
375                 catch (IllegalAccessException e) {
376                     throw new IllegalArgumentException(e);
377                 }
378 
379                 if (instance != null) {
380                     AnnotatedBindingBuilder<Object> abuilder = (AnnotatedBindingBuilder<Object>bind(bindTo.value());
381                     if (classifier != null) {
382                         if (Provider.class.isAssignableFrom(instance.getClass())) {
383                             SingletonBindingBuilder<?> sbuilder = abuilder
384                                 .withClassifier(classifier)
385                                 .toProvider((Provider<Object>instance);
386                             if (isSingleton) { sbuilder.asSingleton()}
387                         else {
388                             abuilder.withClassifier(classifier).toInstance(instance);
389                         }
390                     else if (Provider.class.isAssignableFrom(instance.getClass())) {
391                         SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Provider<Object>instance);
392                         if (isSingleton) { sbuilder.asSingleton()}
393                     else {
394                         abuilder.toInstance(instance);
395                     }
396                 else {
397                     AnnotatedBindingBuilder<?> abuilder = bind(bindTo.value());
398                     if (classifier != null) {
399                         LinkedBindingBuilder<?> lbuilder = abuilder.withClassifier(classifier);
400                         if (Provider.class.isAssignableFrom(field.getType())) {
401                             SingletonBindingBuilder<?> sbuilder = lbuilder.toProvider((Classfield.getType());
402                             if (isSingleton) { sbuilder.asSingleton()}
403                         else {
404                             SingletonBindingBuilder<?> sbuilder = lbuilder.to((Classfield.getType());
405                             if (isSingleton) { sbuilder.asSingleton()}
406                         }
407                     else {
408                         if (Provider.class.isAssignableFrom(field.getType())) {
409                             SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Classfield.getType());
410                             if (isSingleton) { sbuilder.asSingleton()}
411                         else {
412                             SingletonBindingBuilder<?> sbuilder = abuilder.to((Classfield.getType());
413                             if (isSingleton) { sbuilder.asSingleton()}
414                         }
415                     }
416                 }
417             }
418         }
419     }
420 }