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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
94
95
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
144
145
146
147
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
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 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 }
442
443 }