View Javadoc

1   package com.atlassian.pageobjects.elements.query;
2   
3   import com.atlassian.pageobjects.elements.timeout.TimeoutType;
4   import com.atlassian.pageobjects.elements.timeout.Timeouts;
5   import com.google.common.base.Supplier;
6   import com.google.common.collect.Iterables;
7   import org.apache.commons.lang.ArrayUtils;
8   import org.hamcrest.Matcher;
9   import org.hamcrest.StringDescription;
10  import org.slf4j.Logger;
11  import org.slf4j.LoggerFactory;
12  
13  import static com.atlassian.pageobjects.elements.util.StringConcat.asString;
14  import static com.google.common.base.Preconditions.checkNotNull;
15  import static org.hamcrest.Matchers.equalTo;
16  
17  /**
18   * Utilities to create miscellaneous {@link TimedCondition}s.
19   *
20   */
21  public final class Conditions
22  {
23      private static final Logger log = LoggerFactory.getLogger(Conditions.class);
24  
25      private static final int DEFAULT_TIMEOUT = 100;
26  
27  
28      private Conditions()
29      {
30          throw new AssertionError("No way");
31      }
32  
33      /**
34       * Return new timed condition that is a negation of <tt>condition</tt>.
35       *
36       * @param condition condition to be negated
37       * @return negated {@link TimedCondition} instance.
38       */
39      public static TimedQuery<Boolean> not(TimedQuery<Boolean> condition)
40      {
41          if (condition instanceof Not)
42          {
43              return asDecorator(condition).wrapped;
44          }
45          return new Not(condition);
46      }
47  
48      /**
49       * <p>
50       * Return new combinable condition that is logical product of <tt>conditions</tt>.
51       *
52       * <p>
53       * The resulting condition will have interval of the first condition
54       * in the <tt>conditions</tt> array,
55       *
56       * @param conditions conditions to conjoin
57       * @return product of <tt>conditions</tt>
58       * @throws IllegalArgumentException if <tt>conditions</tt> array is <code>null</code> or empty
59       *
60       * @see TimedCondition#interval()
61       */
62      public static CombinableCondition and(TimedQuery<Boolean>... conditions)
63      {
64          return new And(conditions);
65      }
66  
67      /**
68       * <p>
69       * Return new combinable condition that is logical product of <tt>conditions</tt>.
70       *
71       * <p>
72       * The resulting condition will have interval of the first condition
73       * in the <tt>conditions</tt> array,
74       *
75       * @param conditions conditions to conjoin
76       * @return product of <tt>conditions</tt>
77       * @throws IllegalArgumentException if <tt>conditions</tt> array is <code>null</code> or empty
78       *
79       * @see TimedCondition#interval()
80       */
81      public static CombinableCondition and(Iterable<TimedQuery<Boolean>> conditions)
82      {
83          return and(Iterables.toArray(conditions, TimedQuery.class));
84      }
85  
86      /**
87       * <p>
88       * Return new combinable condition that is logical sum of <tt>conditions</tt>.
89       *
90       * <p>
91       * The resulting condition will have interval of the first condition
92       * in the <tt>conditions</tt> array,
93       *
94       * @param conditions conditions to sum
95       * @return logical sum of <tt>conditions</tt>
96       * @throws IllegalArgumentException if <tt>conditions</tt> array is <code>null</code> or empty
97       *
98       * @see TimedCondition#interval()
99       */
100     public static CombinableCondition or(TimedQuery<Boolean>... conditions)
101     {
102         return new Or(conditions);
103     }
104 
105     /**
106      * <p>
107      * Return new combinable condition that is logical sum of <tt>conditions</tt>.
108      *
109      * <p>
110      * The resulting condition will have interval of the first condition
111      * in the <tt>conditions</tt> array,
112      *
113      * @param conditions conditions to sum
114      * @return logical sum of <tt>conditions</tt>
115      * @throws IllegalArgumentException if <tt>conditions</tt> array is <code>null</code> or empty
116      *
117      * @see TimedCondition#interval()
118      */
119     public static CombinableCondition or(Iterable<TimedQuery<Boolean>> conditions)
120     {
121         return or(Iterables.toArray(conditions, TimedQuery.class));
122     }
123 
124     /**
125      * <p>
126      * Returns a condition that combines <tt>original</tt> and <tt>dependant</tt> in a manner that dependant condition
127      * will only ever be retrieved if the <tt>original</tt> condition is <code>true</code>. This is useful
128      * when dependant condition may only be retrieved given the original condition is <code>true</code>.
129      * </p>
130      *
131      * <p>
132      * The supplier for dependant condition is allowed to return <code>null</code> or throw exception if the
133      * original condition returns false. But it <i>may not</i> do so given the original condition is <code>true</code>,
134      * as this will lead to <code>NullPointerException</code> or the raised exception be propagated by
135      * this condition respectively.
136      * </p>
137      *
138      * @param original original condition
139      * @param dependant supplier for dependant condition that will only be evaluated given the original condition
140      * evaluates to <code>true</code>
141      * @return new dependant condition
142      */
143     public static TimedCondition dependantCondition(TimedQuery<Boolean> original, Supplier<TimedQuery<Boolean>> dependant)
144     {
145         return new DependantCondition(original, dependant);
146     }
147 
148 
149     /**
150      * <p>
151      * Return condition that will be <code>true</code>, if given <tt>matcher</tt> will match the <tt>query</tt>. Any
152      * Hamcrest matcher implementation may be used.
153      * </p>
154      * <p>
155      * Example:<br>
156      *
157      * <code>
158      *     TimedCondition textEquals = Conditions.forMatcher(element.getText(), isEqualTo("blah"));
159      * </code>
160      * </p>
161      *
162      * @param query timed query to match
163      * @param matcher matcher for the query
164      * @param <T> type of the result
165      * @return new matching condition
166      */
167     public static <T> TimedCondition forMatcher(TimedQuery<T> query, Matcher<? super T> matcher)
168     {
169         return new MatchingCondition<T>(query, matcher);
170     }
171 
172     /**
173      * Returns timed condition verifying that given query will evaluate to value equal to <tt>value</tt>. The timeouts
174      * are inherited from the provided <tt>query</tt>
175      *
176      * @param value value that <tt>query</tt> should be equalt to
177      * @param query the timed query
178      * @param <T> type of the value
179      * @return timed condition for query equality to value
180      */
181     public static <T> TimedCondition isEqual(T value, TimedQuery<T> query)
182     {
183         return forMatcher(query, equalTo(value));
184     }
185 
186 
187     /**
188      * Returns a timed condition, whose current evaluation is based on a value provided by given <tt>supplier</tt>.
189      *
190      * @param supplier supplier of the current condition value
191      * @return new query based on supplier
192      */
193     public static TimedCondition forSupplier(final Supplier<Boolean> supplier)
194     {
195         return forSupplier(DEFAULT_TIMEOUT, supplier);
196     }
197 
198     /**
199      * Returns a timed condition, whose current evaluation is based on a value provided by given <tt>supplier</tt>.
200      *
201      * @param defaultTimeout default timeout of the condition
202      * @param supplier supplier of the current condition value
203      * @return new query based on supplier
204      */
205     public static TimedCondition forSupplier(long defaultTimeout, final Supplier<Boolean> supplier)
206     {
207         return new AbstractTimedCondition(defaultTimeout, PollingQuery.DEFAULT_INTERVAL) {
208             @Override
209             protected Boolean currentValue() {
210                 return supplier.get();
211             }
212         };
213     }
214 
215     /**
216      * Returns a timed condition, whose current evaluation is based on a value provided by given <tt>supplier</tt>.
217      *
218      * @param timeouts an instance of timeouts to use for the new condition
219      * @param supplier supplier of the current condition value
220      * @return new query based on supplier
221      */
222     public static TimedCondition forSupplier(Timeouts timeouts, final Supplier<Boolean> supplier)
223     {
224         return new AbstractTimedCondition(timeouts.timeoutFor(TimeoutType.DEFAULT), timeouts.timeoutFor(TimeoutType.EVALUATION_INTERVAL)) {
225             @Override
226             protected Boolean currentValue() {
227                 return supplier.get();
228             }
229         };
230     }
231 
232     /**
233      * A timed condition that always returns <code>true</code>
234      *
235      * @return timed condition that always returns true
236      */
237     public static TimedCondition alwaysTrue()
238     {
239         return new StaticCondition(true);
240     }
241 
242     /**
243      * A timed condition that always returns <code>false</code>
244      *
245      * @return timed condition that always returns false
246      */
247     public static TimedCondition alwaysFalse()
248     {
249         return new StaticCondition(false);
250     }
251 
252     private static AbstractConditionDecorator asDecorator(TimedQuery<Boolean> condition)
253     {
254         return (AbstractConditionDecorator) condition;
255     }
256 
257 
258     private static class StaticCondition extends AbstractTimedCondition
259     {
260         private final Boolean value;
261 
262         public StaticCondition(Boolean value)
263         {
264             super(DEFAULT_TIMEOUT, DEFAULT_INTERVAL);
265             this.value = checkNotNull(value);
266         }
267 
268         @Override
269         protected Boolean currentValue()
270         {
271             return value;
272         }
273     }
274 
275 
276     /**
277      * A timed condition that may be logically combined with others, by means of basic logical operations: 'and'/'or'. 
278      *
279      */
280     public static interface CombinableCondition extends TimedCondition
281     {
282         /**
283          * Combine <tt>other</tt> condition with this condition logical query, such that the resulting condition
284          * represents a logical product of this condition and <tt>other</tt>.
285          *
286          * @param other condition to combine with this one
287          * @return new combined 'and' condition
288          */
289         CombinableCondition and(TimedCondition other);
290 
291         /**
292          * Combine <tt>other</tt> condition with this condition logical query, such that the resulting condition
293          * represents a logical sum of this condition and <tt>other</tt>.
294          *
295          * @param other condition to combine with this one
296          * @return new combined 'or' condition
297          */
298         CombinableCondition or(TimedCondition other);
299 
300     }
301 
302 
303     private abstract static class AbstractConditionDecorator extends AbstractTimedCondition
304     {
305         protected final TimedQuery<Boolean> wrapped;
306 
307         public AbstractConditionDecorator(TimedQuery<Boolean> wrapped)
308         {
309             super(wrapped);
310             this.wrapped = checkNotNull(wrapped, "wrapped");
311         }
312     }
313 
314     private abstract static class AbstractConditionsDecorator extends AbstractTimedCondition implements CombinableCondition
315     {
316         protected final TimedQuery<Boolean>[] conditions;
317 
318         public AbstractConditionsDecorator(TimedQuery<Boolean>... conditions)
319         {
320             super(conditions[0]);
321             this.conditions = conditions;
322         }
323 
324         @Override
325         public String toString()
326         {
327             StringBuilder answer = new StringBuilder(conditions.length * 20).append(getClass().getName()).append(":\n");
328             for (TimedQuery<Boolean> condition : conditions)
329             {
330                 answer.append(" -").append(condition.toString()).append('\n');
331             }
332             return answer.deleteCharAt(answer.length()-1).toString();
333         }
334     }
335 
336     private static class Not extends AbstractConditionDecorator
337     {
338         public Not(TimedQuery<Boolean> other)
339         {
340             super(other);
341         }
342 
343         public Boolean currentValue()
344         {
345             return !wrapped.now();
346         }
347 
348         @Override
349         public String toString()
350         {
351             return asString("Negated: <", wrapped, ">");
352         }
353     }
354 
355     private static class And extends AbstractConditionsDecorator
356     {
357         public And(TimedQuery<Boolean>... conditions)
358         {
359             super(conditions);
360         }
361 
362         And(TimedQuery<Boolean>[] somes, TimedQuery<Boolean>[] more)
363         {
364             super((TimedCondition[]) ArrayUtils.addAll(somes, more));
365         }
366 
367         And(TimedQuery<Boolean>[] somes, TimedQuery<Boolean> oneMore)
368         {
369             super((TimedCondition[]) ArrayUtils.add(somes, oneMore));
370         }
371 
372         public Boolean currentValue()
373         {
374             boolean result = true;
375             for (TimedQuery<Boolean> condition : conditions)
376             {
377                 // null should not really happen if TimedCondition contract is observed
378                 final boolean next = condition.now() != null ? condition.now() : false;
379                 result &= next;
380                 if (!result)
381                 {
382                     log.debug(asString("[And] Condition <",condition,"> returned false"));
383                     break;
384                 }
385             }
386             return result;
387         }
388 
389         public CombinableCondition and(TimedCondition other)
390         {
391             if (other.getClass().equals(And.class))
392             {
393                 return new And(this.conditions, ((And) other).conditions);
394             }
395             return new And(this.conditions, other);
396         }
397 
398         public CombinableCondition or(TimedCondition other)
399         {
400             if (other instanceof Or)
401             {
402                 return ((Or)other).or(this);
403             }
404             return new Or(this, other);
405         }
406     }
407 
408     private static class Or extends AbstractConditionsDecorator
409     {
410         public Or(TimedQuery<Boolean>... conditions)
411         {
412             super(conditions);
413         }
414 
415         Or(TimedQuery<Boolean>[] somes, TimedQuery<Boolean>[] more)
416         {
417             super((TimedCondition[]) ArrayUtils.addAll(somes, more));
418         }
419 
420         Or(TimedQuery<Boolean>[] somes, TimedQuery<Boolean> oneMore)
421         {
422             super((TimedCondition[]) ArrayUtils.add(somes, oneMore));
423         }
424 
425         public Boolean currentValue()
426         {
427             boolean result = false;
428             for (TimedQuery<Boolean> condition : conditions)
429             {
430                 // null should not really happen if TimedCondition contract is observed
431                 final boolean next = condition.now() != null ? condition.now() : false;
432                 result |= next;
433                 if (result)
434                 {
435                     break;
436                 }
437                 log.debug(asString("[Or] Condition <",condition,"> returned false"));
438             }
439             return result;
440         }
441 
442         public CombinableCondition and(TimedCondition other)
443         {
444             if (other instanceof And)
445             {
446                 return ((And)other).and(this);
447             }
448             return new And(this, other);
449         }
450 
451         public CombinableCondition or(TimedCondition other)
452         {
453             if (other.getClass().equals(Or.class))
454             {
455                 return new Or(this.conditions, ((Or) other).conditions);
456             }
457             return new Or(this.conditions, other);
458         }
459     }
460 
461     private static final class DependantCondition extends AbstractConditionDecorator
462     {
463         private final Supplier<TimedQuery<Boolean>> dependant;
464 
465         DependantCondition(TimedQuery<Boolean> original, Supplier<TimedQuery<Boolean>> dependant)
466         {
467             super(original);
468             this.dependant = checkNotNull(dependant, "dependant");
469         }
470 
471         @Override
472         public Boolean currentValue()
473         {
474             return wrapped.now() && dependant.get().now();
475         }
476 
477         @Override
478         public String toString()
479         {
480             if (wrapped.now())
481             {
482                 TimedQuery<Boolean> dep = dependant.get();
483                 return asString("DependantCondition[original=",wrapped,",dependant=",dep,"]");
484             }
485             return asString("DependantCondition[original=",wrapped,"]");
486         }
487     }
488 
489 
490     static final class MatchingCondition<T> extends AbstractTimedCondition
491     {
492         final TimedQuery<T> query;
493         final Matcher<? super T> matcher;
494 
495         T lastValue;
496 
497         public MatchingCondition(final TimedQuery<T> query, final Matcher<? super T> matcher)
498         {
499             super(query);
500             this.query = checkNotNull(query);
501             this.matcher = checkNotNull(matcher);
502         }
503 
504         @Override
505         protected Boolean currentValue()
506         {
507             try
508             {
509                 lastValue = query.now();
510                 return matcher.matches(lastValue);
511             }
512             catch (Exception e)
513             {
514                 log.debug(String.format("TimedQuery.now() threw an exception. Not a match for %s", matcher), e);
515                 return false;
516             }
517         }
518 
519         @Override
520         public String toString()
521         {
522             return super.toString() + new StringDescription().appendText("[query=").appendValue(query)
523                     .appendText("][matcher=").appendDescriptionOf(matcher).appendText("]");
524         }
525     }
526 }