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     private List<Module> harvestModulesFromMethod(@Nonnull Class<?> clazz, @Nonnull String methodName, @Nonnull Class<? extends Annotation> annotationClass) {
165         Method annotatedMethod = null;
166 
167         // check annotation first
168         for (Method m : clazz.getDeclaredMethods()) {
169             if (m.getAnnotation(annotationClass!= null) {
170                 annotatedMethod = m;
171                 break;
172             }
173         }
174 
175         // check using naming convention
176         Method namedMethod = null;
177         try {
178             namedMethod = clazz.getDeclaredMethod(methodName);
179         catch (NoSuchMethodException e) {
180             if (annotatedMethod == null) {
181                 return Collections.emptyList();
182             }
183         }
184 
185         if (namedMethod != null && annotatedMethod != namedMethod) {
186             System.err.println("Usage of method named '" + clazz.getName() "." + methodName + "()' is discouraged. Rename the method and annotate it with @" + annotationClass.getSimpleName());
187         }
188 
189         Method method = annotatedMethod != null ? annotatedMethod : namedMethod;
190         try {
191             method.setAccessible(true);
192             return (List<Module>method.invoke(testCase);
193         catch (Exception e) {
194             throw new IllegalArgumentException("An error occurred while initializing modules from " + clazz.getName() "." + method.getName(), e);
195         }
196     }
197 
198     private void doCollectModulesFromInnerClasses(@Nonnull final Collection<Module> modules) {
199         if (testCase != null) {
200             modules.add(new InnerClassesModule());
201             modules.addAll(harvestInnerModules());
202         }
203     }
204 
205     @Nonnull
206     private Collection<? extends Module> harvestInnerModules() {
207         Class<?> clazz = testCase.getClass();
208         List<Class<?>> classes = new ArrayList<>();
209         while (clazz != null) {
210             classes.add(clazz);
211             clazz = clazz.getSuperclass();
212         }
213 
214         Collections.reverse(classes);
215 
216         List<Module> modules = new ArrayList<>();
217         for (Class<?> c : classes) {
218             modules.addAll(doHarvestInnerModules(c));
219         }
220 
221         return modules;
222     }
223 
224     @Nonnull
225     private Collection<? extends Module> doHarvestInnerModules(@Nonnull Class<?> rootClass) {
226         List<Module> modules = new ArrayList<>();
227         for (Class<?> clazz : rootClass.getDeclaredClasses()) {
228             if (!Module.class.isAssignableFrom(clazz|| !Modifier.isPublic(clazz.getModifiers())) {
229                 continue;
230             }
231 
232             try {
233                 modules.add((Moduleclazz.newInstance());
234             catch (InstantiationException | IllegalAccessException e) {
235                 LOG.error("Can't instantiate module " + clazz.getName() " . Make sure it's marked as public and provides a no-args constructor.");
236             }
237         }
238         return modules;
239     }
240 
241     private void doCollectModulesFromFields(@Nonnull final Collection<Module> modules) {
242         if (testCase != null) {
243             modules.add(new FieldsModule());
244         }
245     }
246 
247     @Nonnull
248     protected List<Annotation> harvestQualifiers(@Nonnull Class<?> clazz) {
249         List<Annotation> list = new ArrayList<>();
250         Annotation[] annotations = clazz.getAnnotations();
251         for (Annotation annotation : annotations) {
252             if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) {
253                 if (BindTo.class.isAssignableFrom(annotation.getClass())) {
254                     continue;
255                 }
256 
257                 // special case for @Named
258                 if (Named.class.isAssignableFrom(annotation.getClass())) {
259                     Named named = (Namedannotation;
260                     if (isBlank(named.value())) {
261                         list.add(named(getPropertyName(clazz)));
262                         continue;
263                     }
264                 }
265                 list.add(annotation);
266             }
267         }
268         return list;
269     }
270 
271     @Nonnull
272     protected List<Annotation> harvestQualifiers(@Nonnull Field field) {
273         List<Annotation> list = new ArrayList<>();
274         Annotation[] annotations = field.getAnnotations();
275         for (Annotation annotation : annotations) {
276             if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) {
277                 if (BindTo.class.isAssignableFrom(annotation.getClass())) {
278                     continue;
279                 }
280 
281                 // special case for @Named
282                 if (Named.class.isAssignableFrom(annotation.getClass())) {
283                     Named named = (Namedannotation;
284                     if (isBlank(named.value())) {
285                         list.add(named(getPropertyName(field.getName())));
286                         continue;
287                     }
288                 }
289                 list.add(annotation);
290             }
291         }
292         return list;
293     }
294 
295     private class InnerClassesModule extends AbstractTestingModule {
296         @Override
297         @SuppressWarnings("unchecked")
298         protected void doConfigure() {
299             Class<?> clazz = testCase.getClass();
300             List<Class<?>> classes = new ArrayList<>();
301             while (clazz != null) {
302                 classes.add(clazz);
303                 clazz = clazz.getSuperclass();
304             }
305 
306             Collections.reverse(classes);
307             for (Class<?> c : classes) {
308                 harvestBindings(c);
309             }
310         }
311 
312         protected void harvestBindings(@Nonnull Class<?> rootClass) {
313             for (Class<?> clazz : rootClass.getDeclaredClasses()) {
314                 BindTo bindTo = clazz.getAnnotation(BindTo.class);
315                 if (bindTo == null) { continue}
316                 List<Annotation> qualifiers = harvestQualifiers(clazz);
317                 Annotation classifier = qualifiers.isEmpty() null : qualifiers.get(0);
318                 boolean isSingleton = clazz.getAnnotation(Singleton.class!= null;
319 
320                 AnnotatedBindingBuilder<?> abuilder = bind(bindTo.value());
321                 if (classifier != null) {
322                     LinkedBindingBuilder<?> lbuilder = abuilder.withClassifier(classifier);
323                     if (Provider.class.isAssignableFrom(clazz)) {
324                         SingletonBindingBuilder<?> sbuilder = lbuilder.toProvider((Classclazz);
325                         if (isSingleton) { sbuilder.asSingleton()}
326                     else {
327                         SingletonBindingBuilder<?> sbuilder = lbuilder.to((Classclazz);
328                         if (isSingleton) { sbuilder.asSingleton()}
329                     }
330                 else {
331                     if (Provider.class.isAssignableFrom(clazz)) {
332                         SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Classclazz);
333                         if (isSingleton) { sbuilder.asSingleton()}
334                     else {
335                         SingletonBindingBuilder<?> sbuilder = abuilder.to((Classclazz);
336                         if (isSingleton) { sbuilder.asSingleton()}
337                     }
338                 }
339             }
340         }
341     }
342 
343     private class FieldsModule extends AbstractTestingModule {
344         @Override
345         @SuppressWarnings("unchecked")
346         protected void doConfigure() {
347             Class<?> clazz = testCase.getClass();
348             List<Class<?>> classes = new ArrayList<>();
349             while (clazz != null) {
350                 classes.add(clazz);
351                 clazz = clazz.getSuperclass();
352             }
353 
354             Collections.reverse(classes);
355             for (Class<?> c : classes) {
356                 harvestBindings(c);
357             }
358         }
359 
360         protected void harvestBindings(@Nonnull Class<?> rootClass) {
361             for (Field field : rootClass.getDeclaredFields()) {
362                 BindTo bindTo = field.getAnnotation(BindTo.class);
363                 if (bindTo == null) { continue}
364                 List<Annotation> qualifiers = harvestQualifiers(field);
365                 Annotation classifier = qualifiers.isEmpty() null : qualifiers.get(0);
366                 boolean isSingleton = field.getAnnotation(Singleton.class!= null;
367 
368                 field.setAccessible(true);
369                 Object instance = null;
370                 try {
371                     instance = field.get(testCase);
372                 catch (IllegalAccessException e) {
373                     throw new IllegalArgumentException(e);
374                 }
375 
376                 if (instance != null) {
377                     AnnotatedBindingBuilder<Object> abuilder = (AnnotatedBindingBuilder<Object>bind(bindTo.value());
378                     if (classifier != null) {
379                         if (Provider.class.isAssignableFrom(instance.getClass())) {
380                             SingletonBindingBuilder<?> sbuilder = abuilder
381                                 .withClassifier(classifier)
382                                 .toProvider((Provider<Object>instance);
383                             if (isSingleton) { sbuilder.asSingleton()}
384                         else {
385                             abuilder.withClassifier(classifier).toInstance(instance);
386                         }
387                     else if (Provider.class.isAssignableFrom(instance.getClass())) {
388                         SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Provider<Object>instance);
389                         if (isSingleton) { sbuilder.asSingleton()}
390                     else {
391                         abuilder.toInstance(instance);
392                     }
393                 else {
394                     AnnotatedBindingBuilder<?> abuilder = bind(bindTo.value());
395                     if (classifier != null) {
396                         LinkedBindingBuilder<?> lbuilder = abuilder.withClassifier(classifier);
397                         if (Provider.class.isAssignableFrom(field.getType())) {
398                             SingletonBindingBuilder<?> sbuilder = lbuilder.toProvider((Classfield.getType());
399                             if (isSingleton) { sbuilder.asSingleton()}
400                         else {
401                             SingletonBindingBuilder<?> sbuilder = lbuilder.to((Classfield.getType());
402                             if (isSingleton) { sbuilder.asSingleton()}
403                         }
404                     else {
405                         if (Provider.class.isAssignableFrom(field.getType())) {
406                             SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Classfield.getType());
407                             if (isSingleton) { sbuilder.asSingleton()}
408                         else {
409                             SingletonBindingBuilder<?> sbuilder = abuilder.to((Classfield.getType());
410                             if (isSingleton) { sbuilder.asSingleton()}
411                         }
412                     }
413                 }
414             }
415         }
416     }
417 }