1   package com.atlassian.pageobjects.binder;
2   
3   import com.atlassian.pageobjects.DelayedBinder;
4   import com.atlassian.pageobjects.Page;
5   import com.atlassian.pageobjects.PageBinder;
6   import com.atlassian.pageobjects.ProductInstance;
7   import com.atlassian.pageobjects.Tester;
8   import com.google.common.collect.Lists;
9   import com.google.inject.Binder;
10  import com.google.inject.Binding;
11  import com.google.inject.ConfigurationException;
12  import com.google.inject.Guice;
13  import com.google.inject.Injector;
14  import com.google.inject.Module;
15  import org.apache.commons.lang.ClassUtils;
16  import org.slf4j.Logger;
17  import org.slf4j.LoggerFactory;
18  
19  import java.lang.annotation.Annotation;
20  import java.lang.reflect.Constructor;
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.LinkedList;
27  import java.util.List;
28  import java.util.Map;
29  import javax.inject.Inject;
30  
31  import static com.google.common.base.Preconditions.checkNotNull;
32  import static com.google.common.collect.Iterables.concat;
33  import static java.util.Arrays.asList;
34  import static java.util.Collections.singleton;
35  import static java.util.Collections.unmodifiableList;
36  
37  /**
38   * Page navigator that builds page objects from classes, then injects them with dependencies and calls lifecycle methods.
39   * <p/>
40   * <p>The construction process is as follows:
41   * <ol>
42   * <li>Determine the actual class by checking for an override</li>
43   * <li>Instantiate the class using a constructor that matches the passed arguments</li>
44   * <li>Changes the tester to the corrent URL (if {@link #navigateToAndBind(Class, Object...)})</li>
45   * <li>Inject all fields annotated with {@link Inject}, including private</li>
46   * <li>Execute the supplied {@link PostInjectionProcessor}</li>
47   * <li>Call all methods annotated with {@link WaitUntil}</li>
48   * <li>Call all methods annotated with {@link ValidateState}</li>
49   * <li>Call all methods annotated with {@link Init}</li>
50   * </ol>
51   * <p/>
52   * <p>When going to a page via the {@link #navigateToAndBind(Class, Object...)} method, the page's URL is retrieved and navigated to
53   * via {@link Tester#gotoUrl(String)} after construction and initializing but before {@link WaitUntil} methods are called.
54   */
55  public final class InjectPageBinder implements PageBinder
56  {
57      private final Tester tester;
58      private final ProductInstance productInstance;
59      private static final Logger log = LoggerFactory.getLogger(InjectPageBinder.class);
60  
61      private final Map<Class<?>, Class<?>> overrides =
62              new HashMap<Class<?>, Class<?>>();
63      private final Injector injector;
64      private final List<Binding<PostInjectionProcessor>> postInjectionProcessors;
65  
66      public InjectPageBinder(ProductInstance productInstance, Tester tester, Module... modules)
67      {
68          checkNotNull(productInstance);
69          checkNotNull(tester);
70          checkNotNull(modules);
71          this.tester = tester;
72          this.productInstance = productInstance;
73          this.injector = Guice.createInjector(concat(asList(modules), singleton(new Module()
74          {
75              public void configure(Binder binder)
76              {
77                  binder.bind(PageBinder.class).toInstance(InjectPageBinder.this);
78              }
79          })));
80          List<Binding<PostInjectionProcessor>> procs = Lists.newArrayList();
81          for (Binding binding : injector.getAllBindings().values())
82          {
83              if (PostInjectionProcessor.class.isAssignableFrom(binding.getKey().getTypeLiteral().getRawType()))
84              {
85                  procs.add(binding);
86              }
87          }
88          postInjectionProcessors = unmodifiableList(procs);
89      }
90  
91  
92      /**
93       * Injector used by this binder.
94       *
95       * @return injector used by this binder.
96       */
97      public Injector injector()
98      {
99          return injector;
100     }
101 
102     public <P extends Page> P navigateToAndBind(Class<P> pageClass, Object... args)
103     {
104         checkNotNull(pageClass);
105         DelayedBinder<P> binder = delayedBind(pageClass, args);
106         P p = binder.get();
107         visitUrl(p);
108         return binder.bind();
109     }
110 
111     public <P> P bind(Class<P> pageClass, Object... args)
112     {
113         checkNotNull(pageClass);
114         return delayedBind(pageClass, args).bind();
115     }
116 
117     public <P> DelayedBinder<P> delayedBind(Class<P> pageClass, Object... args)
118     {
119         return new InjectableDelayedBind<P>(asList(
120                 new InstantiatePhase<P>(pageClass, args),
121                 new InjectPhase<P>(),
122                 new WaitUntilPhase<P>(),
123                 new ValidateStatePhase<P>(),
124                 new InitializePhase<P>()));
125     }
126 
127     protected void visitUrl(Page p)
128     {
129         checkNotNull(p);
130         String pageUrl = p.getUrl();
131         String baseUrl = productInstance.getBaseUrl();
132         tester.gotoUrl(baseUrl + pageUrl);
133     }
134 
135     public <P> void override(Class<P> oldClass, Class<? extends P> newClass)
136     {
137         checkNotNull(oldClass);
138         checkNotNull(newClass);
139         overrides.put(oldClass, newClass);
140     }
141 
142     /**
143      * Iterates over all superclasses in reverse order and the class of the instance and
144      * checks public, protected and private methods for the provided annotation.
145      * @param instance the page object to check for the annotation
146      * @param annotation the annotation to find
147      * @throws InvocationTargetException
148      */
149     private void callLifecycleMethod(Object instance, Class<? extends Annotation> annotation) throws InvocationTargetException
150     {
151         Class clazz = instance.getClass();
152         List<Class> classes = ClassUtils.getAllSuperclasses(clazz);
153         Collections.reverse(classes);
154         classes.add(clazz);
155 
156         for (Class cl : classes)
157         {
158             for (Method method : cl.getDeclaredMethods())
159             {
160                 if (method.getAnnotation(annotation) != null)
161                 {
162                     try
163                     {
164                         if (!method.isAccessible())
165                         {
166                             method.setAccessible(true);
167                         }
168 
169                         method.invoke(instance);
170                     }
171                     catch (IllegalAccessException e)
172                     {
173                         throw new RuntimeException(e);
174                     }
175                 }
176             }
177         }
178     }
179 
180     private static interface Phase<T>
181     {
182         T execute(T pageObject);
183     }
184 
185     private class InstantiatePhase<T> implements Phase<T>
186     {
187         private Class<T> pageClass;
188         private final Object[] args;
189 
190         public InstantiatePhase(Class<T> pageClass, Object[] args)
191         {
192             this.pageClass = pageClass;
193             this.args = args;
194         }
195 
196         @SuppressWarnings("unchecked")
197         public T execute(T t)
198         {
199             T instance;
200             Class<T> actualClass = pageClass;
201             if (overrides.containsKey(pageClass))
202             {
203                 actualClass = (Class<T>) overrides.get(pageClass);
204             }
205 
206             try
207             {
208                 instance = instantiate(actualClass, args);
209             }
210             catch (InstantiationException e)
211             {
212                 throw new IllegalArgumentException(e);
213             }
214             catch (IllegalAccessException e)
215             {
216                 throw new IllegalArgumentException(e);
217             }
218             catch (InvocationTargetException e)
219             {
220                 throw new IllegalArgumentException(e.getCause());
221             }
222             return instance;
223         }
224 
225         @SuppressWarnings("unchecked")
226         private T instantiate(Class<T> clazz, Object[] args)
227                 throws InstantiationException, IllegalAccessException, InvocationTargetException
228         {
229             if (args != null && args.length > 0)
230             {
231                 for (Constructor c : clazz.getConstructors())
232                 {
233                     Class[] paramTypes = c.getParameterTypes();
234                     if (args.length == paramTypes.length)
235                     {
236                         boolean match = true;
237                         for (int x = 0; x < args.length; x++)
238                         {
239                             if (args[x] != null && !ClassUtils.isAssignable(args[x].getClass(), paramTypes[x], true /*autoboxing*/))
240                             {
241                                 match = false;
242                                 break;
243                             }
244                         }
245                         if (match)
246                         {
247                             return (T) c.newInstance(args);
248                         }
249                     }
250                 }
251             }
252             else
253             {
254                 try
255                 {
256                     return clazz.newInstance();
257                 }
258                 catch (InstantiationException ex)
259                 {
260                     throw new IllegalArgumentException("Error invoking default constructor", ex);
261                 }
262             }
263             throw new IllegalArgumentException("Cannot find constructor on " + clazz + " to match args: " + asList(args));
264         }
265     }
266 
267     private class InjectPhase<T> implements Phase<T>
268     {
269         public T execute(T t)
270         {
271             autowireInjectables(t);
272             T pageObject = t;
273             for (Binding<PostInjectionProcessor> binding : postInjectionProcessors)
274             {
275                 pageObject = binding.getProvider().get().process(pageObject);
276             }
277             return pageObject;
278         }
279 
280         private void autowireInjectables(final Object instance)
281         {
282             try
283             {
284                 injector.injectMembers(instance);
285             }
286             catch (ConfigurationException ex)
287             {
288                 throw new IllegalArgumentException(ex);
289             }
290         }
291     }
292 
293     private class WaitUntilPhase<T> implements Phase<T>
294     {
295         public T execute(T pageObject)
296         {
297             try
298             {
299                 callLifecycleMethod(pageObject, WaitUntil.class);
300             }
301             catch (InvocationTargetException e)
302             {
303                 Throwable targetException = e.getTargetException();
304                 if (targetException instanceof PageBindingWaitException)
305                 {
306                     throw (PageBindingWaitException) targetException;
307                 }
308                 else
309                 {
310                     throw new PageBindingWaitException(pageObject, targetException);
311                 }
312             }
313             return pageObject;
314         }
315     }
316 
317     private class ValidateStatePhase<T> implements Phase<T>
318     {
319         public T execute(T pageObject)
320         {
321             try
322             {
323                 callLifecycleMethod(pageObject, ValidateState.class);
324             }
325             catch (InvocationTargetException e)
326             {
327                 Throwable targetException = e.getTargetException();
328                 if (targetException instanceof InvalidPageStateException)
329                 {
330                     throw (InvalidPageStateException) targetException;
331                 }
332                 else
333                 {
334                     throw new InvalidPageStateException(pageObject, targetException);
335                 }
336             }
337             return pageObject;
338         }
339     }
340 
341     private class InitializePhase<T> implements Phase<T>
342     {
343         public T execute(T pageObject)
344         {
345             try
346             {
347                 callLifecycleMethod(pageObject, Init.class);
348             }
349             catch (InvocationTargetException e)
350             {
351                 throw new PageBindingException(pageObject, e.getTargetException());
352             }
353             return pageObject;
354         }
355     }
356 
357     private class InjectableDelayedBind<T> implements DelayedBinder<T>
358     {
359         private final LinkedList<Phase<T>> phases;
360         private T pageObject = null;
361 
362         public InjectableDelayedBind(List<Phase<T>> phases)
363         {
364             this.phases = new LinkedList<Phase<T>>(phases);
365         }
366 
367         public boolean canBind()
368         {
369             try
370             {
371                 advanceTo(ValidateStatePhase.class);
372                 return true;
373             }
374             catch (PageBindingException ex)
375             {
376                 return false;
377             }
378         }
379 
380         private void advanceTo(Class<? extends Phase> phaseClass)
381         {
382             boolean found = false;
383             for (Phase<T> phase : phases)
384             {
385                 if (phase.getClass() == phaseClass)
386                 {
387                     found = true;
388                 }
389             }
390 
391             if (found)
392             {
393                 Phase<T> currentPhase;
394                 while (!phases.isEmpty())
395                 {
396                     currentPhase = phases.removeFirst();
397                     pageObject = currentPhase.execute(pageObject);
398                     if (currentPhase.getClass() == phaseClass)
399                     {
400                         break;
401                     }
402                 }
403             }
404             else
405             {
406                 log.debug("Already advanced to state: " + phaseClass.getName());
407             }
408         }
409 
410 
411         public T get()
412         {
413             advanceTo(InstantiatePhase.class);
414             return pageObject;
415         }
416 
417         public DelayedBinder<T> inject()
418         {
419             advanceTo(InjectPhase.class);
420             return this;
421         }
422 
423         public DelayedBinder<T> waitUntil()
424         {
425             advanceTo(WaitUntilPhase.class);
426             return this;
427         }
428 
429         public DelayedBinder<T> validateState()
430         {
431             advanceTo(ValidateStatePhase.class);
432             return this;
433         }
434 
435         public T bind()
436         {
437             advanceTo(InitializePhase.class);
438             return pageObject;
439         }
440     }
441 }