org.apache.commons.beanutils.ExtendedBeanUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.commons.beanutils.ExtendedBeanUtils.java

Source

/*
 * Copyright 2013 Lyor Goldstein
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.beanutils;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.commons.collections15.AbstractExtendedPredicate;
import org.apache.commons.collections15.AbstractExtendedTransformer;
import org.apache.commons.collections15.ExtendedCollectionUtils;
import org.apache.commons.collections15.ExtendedMapUtils;
import org.apache.commons.collections15.ExtendedPredicate;
import org.apache.commons.collections15.ExtendedTransformer;
import org.apache.commons.collections15.Factory;
import org.apache.commons.collections15.Predicate;
import org.apache.commons.collections15.Transformer;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.ExtendedArrayUtils;
import org.apache.commons.lang3.ExtendedCharSequenceUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.ExtendedMethodUtils;
import org.apache.commons.lang3.tuple.Pair;

/**
 * @author Lyor G.
 */
public class ExtendedBeanUtils {
    // copied from Instrospector where they are package scope
    public static final String GET_PREFIX = "get";
    public static final String SET_PREFIX = "set";
    public static final String IS_PREFIX = "is";

    protected ExtendedBeanUtils() {
        super();
    }

    /**
     * A {@link Transformer} that retrieves the getter {@link Method}
     * from a {@link Pair} obtained via one of the <code>describeBean</code>
     * methods
     * @see #describeBean(Class)
     */
    public static final Transformer<Pair<Method, ?>, Method> GETTER_VALUE = new Transformer<Pair<Method, ?>, Method>() {
        @Override
        public Method transform(Pair<Method, ?> p) {
            if (p == null) {
                return null;
            } else {
                return p.getLeft();
            }
        }
    };

    /**
     * A {@link Transformer} that retrieves the getter {@link Method}
     * from a {@link Pair} obtained via one of the <code>describeBean</code>
     * methods
     * @see #describeBean(Class)
     */
    public static final Transformer<Pair<?, Method>, Method> SETTER_VALUE = new Transformer<Pair<?, Method>, Method>() {
        @Override
        public Method transform(Pair<?, Method> p) {
            if (p == null) {
                return null;
            } else {
                return p.getRight();
            }
        }
    };

    /**
     * @param beanClass The bean's class
     * @param attrsMap The attribute getters {@link Map} - key=the attribute
     * name, value=a {@link Pair} whose left-hand is the getter {@link Method}
     * to be used for the attribute.
     * @return A {@link Transformer} that returns a {@link SortedMap} of the
     * bean properties where key=the property name (case <U>insensitive</U>)
     * value=the property value. <B>Note:</B> <code>null</code> property
     * values are not mapped
     * @throws IllegalArgumentException if no bean class or attributes provided
     * @throws NoSuchElementException if no getter found for an attribute
     * @throws IllegalStateException if same attribute re-mapped
     */
    public static final <E> Transformer<E, SortedMap<String, Object>> beanToPropertiesTransformer(
            Class<E> beanClass, final Map<String, ? extends Pair<Method, ?>> attrsMap) {
        if (beanClass == null) {
            throw new IllegalArgumentException("No bean class provided");
        }
        if (ExtendedMapUtils.isEmpty(attrsMap)) {
            throw new IllegalArgumentException("No attributes");
        }

        return new Transformer<E, SortedMap<String, Object>>() {
            @Override
            public SortedMap<String, Object> transform(E input) {
                SortedMap<String, Object> valuesMap = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
                for (Map.Entry<String, ? extends Pair<Method, ?>> pe : attrsMap.entrySet()) {
                    String name = pe.getKey();
                    Pair<Method, ?> pair = pe.getValue();
                    Method getter = GETTER_VALUE.transform(pair);
                    if (getter == null) {
                        throw new NoSuchElementException("No getter for property=" + name);
                    }

                    Object value = ExtendedMethodUtils.invoke(getter, input);
                    if (value == null) {
                        if (valuesMap.containsKey(name)) {
                            throw new IllegalStateException("Multiple (null) values for property=" + name);
                        }
                    } else {
                        Object prev = valuesMap.put(name, value);
                        if (prev != null) {
                            throw new IllegalStateException("Multiple values for property=" + name);
                        }
                    }
                }

                return valuesMap;
            }
        };
    }

