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
164 tester.gotoUrl(normalisedBaseUrl() + normalisedPath(p));
165 }
166
167 public <P> void override(Class<P> oldClass, Class<? extends P> newClass)
168 {
169 checkNotNull(oldClass);
170 checkNotNull(newClass);
171 overrides.put(oldClass, newClass);
172 }
173
174
175
176
177
178
179
180
181
182
183
184 private void callLifecycleMethod(Object instance, Class<? extends Annotation> annotation) throws InvocationTargetException
185 {
186 Class clazz = instance.getClass();
187 List<Class> classes = ClassUtils.getAllSuperclasses(clazz);
188 Collections.reverse(classes);
189 classes.add(clazz);
190
191 for (Class cl : classes)
192 {
193 for (Method method : cl.getDeclaredMethods())
194 {
195 if (method.getAnnotation(annotation) != null)
196 {
197 Browser currentBrowser = BrowserUtil.getCurrentBrowser();
198 if (isIgnoredBrowser(method, method.getAnnotation(IgnoreBrowser.class), currentBrowser) ||
199 !isRequiredBrowser(method, method.getAnnotation(RequireBrowser.class), currentBrowser))
200 {
201 continue;
202 }
203
204 try
205 {
206 if (!method.isAccessible())
207 {
208 method.setAccessible(true);
209 }
210
211 method.invoke(instance);
212 }
213 catch (IllegalAccessException e)
214 {
215 throw new RuntimeException(e);
216 }
217 }
218 }
219 }
220 }
221
222 private boolean isRequiredBrowser(Method method, RequireBrowser requireBrowser, Browser currentBrowser)
223 {
224 if (requireBrowser == null)
225 return true;
226
227 for (Browser browser : requireBrowser.value())
228 {
229 if (browser != currentBrowser)
230 {
231 log.info(method.getName() + " ignored, since it requires <" + browser + ">");
232 return false;
233 }
234 }
235 return true;
236 }
237
238 private boolean isIgnoredBrowser(Method method, IgnoreBrowser ignoreBrowser, Browser currentBrowser)
239 {
240 if (ignoreBrowser == null)
241 return false;
242
243 for (Browser browser : ignoreBrowser.value())
244 {
245 if (browser == currentBrowser || browser == Browser.ALL)
246 {
247 log.info(method.getName() + " ignored, reason: " + ignoreBrowser.reason());
248 return true;
249 }
250 }
251 return false;
252 }
253
254
255
256
257 @SuppressWarnings("ConstantConditions")
258 @Override
259 @Nonnull
260 public <T> T getInstance(@Nonnull Class<T> type)
261 {
262 checkArgument(type != null, "type was null");
263 try
264 {
265 return injector.getInstance(type);
266 }
267 catch (ProvisionException e)
268 {
269 throw new IllegalArgumentException(e);
270 }
271 catch (ConfigurationException e)
272 {
273 throw new IllegalArgumentException(e);
274 }
275 }
276
277 @Override
278 public void injectStatic(@Nonnull final Class<?> targetClass)
279 {
280 injector.createChildInjector(new AbstractModule()
281 {
282 @Override
283 protected void configure()
284 {
285 requestStaticInjection(targetClass);
286 }
287 });
288 }
289
290 @Override
291 public void injectMembers(@Nonnull Object targetInstance)
292 {
293 injector.injectMembers(targetInstance);
294 }
295
296 @Override
297 @Nonnull
298 public InjectionConfiguration configure()
299 {
300 return new InjectConfiguration();
301 }
302
303 void reconfigure(Module module)
304 {
305 this.module = Modules.override(this.module).with(module);
306 this.injector = Guice.createInjector(this.module);
307 initPostInjectionProcessors();
308 }
309
310
311
312
313 private String normalisedBaseUrl()
314 {
315 final String baseUrl = productInstance.getBaseUrl();
316 if (baseUrl.endsWith("/"))
317 {
318 return baseUrl.substring(0, baseUrl.length() - 1);
319 }
320
321 return baseUrl;
322 }
323
324
325
326
327
328 private String normalisedPath(Page p)
329 {
330 final String path = p.getUrl();
331 if (!path.startsWith("/"))
332 {
333 return "/" + path;
334 }
335
336 return path;
337 }
338
339
340
341 private static interface Phase<T>
342 {
343 T execute(T pageObject);
344 }
345
346 private class InstantiatePhase<T> implements Phase<T>
347 {
348 private Class<T> pageClass;
349 private final Object[] args;
350
351 public InstantiatePhase(Class<T> pageClass, Object[] args)
352 {
353 this.pageClass = pageClass;
354 this.args = args;
355 }
356
357 @SuppressWarnings("unchecked")
358 public T execute(T t)
359 {
360 T instance;
361 Class<T> actualClass = pageClass;
362 if (overrides.containsKey(pageClass))
363 {
364 actualClass = (Class<T>) overrides.get(pageClass);
365 }
366
367 try
368 {
369 instance = instantiate(actualClass, args);
370 }
371 catch (InstantiationException e)
372 {
373 throw new IllegalArgumentException(e);
374 }
375 catch (IllegalAccessException e)
376 {
377 throw new IllegalArgumentException(e);
378 }
379 catch (InvocationTargetException e)
380 {
381 throw new IllegalArgumentException(e.getCause());
382 }
383 return instance;
384 }
385
386 @SuppressWarnings("unchecked")
387 private T instantiate(Class<T> clazz, Object[] args)
388 throws InstantiationException, IllegalAccessException, InvocationTargetException
389 {
390 if (args != null && args.length > 0)
391 {
392 for (Constructor c : clazz.getConstructors())
393 {
394 Class[] paramTypes = c.getParameterTypes();
395 if (args.length == paramTypes.length)
396 {
397 boolean match = true;
398 for (int x = 0; x < args.length; x++)
399 {
400 if (args[x] != null && !ClassUtils.isAssignable(args[x].getClass(), paramTypes[x], true
401 {
402 match = false;
403 break;
404 }
405 }
406 if (match)
407 {
408 return (T) c.newInstance(args);
409 }
410 }
411 }
412 }
413 else
414 {
415 try
416 {
417 return clazz.newInstance();
418 }
419 catch (InstantiationException ex)
420 {
421 throw new IllegalArgumentException("Error invoking default constructor", ex);
422 }
423 }
424 throw new IllegalArgumentException("Cannot find constructor on " + clazz + " to match args: " + asList(args));
425 }
426 }
427
428 private class InjectPhase<T> implements Phase<T>
429 {
430 public T execute(T t)
431 {
432 autowireInjectables(t);
433 T pageObject = t;
434 for (Binding<PostInjectionProcessor> binding : postInjectionProcessors)
435 {
436 pageObject = binding.getProvider().get().process(pageObject);
437 }
438 return pageObject;
439 }
440
441 private void autowireInjectables(final Object instance)
442 {
443 try
444 {
445 injector.injectMembers(instance);
446 }
447 catch (ConfigurationException ex)
448 {
449 throw new IllegalArgumentException(ex);
450 }
451 }
452 }
453
454 private class WaitUntilPhase<T> implements Phase<T>
455 {
456 public T execute(T pageObject)
457 {
458 try
459 {
460 callLifecycleMethod(pageObject, WaitUntil.class);
461 }
462 catch (InvocationTargetException e)
463 {
464 Throwable targetException = e.getTargetException();
465 if (targetException instanceof PageBindingWaitException)
466 {
467 throw (PageBindingWaitException) targetException;
468 }
469 else
470 {
471 throw new PageBindingWaitException(pageObject, targetException);
472 }
473 }
474 return pageObject;
475 }
476 }
477
478 private class ValidateStatePhase<T> implements Phase<T>
479 {
480 public T execute(T pageObject)
481 {
482 try
483 {
484 callLifecycleMethod(pageObject, ValidateState.class);
485 }
486 catch (InvocationTargetException e)
487 {
488 Throwable targetException = e.getTargetException();
489 if (targetException instanceof InvalidPageStateException)
490 {
491 throw (InvalidPageStateException) targetException;
492 }
493 else
494 {
495 throw new InvalidPageStateException(pageObject, targetException);
496 }
497 }
498 return pageObject;
499 }
500 }
501
502 private class InitializePhase<T> implements Phase<T>
503 {
504 public T execute(T pageObject)
505 {
506 try
507 {
508 callLifecycleMethod(pageObject, Init.class);
509 }
510 catch (InvocationTargetException e)
511 {
512 throw new PageBindingException(pageObject, e.getTargetException());
513 }
514 return pageObject;
515 }
516 }
517
518 private class InjectableDelayedBind<T> implements DelayedBinder<T>
519 {
520 private final LinkedList<Phase<T>> phases;
521 private T pageObject = null;
522
523 public InjectableDelayedBind(List<Phase<T>> phases)
524 {
525 this.phases = new LinkedList<Phase<T>>(phases);
526 }
527
528 public boolean canBind()
529 {
530 try
531 {
532 advanceTo(InitializePhase.class);
533 return true;
534 }
535 catch (PageBindingException ex)
536 {
537 return false;
538 }
539 }
540
541 private void advanceTo(Class<? extends Phase> phaseClass)
542 {
543 boolean found = false;
544 for (Phase<T> phase : phases)
545 {
546 if (phase.getClass() == phaseClass)
547 {
548 found = true;
549 }
550 }
551
552 if (found)
553 {
554 while (!phases.isEmpty())
555 {
556 pageObject = phases.getFirst().execute(pageObject);
557 if (phases.removeFirst().getClass() == phaseClass)
558 {
559 break;
560 }
561 }
562 }
563 else
564 {
565 log.debug("Already advanced to state: " + phaseClass.getName());
566 }
567 }
568
569
570 public T get()
571 {
572 advanceTo(InstantiatePhase.class);
573 return pageObject;
574 }
575
576 public DelayedBinder<T> inject()
577 {
578 advanceTo(InjectPhase.class);
579 return this;
580 }
581
582 public DelayedBinder<T> waitUntil()
583 {
584 advanceTo(WaitUntilPhase.class);
585 return this;
586 }
587
588 public DelayedBinder<T> validateState()
589 {
590 advanceTo(ValidateStatePhase.class);
591 return this;
592 }
593
594 public T bind()
595 {
596 advanceTo(InitializePhase.class);
597 return pageObject;
598 }
599 }
600
601 private final class InjectConfiguration extends AbstractInjectionConfiguration
602 {
603
604 @Override
605 @Nonnull
606 public ConfigurableInjectionContext finish()
607 {
608 reconfigure(getModule());
609 return InjectPageBinder.this;
610 }
611
612 Module getModule()
613 {
614 return new Module()
615 {
616 @Override
617 @SuppressWarnings("unchecked")
618 public void configure(Binder binder)
619 {
620 for (InterfaceToImpl intToImpl : interfacesToImpls)
621 {
622 binder.bind((Class)intToImpl.interfaceType).to(intToImpl.implementation);
623 }
624 for (InterfaceToInstance intToInstance : interfacesToInstances)
625 {
626 binder.bind((Class)intToInstance.interfaceType).toInstance(intToInstance.instance);
627 }
628 }
629 };
630 }
631 }
632
633 private final class ThisModule extends AbstractModule
634 {
635
636 @Override
637 protected void configure()
638 {
639 bind(PageBinder.class).toInstance(InjectPageBinder.this);
640 }
641 }
642 }