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<Player> players = ...;
039 *
040 * Condition<Player> potentialMVP = new Condition<Player>("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<Player> players = ...;
087 *
088 * Condition<Player> potentialMVP = new Condition<Player>("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<Player> players = ...;
126 *
127 * Condition<Player> potentialMVP = new Condition<Player>("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<Player> players = ...;
176 *
177 * Condition<Player> potentialMVP = new Condition<Player>("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<Player> players = ...;
201 *
202 * Condition<Player> mvpStats = new Condition<Player>("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<E> (even if the constructor parameter type was an array).
420 * @return the Iterable<E> containing the filtered elements.
421 */
422 public Iterable<E> get() {
423 return filteredIterable;
424 }
425
426 }