    /**
     * 
     * @param factory A {@link Factory} to be used to retrieve a bean instance to be manipulated
     * @param attrsMap The attribute setters {@link Map} - key=the attribute
     * name, value=a {@link Pair} whose right-hand is the setter {@link Method}
     * to be used for the attribute.
     * @return A {@link Transformer} that retrieves a new instance and then
     * updates its data using the provided setters from a {@link Map} of values
     * @see #propertiesToBeanTransformer(Object, Map)
     */
    public static final <E> Transformer<Map<String, ?>, E> propertiesToBeanTransformer(
            final Factory<? extends E> factory, final Map<String, ? extends Pair<?, Method>> attrsMap) {
        if (factory == null) {
            throw new IllegalArgumentException("No factory provided");
        }

        if (ExtendedMapUtils.isEmpty(attrsMap)) {
            throw new IllegalArgumentException("No attributes");
        }

        return new Transformer<Map<String, ?>, E>() {
            @Override
            public E transform(Map<String, ?> valuesMap) {
                Transformer<Map<String, ?>, E> delegate = propertiesToBeanTransformer((E) factory.create(),
                        attrsMap);
                return delegate.transform(valuesMap);
            }
        };
    }

    /**
     * @param instance The instance be manipulated
     * @param attrsMap The attribute setters {@link Map} - key=the attribute
     * name, value=a {@link Pair} whose right-hand is the setter {@link Method}
     * to be used for the attribute.
     * @return A {@link Transformer} that updates the instance data using
     * the provided setters from a {@link Map} of values whose keys are assumed
     * to match the attributes' names
     * @throws IllegalArgumentException if no instance or attributes provided
     */
    public static final <E> Transformer<Map<String, ?>, E> propertiesToBeanTransformer(final E instance,
            final Map<String, ? extends Pair<?, Method>> attrsMap) {
        if (instance == null) {
            throw new IllegalArgumentException("No instance provided");
        }

        if (ExtendedMapUtils.isEmpty(attrsMap)) {
            throw new IllegalArgumentException("No attributes");
        }

        return new Transformer<Map<String, ?>, E>() {
            @Override
            public E transform(Map<String, ?> valuesMap) {
                if (ExtendedMapUtils.isEmpty(valuesMap)) {
                    return instance;
                }

                for (Map.Entry<String, ? extends Pair<?, Method>> ae : attrsMap.entrySet()) {
                    String name = ae.getKey();
                    Pair<?, Method> pair = ae.getValue();
                    Method setter = SETTER_VALUE.transform(pair);
                    if (setter == null) {
                        continue; // ignore non settable values
                    }

                    Object value = valuesMap.get(name);
                    ExtendedMethodUtils.invoke(setter, instance, value);
                }

                return instance;
            }
        };
    }

    /**
     * A {@link Predicate} that returns <code>true</code> if the getter
     * {@link Method} in the right-hand of a {@link Pair} is non-<code>null</code>
     * <B>Note:</B> the predicate does not distinguish between a <code>null</code>
     * {@link Pair} and one that has no getter method
     */
    public static final Predicate<Pair<Method, Method>> READABLE_PAIR = new Predicate<Pair<Method, Method>>() {
        @Override
        public boolean evaluate(Pair<Method, Method> pair) {
            if (GETTER_VALUE.transform(pair) == null) {
                return false;
            } else {
                return true;
            }
        }
    };

    // does the same as filterNonReadableAttributes only returns the modified map instead of the removed entries
    public static final <M extends Map<String, Pair<Method, Method>>> M removeNonReadableAttributes(M attrsMap) {
        filterNonReadableAttributes(attrsMap);
        return attrsMap;
    }

