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 }