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 
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      * Calls all methods with the given annotation, starting with methods found in the topmost superclass, then calling
176      * more specific methods in subclasses. Note that this can mean that this will attempt to call the same method
177      * multiple times - once per override in the hierarchy. Will call the methods even if they're private. Skips methods
178      * if they are also annotated with {@link com.atlassian.pageobjects.browser.IgnoreBrowser} (or {@link com.atlassian.pageobjects.browser.RequireBrowser}) if the current {@link Browser}
179      * matches (does not match) any of the browsers listed in that annotation.
180      * @param instance the page object to check for the annotation
181      * @param annotation the annotation to find
182      * @throws InvocationTargetException if any matching method throws any exception.
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     // -----------------------------------------------------------------------------------------------  InjectionContext
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      * @return the base URL for the application with no trailing slash
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      * @param p a Page
326      * @return the path segment for a page with a leading slash
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     // ---------------------------------------------------------------------------------------------------------- Phases
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 /*autoboxing*/))
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 }