    /**
     * Filters all the accessors that do not have a getter
     * @param attrsMap A {@link Map} of attributes accessors where key=attribute
     * name, value=a {@link Pair} of {@link Method}s where the left-hand is the
     * getter - ignored if <code>null</code>/empty
     * @return A {@link List} of all the {@link java.util.Map.Entry}-ies that were removed
     * where key=attribute name (case <U>insensitive</U>), value=a {@link Pair}
     * of {@link Method}s where the left-hand is a <code>null</code> getter
     * @see #READABLE_PAIR
     */
    public static final List<Map.Entry<String, Pair<Method, Method>>> filterNonReadableAttributes(
            Map<String, Pair<Method, Method>> attrsMap) {
        return ExtendedMapUtils.filterRejectedValues(attrsMap, READABLE_PAIR);
    }

    /**
     * A {@link Predicate} that returns <code>true</code> if the setter
     * {@link Method} in the right-hand of a {@link Pair} is non-<code>null</code>
     * <B>Note:</B> the predicate does not distinguish between a <code>null</code>
     * {@link Pair} and one that has no setter method
     */
    public static final Predicate<Pair<Method, Method>> WRITEABLE_PAIR = new Predicate<Pair<Method, Method>>() {
        @Override
        public boolean evaluate(Pair<Method, Method> pair) {
            if (SETTER_VALUE.transform(pair) == null) {
                return false;
            } else {
                return true;
            }
        }
    };

    // does the same as filterNonModifiableAttributes only returns the modified map instead of the removed entries
    public static final <M extends Map<String, Pair<Method, Method>>> M removeNonModifiableAttributes(M attrsMap) {
        filterNonModifiableAttributes(attrsMap);
        return attrsMap;
    }

    /**
     * Filters all the accessors that do not have a setter
     * @param attrsMap A {@link Map} of attributes accessors where key=attribute
     * name, value=a {@link Pair} of {@link Method}s where the right-hand is the
     * setter - ignored if <code>null</code>/empty
     * @return A {@link List} of all the {@link java.util.Map.Entry}-ies that were removed
     * where key=attribute name (case <U>insensitive</U>), value=a {@link Pair}
     * of {@link Method}s where the right-hand is a <code>null</code> setter
     * @see #WRITEABLE_PAIR
     */
    public static final List<Map.Entry<String, Pair<Method, Method>>> filterNonModifiableAttributes(
            Map<String, Pair<Method, Method>> attrsMap) {
        return ExtendedMapUtils.filterRejectedValues(attrsMap, WRITEABLE_PAIR);
    }

    /**
     * @param beanClass The {@link Class} to be described (ignored if <code>null</code>)
     * @return A {@link SortedMap} of all the properties where key=the property name
     * (case <U>insensitive</U>, value={@link Pair} of {@link Method}-s where
     * the left one (if non-<code>null</code>) represents the getter and the
     * right one (if non-<code>null</code> represents the setter
     * @throws IllegalStateException if same property (with different case)
     * encountered or failed to extract bean information
     * @see #describeBean(BeanInfo)
     * @see Introspector#getBeanInfo(Class)
     */
    public static final SortedMap<String, Pair<Method, Method>> describeBean(Class<?> beanClass) {
        return describeBean(beanClass, true, false);
    }

