    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.beanutils2;
    019
    020import java.lang.reflect.InvocationTargetException;
    021import java.util.Objects;
    022import java.util.function.Predicate;
    023
    024import org.apache.commons.logging.Log;
    025import org.apache.commons.logging.LogFactory;
    026
    027/**
    028 * <p>
    029 * {@code Predicate} that evaluates a property value against a specified value.
    030 * </p>
    031 * <p>
    032 * An implementation of {@link java.util.function.Predicate} that evaluates a property value on the object provided against a specified value and returns
    033 * {@code true} if equal; {@code false} otherwise. The {@code BeanPropertyValueEqualsPredicate} constructor takes two parameters which determine what property
    034 * will be evaluated on the target object and what its expected value should be.
    035 * </p>
    036 * <dl>
    037 * <dt><strong> {@code public BeanPropertyValueEqualsPredicate( String propertyName, Object propertyValue )} </strong></dt>
    038 * <dd>Will create a {@code Predicate} that will evaluate the target object and return
    039 * {@code true</code> if the property specified by <code>propertyName} has a value which
    040 *       is equal to the value specified by {@code propertyValue}. Or return
    041 *       {@code false} otherwise.
    042 *    </dd>
    043 * </dl>
    044 * <p>
    045 * <strong>Note:</strong> Property names can be a simple, nested, indexed, or mapped property as defined by
    046 * {@code org.apache.commons.beanutils2.PropertyUtils}.  If any object in the property path
    047 * specified by {@code propertyName</code> is <code>null} then the outcome is based on the
    048 * value of the {@code ignoreNull} attribute.
    049 * </p>
    050 * <p>
    051 * A typical usage might look like:
    052 * </p>
    053 * <pre>{@code
    054 * // create the closure
    055 * BeanPropertyValueEqualsPredicate predicate =
    056 *    new BeanPropertyValueEqualsPredicate( "activeEmployee", Boolean.FALSE );
    057 *
    058 * // filter the Collection
    059 * CollectionUtils.filter( peopleCollection, predicate );
    060 * }</pre>
    061 * <p>
    062 * This would take a {@code Collection} of person objects and filter out any people whose
    063 * {@code activeEmployee</code> property is <code>false}. Assuming...
    064 * </p>
    065 * <ul>
    066 *    <li>
    067 *       The top level object in the {@code peopleCollection} is an object which represents a
    068 *       person.
    069 *    </li>
    070 *    <li>
    071 *       The person object has a {@code getActiveEmployee()} method which returns
    072 *       the boolean value for the object's {@code activeEmployee} property.
    073 *    </li>
    074 * </ul>
    075 * <p>
    076 * Another typical usage might look like:
    077 * </p>
    078 * <pre>{@code
    079 * // create the closure
    080 * BeanPropertyValueEqualsPredicate predicate =
    081 *    new BeanPropertyValueEqualsPredicate( "personId", "456-12-1234" );
    082 *
    083 * // search the Collection
    084 * CollectionUtils.find( peopleCollection, predicate );
    085 * }</pre>
    086 * <p>
    087 * This would search a {@code Collection} of person objects and return the first object whose
    088 * {@code personId</code> property value equals <code>456-12-1234}. Assuming...
    089 * </p>
    090 * <ul>
    091 * <li>The top level object in the {@code peopleCollection} is an object which represents a person.</li>
    092 * <li>The person object has a {@code getPersonId()} method which returns the value for the object's {@code personId} property.</li>
    093 * </ul>
    094 *
    095 * @param <T> The type of the input to the predicate.
    096 * @param <V> The property value type.
    097 * @see org.apache.commons.beanutils2.PropertyUtils
    098 * @see java.util.function.Predicate
    099 */
    100public class BeanPropertyValueEqualsPredicate<T, V> implements Predicate<T> {
    101
    102    /** For logging. Each subclass gets its own log instance. */
    103    private final Log log = LogFactory.getLog(this.getClass());
    104
    105    /**
    106     * The name of the property which will be evaluated when this {@code Predicate} is executed.
    107     */
    108    private final String propertyName;
    109
    110    /**
    111     * The value that the property specified by {@code propertyName} will be compared to when this {@code Predicate} executes.
    112     */
    113    private final V propertyValue;
    114
    115    /**
    116     * <p>
    117     * Should {@code null} objects in the property path be ignored?
    118     * </p>
    119     * <p>
    120     * Determines whether {@code null} objects in the property path will generate an
    121     * {@code IllegalArgumentException</code> or not. If set to <code>true} then if any objects
    122     * in the property path evaluate to {@code null} then the
    123     * {@code IllegalArgumentException</code> throw by <code>PropertyUtils} will be logged but
    124     * not re-thrown and {@code false</code> will be returned.  If set to <code>false} then if
    125     * any objects in the property path evaluate to {@code null} then the
    126     * {@code IllegalArgumentException</code> throw by <code>PropertyUtils} will be logged and re-thrown.
    127     * </p>
    128     */
    129    private final boolean ignoreNull;
    130
    131    /**
    132     * Constructor which takes the name of the property, its expected value to be used in evaluation, and assumes {@code ignoreNull</code> to be <code>false}.
    133     *
    134     * @param propertyName  The name of the property that will be evaluated against the expected value.
    135     * @param propertyValue The value to use in object evaluation.
    136     * @throws IllegalArgumentException If the property name provided is null or empty.
    137     */
    138    public BeanPropertyValueEqualsPredicate(final String propertyName, final V propertyValue) {
    139        this(propertyName, propertyValue, false);
    140    }
    141
    142    /**
    143     * Constructor which takes the name of the property, its expected value to be used in evaluation, and a boolean which determines whether {@code null}
    144     * objects in the property path will generate an {@code IllegalArgumentException} or not.
    145     *
    146     * @param propertyName  The name of the property that will be evaluated against the expected value.
    147     * @param propertyValue The value to use in object evaluation.
    148     * @param ignoreNull    Determines whether {@code null} objects in the property path will generate an {@code IllegalArgumentException} or not.
    149     * @throws IllegalArgumentException If the property name provided is null or empty.
    150     */
    151    public BeanPropertyValueEqualsPredicate(final String propertyName, final V propertyValue, final boolean ignoreNull) {
    152        if (propertyName == null || propertyName.isEmpty()) {
    153            throw new IllegalArgumentException("propertyName cannot be null or empty");
    154        }
    155        this.propertyName = propertyName;
    156        this.propertyValue = propertyValue;
    157        this.ignoreNull = ignoreNull;
    158    }
    159
    160    /**
    161     * Utility method which evaluates whether the actual property value equals the expected property value.
    162     *
    163     * @param expected The expected value.
    164     * @param actual   The actual value.
    165     * @return True if they are equal; false otherwise.
    166     */
    167    protected boolean evaluateValue(final V expected, final Object actual) {
    168        return Objects.equals(expected, actual);
    169    }
    170
    171    /**
    172     * Returns the name of the property which will be evaluated when this {@code Predicate} is executed.
    173     *
    174     * @return The name of the property which will be evaluated when this {@code Predicate} is executed.
    175     */
    176    public String getPropertyName() {
    177        return propertyName;
    178    }
    179
    180    /**
    181     * Returns the value that the property specified by {@code propertyName} will be compared to when this {@code Predicate} executes.
    182     *
    183     * @return The value that the property specified by {@code propertyName} will be compared to when this {@code Predicate} executes.
    184     */
    185    public V getPropertyValue() {
    186        return propertyValue;
    187    }
    188
    189    /**
    190     * Returns the flag which determines whether {@code null} objects in the property path will generate an
    191     * {@code IllegalArgumentException</code> or not. If set to <code>true} then
    192     * if any objects in the property path evaluate to {@code null} then the
    193     * {@code IllegalArgumentException</code> throw by <code>PropertyUtils} will be logged but
    194     * not re-thrown and {@code false</code> will be returned.  If set to <code>false} then if
    195     * any objects in the property path evaluate to {@code null} then the
    196     * {@code IllegalArgumentException</code> throw by <code>PropertyUtils} will be logged and re-thrown.
    197     *
    198     * @return The flag which determines whether {@code null} objects in the property path will generate an {@code IllegalArgumentException} or not.
    199     */
    200    public boolean isIgnoreNull() {
    201        return ignoreNull;
    202    }
    203
    204    /**
    205     * Evaluates the object provided against the criteria specified when this {@code BeanPropertyValueEqualsPredicate} was constructed. Equality is based on
    206     * either reference or logical equality as defined by the property object's equals method. If any object in the property path leading up to the target
    207     * property is {@code null} then the outcome will be based on the value of the {@code ignoreNull} attribute. By default,
    208     * {@code ignoreNull</code> is <code>false} and would result in an
    209     * {@code IllegalArgumentException} if an object in the property path leading up to the
    210     * target property is {@code null}.
    211     *
    212     * &#64;param object The object to be evaluated.
    213     * &#64;return True if the object provided meets all the criteria for this {@code Predicate};
    214     * false otherwise.
    215     * @throws IllegalArgumentException If an IllegalAccessException, InvocationTargetException, or
    216     * NoSuchMethodException is thrown when trying to access the property specified on the object
    217     * provided. Or if an object in the property path provided is {@code null} and
    218     * {@code ignoreNull</code> is set to <code>false}.
    219     */
    220    @Override
    221    public boolean test(final T object) {
    222
    223        boolean evaluation = false;
    224
    225        try {
    226            evaluation = evaluateValue(propertyValue, PropertyUtils.getProperty(object, propertyName));
    227        } catch (final IllegalArgumentException e) {
    228            final String errorMsg = "Problem during evaluation. Null value encountered in property path...";
    229
    230            if (!ignoreNull) {
    231                throw new IllegalArgumentException(errorMsg, e);
    232            }
    233            log.warn(errorMsg, e);
    234        } catch (final IllegalAccessException e) {
    235            final String errorMsg = "Unable to access the property provided.";
    236            throw new IllegalArgumentException(errorMsg, e);
    237        } catch (final InvocationTargetException e) {
    238            final String errorMsg = "Exception occurred in property's getter";
    239            throw new IllegalArgumentException(errorMsg, e);
    240        } catch (final NoSuchMethodException e) {
    241            final String errorMsg = "Property not found.";
    242            throw new IllegalArgumentException(errorMsg, e);
    243        }
    244
    245        return evaluation;
    246    }
    247}