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