    /**
     * @param beanClass The {@link Class} to be described (ignored if <code>null</code>)
     * @param publicOnly If <code>true</code> then consider only <code>public</code>
     * {@link Method}-s otherwise consider all methods
     * @param makeAccessible If not only <code>public</code> methods considered and
     * a non-accessible method found whether to make it accessible
     * @return A {@link SortedMap} of all the properties where key=the property name
     * (case <U>insensitive</U>, value={@link Pair} of {@link Method}-s where
     * the left one (if non-<code>null</code>) represents the getter and the
     * right one (if non-<code>null</code> represents the setter
     * @throws IllegalStateException if same property (with different case)
     * encountered or failed to extract bean information
     */
    public static final SortedMap<String, Pair<Method, Method>> describeBean(Class<?> beanClass, boolean publicOnly,
            boolean makeAccessible) {
        if (beanClass == null) {
            return ExtendedMapUtils.emptySortedMap();
        }

        /*
         * NOTE: for interfaces, the "superclass" is always null, so if this
         * is an interface or an abstract class we need to "climb" the hierarchy
         */
        SortedMap<String, Pair<Method, Method>> map;

        /*
         * For a Proxy we cannot use introspection as it would give us the
         * proxy methods and we want the interfaces it implements
         */
        if (Proxy.isProxyClass(beanClass)) {
            map = ExtendedMapUtils.emptySortedMap();
        } else {
            map = describeBeanProperties(beanClass, publicOnly, makeAccessible);
            if ((!beanClass.isInterface()) && (!Modifier.isAbstract(beanClass.getModifiers()))) {
                return map;
            }
        }

        List<Class<?>> ifcs = ClassUtils.getAllInterfaces(beanClass);
        if (ExtendedCollectionUtils.isEmpty(ifcs)) {
            return map;
        }

        for (Class<?> ifc : ifcs) {
            SortedMap<String, Pair<Method, Method>> ifcMap = describeBeanProperties(ifc, true, false);
            if (ExtendedMapUtils.isEmpty(ifcMap)) {
                continue;
            }

            if (ExtendedMapUtils.isEmpty(map)) {
                map = ifcMap;
                continue;
            }

            for (Map.Entry<String, Pair<Method, Method>> pe : ifcMap.entrySet()) {
                String name = pe.getKey();
                Pair<Method, Method> newPair = pe.getValue(), oldPair = map.get(name);
                if (oldPair == null) {
                    map.put(name, newPair);
                } else {
                    map.put(name, mergeAccessors(oldPair, newPair));
                }
            }
        }

        return map;
    }

    private static SortedMap<String, Pair<Method, Method>> describeBeanProperties(Class<?> beanClass,
            boolean publicOnly, boolean makeAccessible) {
        if (publicOnly) {
            try {
                return describeBean(Introspector.getBeanInfo(beanClass));
            } catch (IntrospectionException e) {
                throw new IllegalStateException(
                        "describeBeanProperties(" + beanClass.getSimpleName() + ")" + " failed ("
                                + e.getClass().getSimpleName() + ")" + " to retrieve bean info: " + e.getMessage());
            }
        }

        SortedMap<String, Pair<Method, Method>> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (Class<?> clazz = beanClass; clazz != null; clazz = clazz.getSuperclass()) {
            Method[] methods = clazz.getDeclaredMethods();
            for (Method m : methods) {
                String name = toAttributeName(m);
                if (StringUtils.isEmpty(name)) {
                    continue;
                }

                if (makeAccessible && (!m.isAccessible())) {
                    m.setAccessible(true);
                }

                Pair<Method, Method> newPair = isGetter(m) ? Pair.<Method, Method>of(m, null)
                        : Pair.<Method, Method>of(null, m);
                Pair<Method, Method> oldPair = map.get(name),
                        mergedPair = oldPair == null ? newPair : mergeAccessors(oldPair, newPair);

                Method gm = mergedPair.getLeft(), sm = mergedPair.getRight();
                if ((gm != null) && (sm != null) && (!isMatchingAccessorPair(gm, sm))) {
                    throw new IllegalStateException("describeBeanProperties(" + beanClass.getSimpleName() + ")["
                            + name + "] mismatched accessor pair");
                }

                map.put(name, mergedPair);
            }
        }

        return map;
    }

