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 ((TestModuleAware) testCase).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 = ((TestModuleAware) testCase).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((Module) clazz.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 = (Named) annotation;
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 = (Named) annotation;
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((Class) clazz);
327 if (isSingleton) { sbuilder.asSingleton(); }
328 } else {
329 SingletonBindingBuilder<?> sbuilder = lbuilder.to((Class) clazz);
330 if (isSingleton) { sbuilder.asSingleton(); }
331 }
332 } else {
333 if (Provider.class.isAssignableFrom(clazz)) {
334 SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Class) clazz);
335 if (isSingleton) { sbuilder.asSingleton(); }
336 } else {
337 SingletonBindingBuilder<?> sbuilder = abuilder.to((Class) clazz);
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((Class) field.getType());
402 if (isSingleton) { sbuilder.asSingleton(); }
403 } else {
404 SingletonBindingBuilder<?> sbuilder = lbuilder.to((Class) field.getType());
405 if (isSingleton) { sbuilder.asSingleton(); }
406 }
407 } else {
408 if (Provider.class.isAssignableFrom(field.getType())) {
409 SingletonBindingBuilder<?> sbuilder = abuilder.toProvider((Class) field.getType());
410 if (isSingleton) { sbuilder.asSingleton(); }
411 } else {
412 SingletonBindingBuilder<?> sbuilder = abuilder.to((Class) field.getType());
413 if (isSingleton) { sbuilder.asSingleton(); }
414 }
415 }
416 }
417 }
418 }
419 }
420 }
|