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