    /**
     * Checks that a getter/setter pair refer to the same attribute type
     * @param getter The getter {@link Method}
     * @param setter The setter {@link Method}
     * @return <code>true</code> <U>all</U> of the following holds:</BR>
     * <UL>
     *      <LI>Neither accessor is <code>null</code></LI>
     *      <LI>The getter is indeed a getter and the setter is a setter</LI>
     *      <LI>
     *      The setter's argument is assignable from the getter's return
     *      type (<B>Note:</B> we don't check equality in order to enable
     *      <U>co-variant return types</U>).
     *      </LI>
     * </UL>
     * @see #isGetter(Method)
     * @see #isSetter(Method)
     */
    public static final boolean isMatchingAccessorPair(Method getter, Method setter) {
        if ((getter == null) || (setter == null)) {
            return false;
        }

        if ((!isGetter(getter)) || (!isSetter(setter))) {
            return false;
        }

        Class<?> gType = getter.getReturnType();
        Class<?>[] params = setter.getParameterTypes();
        Class<?> sType = params[0];
        // we check if assignable to allow for co-variant return
        if (sType.isAssignableFrom(gType)) {
            return true;
        } else {
            return false;
        }
    }

    private static Pair<Method, Method> mergeAccessors(Pair<Method, Method> oldPair, Pair<Method, Method> newPair) {
        Method oldGetter = oldPair.getLeft(), newGetter = newPair.getLeft();
        Method oldSetter = oldPair.getRight(), newSetter = newPair.getRight();
        Method getter = mergeAccessors(oldGetter, newGetter);
        Method setter = mergeAccessors(oldSetter, newSetter);
        if ((getter == oldGetter) && (setter == oldSetter)) {
            return oldPair;
        } else if ((getter == newGetter) && (setter == newSetter)) {
            return newPair;
        } else {
            return Pair.of(getter, setter);
        }
    }

    private static Method mergeAccessors(Method oldMethod, Method newMethod) {
        if (oldMethod == null) {
            return newMethod;
        } else if (newMethod == null) {
            return oldMethod;
        } else if (oldMethod == newMethod) {
            return oldMethod;
        }

        // prefer interface methods over non-interface ones
        Class<?> oldContainer = oldMethod.getDeclaringClass(), newContainer = newMethod.getDeclaringClass();
        if (oldContainer.isInterface()) {
            return oldMethod;
        } else if (newContainer.isInterface()) {
            return newMethod;
        }

        // prefer a super-class over its derived one
        if (oldContainer.isAssignableFrom(newContainer)) {
            return oldMethod;
        } else if (newContainer.isAssignableFrom(oldContainer)) {
            return newMethod;
        }

        // prefer abstract classes over concrete ones
        if (Modifier.isAbstract(oldContainer.getModifiers())) {
            return oldMethod;
        } else if (Modifier.isAbstract(newContainer.getModifiers())) {
            return newMethod;
        }

        // if all else fails prefer the old method
        return oldMethod;
    }

    /**
     * Calculates the name of the counterpart accessor method name for getter or setter
     * 
     * @param m the getter/setter {@link Method}
     * @return the counterpart method name or <code>null</code> if m is not a setter, 
     *         getter or <code>null</code>
     * @see #isGetter(Method)
     * @see #isSetter(Method)
     */
    public static final String getCounterpartAccessorName(Method m) {
        String capitalizedAttrName = ExtendedCharSequenceUtils.capitalize(toAttributeName(m));

        if (StringUtils.isEmpty(capitalizedAttrName)) {
            return null;
        }

        if (isGetter(m)) {
            return SET_PREFIX + capitalizedAttrName;
        } else {
            Class<?> attributeType = getSetterAttributeType(m);

            if (Boolean.TYPE == attributeType) {
                return IS_PREFIX + capitalizedAttrName;
            } else {
                return GET_PREFIX + capitalizedAttrName;
            }
        }
    }

    /**
     * Calculates the attribute type of setter or a getter
     * 
     * @param m the getter/setter {@link Method}
     * @return the {@link Class} of the attribute or <code>null</code> if m is not a setter, 
     *         getter or <code>null</code>
     * @see #isGetter(Method)
     * @see #isSetter(Method)
     */
    public static final Class<?> getAttrributeType(Method m) {
        if (isGetter(m)) {
            return getGetterAttributeType(m);
        } else if (isSetter(m)) {
            return getSetterAttributeType(m);
        } else {
            return null;
        }
    }

