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