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