    private static Class<?> getSetterAttributeType(Method m) {
        return m.getParameterTypes()[0];
    }

    private static Class<?> getGetterAttributeType(Method m) {
        return m.getReturnType();
    }

    /**
     * Returns the pure attribute name derived from either a getter
     * or setter method
     * @param m The {@link Method} to be evaluated
     * @return The pure attribute name - <code>null</code> if method
     * is <code>null</code> or not a getter/setter
     * @see #isGetter(Method)
     * @see #isSetter(Method)
     */
    public static final String toAttributeName(Method m) {
        if (isGetter(m)) {
            final String name = m.getName();
            final String prefix = ExtendedCharSequenceUtils.isProperPrefix(name, GET_PREFIX, true) ? GET_PREFIX
                    : IS_PREFIX;
            return ExtendedCharSequenceUtils.uncapitalize(name.substring(prefix.length()));
        } else if (isSetter(m)) {
            final String name = m.getName();
            return ExtendedCharSequenceUtils.uncapitalize(name.substring(SET_PREFIX.length()));
        } else {
            return null;
        }
    }

    /**
     * An {@link ExtendedPredicate} that returns <code>true</code> if it evaluates
     * a getter {@link Method}
     * @see #isGetter(Method)
     */
    public static final ExtendedPredicate<Method> IS_GETTER = new AbstractExtendedPredicate<Method>(Method.class) {
        @Override
        public boolean evaluate(Method m) {
            return isGetter(m);
        }
    };

    /**
     * @param m The {@link Method} to be checked
     * @return <code>true</code> if <U>all</U> of these conditions are satisfied:</BR>
     * <UL>
     *      <LI>Not <code>null</code></LI>
     *      <LI>Not static</LI>
     *      <LI>Starts with either {@link #GET_PREFIX} or {@link #IS_PREFIX}</LI>
     *      <LI>Has some extra characters after the prefix</LI>
     *      <LI>Has no arguments</LI>
     *      <LI>
     *      Return type is not <code>void</code> (<B>Note:</B> {@link Void} wrapper
     *      is considered a valid return type)
     *      </LI>
     *      <LI>
     *      If it has {@link #IS_PREFIX} then it must return a <code>boolean</code>
     *      (<B>Note:</B> {@link Boolean} wrapper is <U>not</U> considered a valid
     *      return type)
     *      </LI>
     * </UL>
     * @see #isGetterName(CharSequence)
     */
    public static final boolean isGetter(Method m) {
        if (m == null) {
            return false;
        }

        if (Modifier.isStatic(m.getModifiers())) {
            return false;
        }

        String name = m.getName();
        if (!isGetterName(name)) {
            return false;
        }

        Class<?>[] params = m.getParameterTypes();
        if (ExtendedArrayUtils.length(params) != 0) {
            return false;
        }

        Class<?> returnType = m.getReturnType();
        if (Void.TYPE.isAssignableFrom(returnType)) {
            return false;
        }

        if (isBooleanGetterName(name) && (!Boolean.TYPE.isAssignableFrom(returnType))) {
            return false;
        }

        return true;
    }

