View Javadoc

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   * Page navigator that builds page objects from classes, then injects them with dependencies and calls lifecycle methods.
53   * <p/>
54   * <p>The construction process is as follows:
55   * <ol>
56   * <li>Determine the actual class by checking for an override</li>
57   * <li>Instantiate the class using a constructor that matches the passed arguments</li>
58   * <li>Changes the tester to the corrent URL (if {@link #navigateToAndBind(Class, Object...)})</li>
59   * <li>Inject all fields annotated with {@link Inject}, including private</li>
60   * <li>Execute the supplied {@link PostInjectionProcessor}</li>
61   * <li>Call all methods annotated with {@link WaitUntil}</li>
62   * <li>Call all methods annotated with {@link ValidateState}</li>
63   * <li>Call all methods annotated with {@link Init}</li>
64   * </ol>
65   * <p/>
66   * <p>When going to a page via the {@link #navigateToAndBind(Class, Object...)} method, the page's URL is retrieved and navigated to
67   * via {@link Tester#gotoUrl(String)} after construction and initializing but before {@link WaitUntil} methods are called.
68   *
69   * <p/>
70   * This class also implements a mutable variant of {@link com.atlassian.pageobjects.inject.ConfigurableInjectionContext},
71   * where injection configuration changes are applied in-place, by creating a new Guice injector.
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      * Injector used by this binder.
126      *
127      * @return injector used by this binder.
128      * @deprecated take advantage of {@link InjectionContext} API instead. Scheduled for removal in 3.0
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      * Calls all methods with the given annotation, starting with methods found in the topmost superclass, then calling
177      * more specific methods in subclasses. Note that this can mean that this will attempt to call the same method
178      * multiple times - once per override in the hierarchy. Will call the methods even if they're private. Skips methods
179      * if they are also annotated with {@link com.atlassian.pageobjects.browser.IgnoreBrowser} (or {@link com.atlassian.pageobjects.browser.RequireBrowser}) if the current {@link Browser}
180      * matches (does not match) any of the browsers listed in that annotation.
181      * @param instance the page object to check for the annotation
182      * @param annotation the annotation to find
183      * @throws InvocationTargetException if any matching method throws any exception.
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     // -----------------------------------------------------------------------------------------------  InjectionContext
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     // ---------------------------------------------------------------------------------------------------------- Phases
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 /*autoboxing*/))
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 }