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<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<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<E> (even if the constructor parameter type was an array).
425 * @return the Iterable<E> containing the filtered elements.
426 */
427 public Iterable<E> get() {
428 return filteredIterable;
429 }
430
431 }