    /**
     * @param name A candidate method name
     * @return <code>true</code> if the candidate method name is a valid
     * getter name - i.e., not {@code null}/empty and starts with either
     * {@link #GET_PREFIX} or {@link #IS_PREFIX}
     * @see #isBooleanGetterName(CharSequence)
     * @see #isValueGetterName(CharSequence)
     */
    public static final boolean isGetterName(CharSequence name) {
        if (StringUtils.isEmpty(name)) {
            return false;
        } else if (isValueGetterName(name) || isBooleanGetterName(name)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param name A candidate method name
     * @return <code>true</code> if the candidate method name is a valid
     * <U>non-{@code boolean}</U> getter name -  i.e., not {@code null}/empty
     * and starts with {@link #GET_PREFIX}
     */
    public static final boolean isValueGetterName(CharSequence name) {
        if (StringUtils.isEmpty(name)) {
            return false;
        } else if (ExtendedCharSequenceUtils.isProperPrefix(name, GET_PREFIX, true)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param name A candidate method name
     * @return <code>true</code> if the candidate method name is a valid
     * {@code boolean} getter name -  i.e., not {@code null}/empty and starts
     * with {@link #IS_PREFIX}
     */
    public static final boolean isBooleanGetterName(CharSequence name) {
        if (StringUtils.isEmpty(name)) {
            return false;
        } else if (ExtendedCharSequenceUtils.isProperPrefix(name, IS_PREFIX, true)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * An {@link ExtendedPredicate} that returns <code>true</code> if it evaluates
     * a setter {@link Method}
     * @see #isSetter(Method)
     */
    public static final ExtendedPredicate<Method> IS_SETTER = new AbstractExtendedPredicate<Method>(Method.class) {
        @Override
        public boolean evaluate(Method m) {
            return isSetter(m);
        }
    };

    /**
     * @param m The {@link Method} to be checked
     * @return <code>true</code> if <U>all</U> of these conditions are satisfied:</BR>
     * <UL>
     *      <LI>Not <code>null</code></LI>
     *      <LI>Not static</LI>
     *      <LI>Starts with {@link #SET_PREFIX}
     *      <LI>Has some extra characters after the prefix</LI>
     *      <LI>Has <U>exactly <B>one</B></U> argument</LI>
     *      <LI>
     *      The argument is not <code>void</code>  (<B>Note:</B> {@link Void} wrapper
     *      is considered a valid argument type)
     *      </LI>
     *      <LI>
     *      Return type is <code>void</code> (<B>Note:</B> {@link Void} wrapper
     *      is <U>not</U> considered a valid return type)
     *      </LI>
     * </UL>
     */
    public static final boolean isSetter(Method m) {
        if (m == null) {
            return false;
        }

        if (Modifier.isStatic(m.getModifiers())) {
            return false;
        }

        String name = m.getName();
        if (!isSetterName(name)) {
            return false;
        }

        Class<?>[] params = m.getParameterTypes();
        if (ExtendedArrayUtils.length(params) != 1) {
            return false;
        }

        Class<?> propType = params[0];
        if (Void.TYPE.isAssignableFrom(propType)) {
            return false;
        }

        Class<?> returnType = m.getReturnType();
        if (!Void.TYPE.isAssignableFrom(returnType)) {
            return false;
        }

        return true;
    }

    /**
     * @param name Candidate method name
     * @return {@code true} if the method name is not {@code null}/empty
     * and starts with {@link #SET_PREFIX}
     */
    public static final boolean isSetterName(CharSequence name) {
        if (StringUtils.isEmpty(name)) {
            return false;
        } else if (ExtendedCharSequenceUtils.isProperPrefix(name, SET_PREFIX, true)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param beanInfo A {@link BeanInfo} description (ignored if <code>null</code>)
     * @return A {@link SortedMap} of all the properties where key=the property name
     * (case <U>insensitive</U>, value={@link Pair} of {@link Method}-s where
     * the left one (if non-<code>null</code>) represents the getter and the
     * right one (if non-<code>null</code> represents the setter
     * @throws IllegalStateException if same property (with different case)
     * encountered
     * @see #describeBean(PropertyDescriptor...)
     */
    public static final SortedMap<String, Pair<Method, Method>> describeBean(BeanInfo beanInfo) {
        if (beanInfo == null) {
            return ExtendedMapUtils.emptySortedMap();
        } else {
            return describeBean(beanInfo.getPropertyDescriptors());
        }
    }

    /**
     * @param descriptors A group of {@link PropertyDescriptor}-s (ignored if
     * <code>null</code>/empty)
     * @return A {@link SortedMap} of all the properties where key=the property name
     * (case <U>insensitive</U>, value={@link Pair} of {@link Method}-s where
     * the left one (if non-<code>null</code>) represents the getter and the
     * right one (if non-<code>null</code> represents the setter
     * @throws IllegalStateException if same property (with different case)
     * encountered
     * @see #describeBean(Collection)
     */
    public static final SortedMap<String, Pair<Method, Method>> describeBean(PropertyDescriptor... descriptors) {
        return describeBean(ExtendedArrayUtils.asList(descriptors));
    }

    /**
     * An {@link ExtendedTransformer} that retrieves the {@link PropertyDescriptor#getReadMethod()}
     * value. <B>Note:</B> one cannot distinguish between a <code>null</code>
     * descriptor and one with no read {@link Method}
     */
    public static final ExtendedTransformer<PropertyDescriptor, Method> PROPERTY_READER = new AbstractExtendedTransformer<PropertyDescriptor, Method>(
            PropertyDescriptor.class, Method.class) {
        @Override
        public Method transform(PropertyDescriptor prop) {
            if (prop == null) {
                return null;
            } else {
                return prop.getReadMethod();
            }
        }
    };

    /**
     * An {@link ExtendedTransformer} that retrieves the {@link PropertyDescriptor#getWriteMethod()}
     * value. <B>Note:</B> one cannot distinguish between a <code>null</code>
     * descriptor and one with no write {@link Method}
     */
    public static final ExtendedTransformer<PropertyDescriptor, Method> PROPERTY_WRITER = new AbstractExtendedTransformer<PropertyDescriptor, Method>(
            PropertyDescriptor.class, Method.class) {
        @Override
        public Method transform(PropertyDescriptor prop) {
            if (prop == null) {
                return null;
            } else {
                return prop.getWriteMethod();
            }
        }
    };

    /**
     * A {@link Transformer} that extracts the read/write accessor {@link Method}s
     * from a {@link PropertyDescriptor} and returns them as a {@link Pair}
     * whose left-hand is the getter (if any) and right-hand the setter (if any).
     * If no descriptor or no accessors then returns <code>null</code>.
     */
    public static final Transformer<PropertyDescriptor, Pair<Method, Method>> PROPERTY_ACCESSORS = new Transformer<PropertyDescriptor, Pair<Method, Method>>() {
        @Override
        public Pair<Method, Method> transform(PropertyDescriptor prop) {
            if (prop == null) {
                return null;
            }
            Method readMethod = PROPERTY_READER.transform(prop);
            Method writeMethod = PROPERTY_WRITER.transform(prop);
            if ((readMethod == null) && (writeMethod == null)) {
                return null; // strange, but does not matter
            } else {
                return Pair.of(readMethod, writeMethod);
            }
        }
    };

    /**
     * @param descriptors A {@link Collection} of {@link PropertyDescriptor}-s
     * (ignored if <code>null</code>/empty)
     * @return A {@link SortedMap} of all the properties where key=the property name
     * (case <U>insensitive</U>, value={@link Pair} of {@link Method}-s where
     * the left one (if non-<code>null</code>) represents the getter and the
     * right one (if non-<code>null</code> represents the setter
     * @throws IllegalStateException if same property (with different case)
     * encountered
     */
    public static final SortedMap<String, Pair<Method, Method>> describeBean(
            Collection<? extends PropertyDescriptor> descriptors) {
        if (ExtendedCollectionUtils.isEmpty(descriptors)) {
            return ExtendedMapUtils.emptySortedMap();
        }

        SortedMap<String, Pair<Method, Method>> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        for (PropertyDescriptor prop : descriptors) {
            String name = prop.getName();
            Pair<Method, Method> pair = PROPERTY_ACCESSORS.transform(prop);
            if (pair == null) {
                continue; // strange, but does not matter
            }

            Object prev = result.put(name, pair);
            if (prev != null) {
                throw new IllegalStateException(
                        "Multiple descriptors for property=" + name + " (possible case sensitivity issue)");
            }
        }

        return result;
    }

}