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.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   * Page navigator that builds page objects from classes, then injects them with dependencies and calls lifecycle methods.
55   * <p/>
56   * The construction process is as follows:
57   * <ol>
58   *  <li>Determine the actual class by checking for an override</li>
59   *  <li>Instantiate the class using a constructor that matches the passed arguments</li>
60   *  <li>Changes the tester to the corrent URL (if {@link #navigateToAndBind(Class, Object...)})</li>
61   *  <li>Inject all fields annotated with {@link Inject}, including private</li>
62   *  <li>Execute the supplied {@link PostInjectionProcessor}</li>
63   *  <li>Call all methods annotated with {@link WaitUntil}</li>
64   *  <li>Call all methods annotated with {@link ValidateState}</li>
65   *  <li>Call all methods annotated with {@link Init}</li>
66   * </ol>
67   * <p/>
68   * When going to a page via the {@link #navigateToAndBind(Class, Object...)} method, the page's URL is retrieved and
69   * navigated to via {@link Tester#gotoUrl(String)} after construction and initializing but before {@link WaitUntil}
70   * methods are called.
71   * <p/>
72   * This class also implements a mutable variant of {@link ConfigurableInjectionContext}, where injection configuration
73   * changes are applied in-place, by creating a new Guice injector.
74   *
75   * @since 2.0
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      * Injector used by this binder.
104      *
105      * @return injector used by this binder.
106      * @deprecated take advantage of {@link InjectionContext} API instead. Scheduled for removal in 3.0
107      */
108     @Deprecated
109     @SuppressWarnings("UnusedDeclaration")
110     public Injector injector()
111     {
112         return injector;
113     }
114 
115     // ------------------------------------------------------------------------------------------------------ PageBinder
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     // -----------------------------------------------------------------------------------------------  InjectionContext
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     // ------------------------------------------------------------------------------------------------- Private methods
220 
221     /**
222      * Calls all methods with the given annotation, starting with methods found in the topmost superclass, then calling
223      * more specific methods in subclasses. Note that this can mean that this will attempt to call the same method
224      * multiple times - once per override in the hierarchy. Will call the methods even if they're private. Skips methods
225      * if they are also annotated with {@link com.atlassian.pageobjects.browser.IgnoreBrowser}
226      * (or {@link com.atlassian.pageobjects.browser.RequireBrowser}) if the current {@link Browser}
227      * matches (does not match) any of the browsers listed in that annotation.
228      * @param instance the page object to check for the annotation
229      * @param annotation the annotation to find
230      * @throws InvocationTargetException if any matching method throws any exception.
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             // no browser in context
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      * @return the base URL for the application with no trailing slash
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      * @param page a Page
366      * @return the path segment for a page with a leading slash
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     // ---------------------------------------------------------------------------------------------------------- Phases
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     // --------------------------------------------------------------------------------------------------- DelayedBinder
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     // ------------------------------------------------------------------------------------------ InjectionConfiguration
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     // --------------------------------------------------------------------------------------------- Other inner classes
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 }