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 * @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<Player> players = ...;
088 *
089 * Condition<Player> potentialMVP = new Condition<Player>("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<Player> players = ...;
127 *
128 * Condition<Player> potentialMVP = new Condition<Player>("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<Player> players = ...;
177 *
178 * Condition<Player> potentialMVP = new Condition<Player>("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<? super 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<Player> players = ...;
202 *
203 * Condition<Player> mvpStats = new Condition<Player>("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<? super 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<? super 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 // As we don't know the propertyValue class, we use Object.class
313 Class<? extends Object> propertyValueClass = propertyValue == null ? Object.class : propertyValue.getClass();
314 Object propertyValueOfCurrentElement =
315 propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValueClass, element);
316 if (areEqual(propertyValueOfCurrentElement, propertyValue)) {
317 newFilteredIterable.add(element);
318 }
319 }
320 this.filteredIterable = newFilteredIterable;
321 return this;
322 }
323
324 /**
325 * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>not equals
326 * to</b> given value.
327 * <p>
328 * Typical usage :
329 *
330 * <pre>
331 * filter(employees).with("name").notEqualsTo("Vader").get();
332 * </pre>
333 *
334 * @param propertyValue the filter value.
335 * @return this {@link Filters} to chain other filter operation.
336 * @throws NullPointerException if the property name to filter on has not been set.
337 */
338 public Filters<E> notEqualsTo(Object propertyValue) {
339 checkPropertyNameToFilterOnIsNotNull();
340 List<E> newFilteredIterable = new ArrayList<E>();
341 for (E element : filteredIterable) {
342 Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValue
343 .getClass(), element);
344 if (!areEqual(propertyValueOfCurrentElement, propertyValue)) {
345 newFilteredIterable.add(element);
346 }
347 }
348 this.filteredIterable = newFilteredIterable;
349 return this;
350 }
351
352 private void checkPropertyNameToFilterOnIsNotNull() {
353 if (propertyNameToFilterOn == null)
354 throw new NullPointerException("The property name to filter on has not been set - no filtering is possible");
355 }
356
357 /**
358 * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>equals to</b>
359 * one of the given values.
360 * <p>
361 * Typical usage :
362 *
363 * <pre>
364 * filter(players).with("team").in("Bulls", "Lakers").get();
365 * </pre>
366 *
367 * @param propertyValues the filter values.
368 * @return this {@link Filters} to chain other filter operation.
369 * @throws NullPointerException if the property name to filter on has not been set.
370 */
371 public Filters<E> in(Object... propertyValues) {
372 checkPropertyNameToFilterOnIsNotNull();
373 List<E> newFilteredIterable = new ArrayList<E>();
374 for (E element : filteredIterable) {
375 Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValues
376 .getClass().getComponentType(), element);
377 if (isItemInArray(propertyValueOfCurrentElement, propertyValues)) {
378 newFilteredIterable.add(element);
379 }
380 }
381 this.filteredIterable = newFilteredIterable;
382 return this;
383 }
384
385 /**
386 * Filters the underlying iterable to keep object with property (specified by {@link #with(String)}) <b>not in</b> the
387 * given values.
388 * <p>
389 * Typical usage :
390 *
391 * <pre>
392 * filter(players).with("team").notIn("Heat", "Lakers").get();
393 * </pre>
394 *
395 * @param propertyValues the filter values.
396 * @return this {@link Filters} to chain other filter operation.
397 * @throws NullPointerException if the property name to filter on has not been set.
398 */
399 public Filters<E> notIn(Object... propertyValues) {
400 checkPropertyNameToFilterOnIsNotNull();
401 List<E> newFilteredIterable = new ArrayList<E>();
402 for (E element : filteredIterable) {
403 Object propertyValueOfCurrentElement = propertySupport.propertyValueOf(propertyNameToFilterOn, propertyValues
404 .getClass().getComponentType(), element);
405 if (!isItemInArray(propertyValueOfCurrentElement, propertyValues)) {
406 newFilteredIterable.add(element);
407 }
408 }
409 this.filteredIterable = newFilteredIterable;
410 return this;
411 }
412
413 /**
414 * Returns <code>true</code> if given item is in given array, <code>false</code> otherwise.
415 * @param item the object to look for in arrayOfValues
416 * @param arrayOfValues the array of values
417 * @return <code>true</code> if given item is in given array, <code>false</code> otherwise.
418 */
419 private static boolean isItemInArray(Object item, Object[] arrayOfValues) {
420 for (Object value : arrayOfValues)
421 if (areEqual(value, item)) return true;
422 return false;
423 }
424
425 /**
426 * Returns the resulting filtered Iterable<E> (even if the constructor parameter type was an array).
427 * @return the Iterable<E> containing the filtered elements.
428 */
429 public Iterable<E> get() {
430 return filteredIterable;
431 }
432
433 }