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