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