001    /*
002     * Created on Feb 22, 2011
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
005     * the License. You may obtain a copy of the License at
006     * 
007     * http://www.apache.org/licenses/LICENSE-2.0
008     * 
009     * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
010     * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
011     * specific language governing permissions and limitations under the License.
012     * 
013     * Copyright @2011 the original author or authors.
014     */
015    package org.fest.assertions.api.filter;
016    
017    import static org.fest.util.Collections.list;
018    import static org.fest.util.Objects.areEqual;
019    
020    import java.util.ArrayList;
021    import java.util.List;
022    
023    import org.fest.assertions.core.Condition;
024    import org.fest.assertions.internal.PropertySupport;
025    import org.fest.util.IntrospectionError;
026    import org.fest.util.VisibleForTesting;
027    
028    /**
029     * Filters the elements of a given <code>{@link Iterable}</code> or array according to the specified filter criteria.
030     * <p>
031     * Filter criteria can be expressed either by a {@link Condition} or a pseudo filter language on elements properties.
032     * <p>
033     * Note that the given {@link Iterable} or array is not modified, the filters are performed on a copy.
034     * <p>
035     * With {@link Condition} :
036     * 
037     * <pre>
038     * List&lt;Player&gt; players = ...; 
039     *   
040     * Condition&lt;Player&gt; potentialMVP = new Condition&lt;Player&gt;("is a possible MVP"){
041     *   public boolean matches(Player player) {
042     *     return player.getPointsPerGame() > 20 && player.getAssistsPerGame() > 7;
043     *   };
044     * };
045     * 
046     * // use filter static method to build Filters
047     * assertThat(filter(players).being(potentialMVP).get()).containsOnly(james, rose)
048     * </pre>
049     * 
050     * With pseudo filter language on element properties :
051     * 
052     * <pre>
053     * assertThat(filter(players).with("pointsPerGame").greaterThan(20)
054     *                           .and("assistsPerGame").greaterThan(7)
055     *                           .get()).containsOnly(james, rose);</pre>
056     * 
057     * @param <E> the type of element of group to filter.
058     * 
059     * @author Joel Costigliola
060     * @author Mikhail Mazursky
061     */
062    public class Filters<E> {
063    
064      // initialIterable is never modified, it represents the group before any filters have been performed
065      @VisibleForTesting
066      final Iterable<E> initialIterable;
067      Iterable<E> filteredIterable;
068    
069      private PropertySupport propertySupport = PropertySupport.instance();
070    
071      /**
072       * The name of the property used for filtering.
073       */
074      private String propertyNameToFilterOn;
075    
076      /**
077       * Creates a new <code>{@link Filters}</code> with the {@link Iterable} to filter.
078       * <p>
079       * Chain this call to express filter criteria either by a {@link Condition} or a pseudo filter language on elements
080       * properties.
081       * <p>
082       * Note that the given {@link Iterable} is not modified, the filters are performed on a copy.
083       * <p>
084       * - With {@link Condition} :
085       * 
086       * <pre>
087       * List&lt;Player&gt; players = ...; 
088       *   
089       * Condition&lt;Player&gt; potentialMVP = new Condition&lt;Player&gt;("is a possible MVP"){
090       *   public boolean matches(Player player) {
091       *     return player.getPointsPerGame() > 20 && player.getAssistsPerGame() > 7;
092       *   };
093       * };
094       * 
095       * // use filter static method to build Filters
096       * assertThat(filter(players).being(potentialMVP).get()).containsOnly(james, rose)
097       * </pre>
098       * 
099       * - With pseudo filter language on element properties :
100       * 
101       * <pre>
102       * assertThat(filter(players).with("pointsPerGame").greaterThan(20)
103       *                           .and("assistsPerGame").greaterThan(7).get())
104       *                           .containsOnly(james, rose);</pre>
105       * 
106       * @param iterable the {@code Iterable} to filter.
107       * @throws NullPointerException if the given iterable is {@code null}.
108       * @return the created <code>{@link Filters}</code>.
109       */
110      public static <E> Filters<E> filter(Iterable<E> iterable) {
111        if (iterable == null) throw new NullPointerException("The iterable to filter should not be null");
112        return new Filters<E>(iterable);
113      }
114    
115      /**
116       * Creates a new <code>{@link Filters}</code> with the array to filter.
117       * <p>
118       * Chain this call to express filter criteria either by a {@link Condition} or a pseudo filter language on elements
119       * properties.
120       * <p>
121       * Note that the given array is not modified, the filters are performed on an {@link Iterable} copy of the array.
122       * <p>
123       * With {@link Condition} :
124       * 
125       * <pre>
126       * List&lt;Player&gt; players = ...; 
127       *   
128       * Condition&lt;Player&gt; potentialMVP = new Condition&lt;Player&gt;("is a possible MVP"){
129       *   public boolean matches(Player player) {
130       *     return player.getPointsPerGame() > 20 && player.getAssistsPerGame() > 7;
131       *   };
132       * };
133       * 
134       * // use filter static method to build Filters
135       * assertThat(filter(players).being(potentialMVP).get()).containsOnly(james, rose);
136       * </pre>
137       * 
138       * With pseudo filter language on element properties :
139       * 
140       * <pre>
141       * assertThat(filter(players).with("pointsPerGame").greaterThan(20)
142       *                           .and("assistsPerGame").greaterThan(7)
143       *                           .get()).containsOnly(james, rose);</pre>
144       * @param array the array to filter.
145       * @throws NullPointerException if the given array is {@code null}.
146       * @return the created <code>{@link Filters}</code>.
147       */
148      public static <E> Filters<E> filter(E[] array) {
149        if (array == null) throw new NullPointerException("The array to filter should not be null");
150        return new Filters<E>(array);
151      }
152    
153      @VisibleForTesting
154      Filters(Iterable<E> iterable) {
155        this.initialIterable = iterable;
156        // copy list to avoid modifying iterable
157        this.filteredIterable = list(iterable);
158      }
159    
160      @VisibleForTesting
161      Filters(E[] array) {
162        List<E> iterable = new ArrayList<E>(array.length);
163        for (int i = 0; i < array.length; i++) {
164          iterable.add(array[i]);
165        }
166        this.initialIterable = iterable;
167        // copy list to avoid modifying iterable
168        this.filteredIterable = list(iterable);
169      }
170    
171      /**
172       * Filter the underlying group, keeping only elements satisfying the given {@link Condition}.<br>
173       * Same as {@link #having(Condition)} - pick the method you prefer to have the most readable code.
174       * 
175       * <pre>
176       * List&lt;Player&gt; players = ...; 
177       *   
178       * Condition&lt;Player&gt; potentialMVP = new Condition&lt;Player&gt;("is a possible MVP") {
179       *   public boolean matches(Player player) {
180       *     return player.getPointsPerGame() > 20 && player.getAssistsPerGame() > 7;
181       *   };
182       * };
183       * 
184       * // use filter static method to build Filters
185       * assertThat(filter(players).being(potentialMVP).get()).containsOnly(james, rose);</pre>
186       * 
187       * @param condition the filter {@link Condition}.
188       * @return this {@link Filters} to chain other filter operations.
189       * @throws NullPointerException if the given condition is {@code null}.
190       */
191      public Filters<E> being(Condition<E> condition) {
192        if (condition == null) throw new NullPointerException("The filter condition should not be null");
193        return applyFilterCondition(condition);
194      }
195    
196      /**
197       * Filter the underlying group, keeping only elements satisfying the given {@link Condition}.<br>
198       * Same as {@link #being(Condition)} - pick the method you prefer to have the most readable code.
199       * 
200       * <pre>
201       * List&lt;Player&gt; players = ...; 
202       *   
203       * Condition&lt;Player&gt; mvpStats = new Condition&lt;Player&gt;("is a possible MVP") {
204       *   public boolean matches(Player player) {
205       *     return player.getPointsPerGame() > 20 && player.getAssistsPerGame() > 7;
206       *   };
207       * };
208       * 
209       * // use filter static method to build Filters
210       * assertThat(filter(players).having(mvpStats).get()).containsOnly(james, rose);</pre>
211       * 
212       * @param condition the filter {@link Condition}.
213       * @return this {@link Filters} to chain other filter operations.
214       * @throws NullPointerException if the given condition is {@code null}.
215       */
216      public Filters<E> having(Condition<E> condition) {
217        if (condition == null) throw new NullPointerException("The filter condition should not be null");
218        return applyFilterCondition(condition);
219      }
220    
221      private Filters<E> applyFilterCondition(Condition<E> condition) {
222        List<E> newFilteredIterable = new ArrayList<E>();
223        for (E element : filteredIterable) {
224          if (condition.matches(element)) {
225            newFilteredIterable.add(element);
226          }
227        }
228        this.filteredIterable = newFilteredIterable;
229        return this;
230      }
231    
232      /**
233       * Filter the underlying group, keeping only elements with a property equals to given value.
234       * <p>
235       * Let's, for example, filter Employees with name "Alex" :
236       * 
237       * <pre>
238       * filter(employees).with("name", "Alex").get();
239       * </pre>
240       * which is shortcut of :
241       * 
242       * <pre>
243       * filter(employees).with("name").equalsTo("Alex").get();
244       * </pre>
245       * 
246       * @param propertyName the name of the property whose value will compared to given value. It may be a nested property.
247       * @param propertyValue the expected property value.
248       * @return this {@link Filters} to chain other filter operations.
249       * @throws IntrospectionError if an element in the given {@code Iterable} does not have a property with a given
250       *           propertyName.
251       * @throws NullPointerException if the given propertyName is {@code null}.
252       */
253      public Filters<E> with(String propertyName, Object propertyValue) {
254        if (propertyName == null) throw new NullPointerException("The property name to filter on should not be null");
255        propertyNameToFilterOn = propertyName;
256        return equalsTo(propertyValue);
257      }
258    
259      /**
260       * Sets the name of the property used for filtering, it may be a nested property like
261       * <code>"adress.street.name"</code>.
262       * <p>
263       * The typical usage is to chain this call with a comparison method, for example :
264       * 
265       * <pre>
266       * filter(employees).with("name").equalsTo("Alex").get();
267       * </pre>
268       * 
269       * @param propertyName the name of the property used for filtering. It may be a nested property.
270       * @return this {@link Filters} to chain other filter operation.
271       * @throws NullPointerException if the given propertyName is {@code null}.
272       */
273      public Filters<E> with(String propertyName) {
274        if (propertyName == null) throw new NullPointerException("The property name to filter on should not be null");
275        propertyNameToFilterOn = propertyName;
276        return this;
277      }
278    
279      /**
280       * Alias of {@link #with(String)} for synthetic sugar to write things like :.
281       * 
282       * <pre>
283       * filter(employees).with("name").equalsTo("Alex").and("job").notEqualsTo("lawyer").get();
284       * </pre>
285       * 
286       * @param propertyName the name of the property used for filtering. It may be a nested property.
287       * @return this {@link Filters} to chain other filter operation.
288       * @throws NullPointerException if the given propertyName is {@code null}.
289       */
290      public Filters<E> and(String propertyName) {
291        return with(propertyName);
292      }
293    
294      /**
295       * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>equals to</b>
296       * given value.
297       * <p>
298       * Typical usage :
299       * 
300       * <pre>
301       * filter(employees).with("name").equalsTo("Luke").get();
302       * </pre>
303       * 
304       * @param propertyValue the filter value.
305       * @return this {@link Filters} to chain other filter operation.
306       * @throws NullPointerException if the property name to filter on has not been set.
307       */
308      public Filters<E> equalsTo(Object propertyValue) {
309        checkPropertyNameToFilterOnIsNotNull();
310        List<E> newFilteredIterable = new ArrayList<E>();
311        for (E element : filteredIterable) {
312          Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValue
313              .getClass(), element);
314          if (areEqual(propertyValueOfCurrentElement, propertyValue)) {
315            newFilteredIterable.add(element);
316          }
317        }
318        this.filteredIterable = newFilteredIterable;
319        return this;
320      }
321    
322      /**
323       * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>not equals
324       * to</b> given value.
325       * <p>
326       * Typical usage :
327       * 
328       * <pre>
329       * filter(employees).with("name").notEqualsTo("Vader").get();
330       * </pre>
331       * 
332       * @param propertyValue the filter value.
333       * @return this {@link Filters} to chain other filter operation.
334       * @throws NullPointerException if the property name to filter on has not been set.
335       */
336      public Filters<E> notEqualsTo(Object propertyValue) {
337        checkPropertyNameToFilterOnIsNotNull();
338        List<E> newFilteredIterable = new ArrayList<E>();
339        for (E element : filteredIterable) {
340          Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValue
341              .getClass(), element);
342          if (!areEqual(propertyValueOfCurrentElement, propertyValue)) {
343            newFilteredIterable.add(element);
344          }
345        }
346        this.filteredIterable = newFilteredIterable;
347        return this;
348      }
349    
350      private void checkPropertyNameToFilterOnIsNotNull() {
351        if (propertyNameToFilterOn == null)
352          throw new NullPointerException("The property name to filter on has not been set - no filtering is possible");
353      }
354    
355      /**
356       * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>equals to</b>
357       * one of the given values.
358       * <p>
359       * Typical usage :
360       * 
361       * <pre>
362       * filter(players).with("team").in("Bulls", "Lakers").get();
363       * </pre>
364       * 
365       * @param propertyValues the filter values.
366       * @return this {@link Filters} to chain other filter operation.
367       * @throws NullPointerException if the property name to filter on has not been set.
368       */
369      public Filters<E> in(Object... propertyValues) {
370        checkPropertyNameToFilterOnIsNotNull();
371        List<E> newFilteredIterable = new ArrayList<E>();
372        for (E element : filteredIterable) {
373          Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValues
374              .getClass().getComponentType(), element);
375          if (isItemInArray(propertyValueOfCurrentElement, propertyValues)) {
376            newFilteredIterable.add(element);
377          }
378        }
379        this.filteredIterable = newFilteredIterable;
380        return this;
381      }
382    
383      /**
384       * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>not in</b> the
385       * given values.
386       * <p>
387       * Typical usage :
388       * 
389       * <pre>
390       * filter(players).with("team").notIn("Heat", "Lakers").get();
391       * </pre>
392       * 
393       * @param propertyValues the filter values.
394       * @return this {@link Filters} to chain other filter operation.
395       * @throws NullPointerException if the property name to filter on has not been set.
396       */
397      public Filters<E> notIn(Object... propertyValues) {
398        checkPropertyNameToFilterOnIsNotNull();
399        List<E> newFilteredIterable = new ArrayList<E>();
400        for (E element : filteredIterable) {
401          Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValues
402              .getClass().getComponentType(), element);
403          if (!isItemInArray(propertyValueOfCurrentElement, propertyValues)) {
404            newFilteredIterable.add(element);
405          }
406        }
407        this.filteredIterable = newFilteredIterable;
408        return this;
409      }
410    
411      /**
412       * Returns <code>true</code> if given item is in given array, <code>false</code> otherwise.
413       * @param item the object to look for in arrayOfValues
414       * @param arrayOfValues the array of values
415       * @return <code>true</code> if given item is in given array, <code>false</code> otherwise.
416       */
417      private static boolean isItemInArray(Object item, Object[] arrayOfValues) {
418        for (Object value : arrayOfValues)
419          if (areEqual(value, item)) return true;
420        return false;
421      }
422    
423      /**
424       * Returns the resulting filtered Iterable&lt;E&gt; (even if the constructor parameter type was an array).
425       * @return the Iterable&lt;E&gt; containing the filtered elements.
426       */
427      public Iterable<E> get() {
428        return filteredIterable;
429      }
430    
431    }