1 package com.atlassian.pageobjects.binder;
2
3 import com.atlassian.annotations.Internal;
4 import com.atlassian.pageobjects.DelayedBinder;
5 import com.atlassian.pageobjects.Page;
6 import com.atlassian.pageobjects.PageBinder;
7 import com.atlassian.pageobjects.ProductInstance;
8 import com.atlassian.pageobjects.Tester;
9 import com.atlassian.pageobjects.browser.Browser;
10 import com.atlassian.pageobjects.browser.IgnoreBrowser;
11 import com.atlassian.pageobjects.browser.RequireBrowser;
12 import com.atlassian.pageobjects.inject.AbstractInjectionConfiguration;
13 import com.atlassian.pageobjects.inject.ConfigurableInjectionContext;
14 import com.atlassian.pageobjects.inject.InjectionConfiguration;
15 import com.atlassian.pageobjects.inject.InjectionContext;
16 import com.atlassian.pageobjects.util.BrowserUtil;
17 import com.google.common.collect.ImmutableMap;
18 import com.google.common.collect.Lists;
19 import com.google.inject.AbstractModule;
20 import com.google.inject.Binder;
21 import com.google.inject.Binding;
22 import com.google.inject.ConfigurationException;
23 import com.google.inject.Guice;
24 import com.google.inject.Injector;
25 import com.google.inject.Key;
26 import com.google.inject.Module;
27 import com.google.inject.ProvisionException;
28 import com.google.inject.util.Modules;
29 import org.apache.commons.lang.ClassUtils;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import javax.annotation.Nonnull;
34 import javax.annotation.concurrent.NotThreadSafe;
35 import javax.inject.Inject;
36 import java.lang.annotation.Annotation;
37 import java.lang.reflect.Constructor;
38 import java.lang.reflect.InvocationTargetException;
39 import java.lang.reflect.Method;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.LinkedList;
43 import java.util.List;
44 import java.util.Map;
45
46 import static com.google.common.base.Preconditions.checkArgument;
47 import static com.google.common.base.Preconditions.checkNotNull;
48 import static java.util.Arrays.asList;
49 import static java.util.Collections.unmodifiableList;
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73 @NotThreadSafe
74 @Internal
75 public final class InjectPageBinder implements PageBinder, ConfigurableInjectionContext
76 {
77 private final Tester tester;
78 private final ProductInstance productInstance;
79 private static final Logger log = LoggerFactory.getLogger(InjectPageBinder.class);
80
81 private final Map<Class<?>, Class<?>> overrides = new HashMap<Class<?>, Class<?>>();
82 private volatile Module module;
83 private volatile Injector injector;
84 private volatile List<Binding<PostInjectionProcessor>> postInjectionProcessors;
85
86 public InjectPageBinder(ProductInstance productInstance, Tester tester, Module... modules)
87 {
88 checkNotNull(productInstance);
89 checkNotNull(tester);
90 checkNotNull(modules);
91 this.tester = tester;
92 this.productInstance = productInstance;
93 this.module = Modules.override(modules).with(new ThisModule());
94 this.injector = Guice.createInjector(module);
95 initPostInjectionProcessors();
96 }
97
98 private void initPostInjectionProcessors()
99 {
100 List<Binding<PostInjectionProcessor>> procs = Lists.newArrayList();
101 for (Binding binding : collectBindings().values())
102 {
103 if (PostInjectionProcessor.class.isAssignableFrom(binding.getKey().getTypeLiteral().getRawType()))
104 {
105 procs.add(binding);
106 }
107 }
108 postInjectionProcessors = unmodifiableList(procs);
109 }
110
111 private Map<Key<?>, Binding<?>> collectBindings()
112 {
113 ImmutableMap.Builder<Key<?>, Binding<?>> result = ImmutableMap.builder();
114 Injector current = this.injector;
115 while (current != null)
116 {
117 result.putAll(injector.getAllBindings());
118 current = current.getParent();
119 }
120 return result.build();
121 }
122
123
124
125
126
127
128
129
130 public Injector injector()
131 {
132 return injector;
133 }
134
135 public <P extends Page> P navigateToAndBind(Class<P> pageClass, Object... args)
136 {
137 checkNotNull(pageClass);
138 DelayedBinder<P> binder = delayedBind(pageClass, args);
139 P p = binder.get();
140 visitUrl(p);
141 return binder.bind();
142 }
143
144 public <P> P bind(Class<P> pageClass, Object... args)
145 {
146 checkNotNull(pageClass);
147 return delayedBind(pageClass, args).bind();
148 }
149
150 public <P> DelayedBinder<P> delayedBind(Class<P> pageClass, Object... args)
151 {
152 return new InjectableDelayedBind<P>(asList(
153 new InstantiatePhase<P>(pageClass, args),
154 new InjectPhase<P>(),
155 new WaitUntilPhase<P>(),
156 new ValidateStatePhase<P>(),
157 new InitializePhase<P>()));
158 }
159
160 protected void visitUrl(Page p)
161 {
162 checkNotNull(p);
163 String pageUrl = p.getUrl();
164 String baseUrl = productInstance.getBaseUrl();
165 tester.gotoUrl(baseUrl + pageUrl);
166 }
167
168 public <P> void override(Class<P> oldClass, Class<? extends P> newClass)
169 {
170 checkNotNull(oldClass);
171 checkNotNull(newClass);
172 overrides.put(oldClass, newClass);
173 }
174
175
176
177
178
179
180
181
182
183
184
185 private void callLifecycleMethod(Object instance, Class<? extends Annotation> annotation) throws InvocationTargetException
186 {
187 Class clazz = instance.getClass();
188 List<Class> classes = ClassUtils.getAllSuperclasses(clazz);
189 Collections.reverse(classes);
190 classes.add(clazz);
191
192 for (Class cl : classes)
193 {
194 for (Method method : cl.getDeclaredMethods())
195 {
196 if (method.getAnnotation(annotation) != null)
197 {
198 Browser currentBrowser = BrowserUtil.getCurrentBrowser();
199 if (isIgnoredBrowser(method, method.getAnnotation(IgnoreBrowser.class), currentBrowser) ||
200 !isRequiredBrowser(method, method.getAnnotation(RequireBrowser.class), currentBrowser))
201 {
202 continue;
203 }
204
205 try
206 {
207 if (!method.isAccessible())
208 {
209 method.setAccessible(true);
210 }
211
212 method.invoke(instance);
213 }
214 catch (IllegalAccessException e)
215 {
216 throw new RuntimeException(e);
217 }
218 }
219 }
220 }
221 }
222
223 private boolean isRequiredBrowser(Method method, RequireBrowser requireBrowser, Browser currentBrowser)
224 {
225 if (requireBrowser == null)
226 return true;
227
228 for (Browser browser : requireBrowser.value())
229 {
230 if (browser != currentBrowser)
231 {
232 log.info(method.getName() + " ignored, since it requires <" + browser + ">");
233 return false;
234 }
235 }
236 return true;
237 }
238
239 private boolean isIgnoredBrowser(Method method, IgnoreBrowser ignoreBrowser, Browser currentBrowser)
240 {
241 if (ignoreBrowser == null)
242 return false;
243
244 for (Browser browser : ignoreBrowser.value())
245 {
246 if (browser == currentBrowser || browser == Browser.ALL)
247 {
248 log.info(method.getName() + " ignored, reason: " + ignoreBrowser.reason());
249 return true;
250 }
251 }
252 return false;
253 }
254
255
256
257
258 @SuppressWarnings("ConstantConditions")
259 @Override
260 @Nonnull
261 public <T> T getInstance(@Nonnull Class<T> type)
262 {
263 checkArgument(type != null, "type was null");
264 try
265 {
266 return injector.getInstance(type);
267 }
268 catch (ProvisionException e)
269 {
270 throw new IllegalArgumentException(e);
271 }
272 catch (ConfigurationException e)
273 {
274 throw new IllegalArgumentException(e);
275 }
276 }
277
278 @Override
279 public void injectStatic(@Nonnull final Class<?> targetClass)
280 {
281 injector.createChildInjector(new AbstractModule()
282 {
283 @Override
284 protected void configure()
285 {
286 requestStaticInjection(targetClass);
287 }
288 });
289 }
290
291 @Override
292 public void injectMembers(@Nonnull Object targetInstance)
293 {
294 injector.injectMembers(targetInstance);
295 }
296
297 @Override
298 @Nonnull
299 public InjectionConfiguration configure()
300 {
301 return new InjectConfiguration();
302 }
303
304 void reconfigure(Module module)
305 {
306 this.module = Modules.override(this.module).with(module);
307 this.injector = Guice.createInjector(this.module);
308 initPostInjectionProcessors();
309 }
310
311
312
313 private static interface Phase<T>
314 {
315 T execute(T pageObject);
316 }
317
318 private class InstantiatePhase<T> implements Phase<T>
319 {
320 private Class<T> pageClass;
321 private final Object[] args;
322
323 public InstantiatePhase(Class<T> pageClass, Object[] args)
324 {
325 this.pageClass = pageClass;
326 this.args = args;
327 }
328
329 @SuppressWarnings("unchecked")
330 public T execute(T t)
331 {
332 T instance;
333 Class<T> actualClass = pageClass;
334 if (overrides.containsKey(pageClass))
335 {
336 actualClass = (Class<T>) overrides.get(pageClass);
337 }
338
339 try
340 {
341 instance = instantiate(actualClass, args);
342 }
343 catch (InstantiationException e)
344 {
345 throw new IllegalArgumentException(e);
346 }
347 catch (IllegalAccessException e)
348 {
349 throw new IllegalArgumentException(e);
350 }
351 catch (InvocationTargetException e)
352 {
353 throw new IllegalArgumentException(e.getCause());
354 }
355 return instance;
356 }
357
358 @SuppressWarnings("unchecked")
359 private T instantiate(Class<T> clazz, Object[] args)
360 throws InstantiationException, IllegalAccessException, InvocationTargetException
361 {
362 if (args != null && args.length > 0)
363 {
364 for (Constructor c : clazz.getConstructors())
365 {
366 Class[] paramTypes = c.getParameterTypes();
367 if (args.length == paramTypes.length)
368 {
369 boolean match = true;
370 for (int x = 0; x < args.length; x++)
371 {
372 if (args[x] != null && !ClassUtils.isAssignable(args[x].getClass(), paramTypes[x], true
373 {
374 match = false;
375 break;
376 }
377 }
378 if (match)
379 {
380 return (T) c.newInstance(args);
381 }
382 }
383 }
384 }
385 else
386 {
387 try
388 {
389 return clazz.newInstance();
390 }
391 catch (InstantiationException ex)
392 {
393 throw new IllegalArgumentException("Error invoking default constructor", ex);
394 }
395 }
396 throw new IllegalArgumentException("Cannot find constructor on " + clazz + " to match args: " + asList(args));
397 }
398 }
399
400 private class InjectPhase<T> implements Phase<T>
401 {
402 public T execute(T t)
403 {
404 autowireInjectables(t);
405 T pageObject = t;
406 for (Binding<PostInjectionProcessor> binding : postInjectionProcessors)
407 {
408 pageObject = binding.getProvider().get().process(pageObject);
409 }
410 return pageObject;
411 }
412
413 private void autowireInjectables(final Object instance)
414 {
415 try
416 {
417 injector.injectMembers(instance);
418 }
419 catch (ConfigurationException ex)
420 {
421 throw new IllegalArgumentException(ex);
422 }
423 }
424 }
425
426 private class WaitUntilPhase<T> implements Phase<T>
427 {
428 public T execute(T pageObject)
429 {
430 try
431 {
432 callLifecycleMethod(pageObject, WaitUntil.class);
433 }
434 catch (InvocationTargetException e)
435 {
436 Throwable targetException = e.getTargetException();
437 if (targetException instanceof PageBindingWaitException)
438 {
439 throw (PageBindingWaitException) targetException;
440 }
441 else
442 {
443 throw new PageBindingWaitException(pageObject, targetException);
444 }
445 }
446 return pageObject;
447 }
448 }
449
450 private class ValidateStatePhase<T> implements Phase<T>
451 {
452 public T execute(T pageObject)
453 {
454 try
455 {
456 callLifecycleMethod(pageObject, ValidateState.class);
457 }
458 catch (InvocationTargetException e)
459 {
460 Throwable targetException = e.getTargetException();
461 if (targetException instanceof InvalidPageStateException)
462 {
463 throw (InvalidPageStateException) targetException;
464 }
465 else
466 {
467 throw new InvalidPageStateException(pageObject, targetException);
468 }
469 }
470 return pageObject;
471 }
472 }
473
474 private class InitializePhase<T> implements Phase<T>
475 {
476 public T execute(T pageObject)
477 {
478 try
479 {
480 callLifecycleMethod(pageObject, Init.class);
481 }
482 catch (InvocationTargetException e)
483 {
484 throw new PageBindingException(pageObject, e.getTargetException());
485 }
486 return pageObject;
487 }
488 }
489
490 private class InjectableDelayedBind<T> implements DelayedBinder<T>
491 {
492 private final LinkedList<Phase<T>> phases;
493 private T pageObject = null;
494
495 public InjectableDelayedBind(List<Phase<T>> phases)
496 {
497 this.phases = new LinkedList<Phase<T>>(phases);
498 }
499
500 public boolean canBind()
501 {
502 try
503 {
504 advanceTo(InitializePhase.class);
505 return true;
506 }
507 catch (PageBindingException ex)
508 {
509 return false;
510 }
511 }
512
513 private void advanceTo(Class<? extends Phase> phaseClass)
514 {
515 boolean found = false;
516 for (Phase<T> phase : phases)
517 {
518 if (phase.getClass() == phaseClass)
519 {
520 found = true;
521 }
522 }
523
524 if (found)
525 {
526 while (!phases.isEmpty())
527 {
528 pageObject = phases.getFirst().execute(pageObject);
529 if (phases.removeFirst().getClass() == phaseClass)
530 {
531 break;
532 }
533 }
534 }
535 else
536 {
537 log.debug("Already advanced to state: " + phaseClass.getName());
538 }
539 }
540
541
542 public T get()
543 {
544 advanceTo(InstantiatePhase.class);
545 return pageObject;
546 }
547
548 public DelayedBinder<T> inject()
549 {
550 advanceTo(InjectPhase.class);
551 return this;
552 }
553
554 public DelayedBinder<T> waitUntil()
555 {
556 advanceTo(WaitUntilPhase.class);
557 return this;
558 }
559
560 public DelayedBinder<T> validateState()
561 {
562 advanceTo(ValidateStatePhase.class);
563 return this;
564 }
565
566 public T bind()
567 {
568 advanceTo(InitializePhase.class);
569 return pageObject;
570 }
571 }
572
573 private final class InjectConfiguration extends AbstractInjectionConfiguration
574 {
575
576 @Override
577 @Nonnull
578 public ConfigurableInjectionContext finish()
579 {
580 reconfigure(getModule());
581 return InjectPageBinder.this;
582 }
583
584 Module getModule()
585 {
586 return new Module()
587 {
588 @Override
589 @SuppressWarnings("unchecked")
590 public void configure(Binder binder)
591 {
592 for (InterfaceToImpl intToImpl : interfacesToImpls)
593 {
594 binder.bind((Class)intToImpl.interfaceType).to(intToImpl.implementation);
595 }
596 for (InterfaceToInstance intToInstance : interfacesToInstances)
597 {
598 binder.bind((Class)intToInstance.interfaceType).toInstance(intToInstance.instance);
599 }
600 }
601 };
602 }
603 }
604
605 private final class ThisModule extends AbstractModule
606 {
607
608 @Override
609 protected void configure()
610 {
611 bind(PageBinder.class).toInstance(InjectPageBinder.this);
612 }
613 }
614 }