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