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 }