    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     *     http://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    package org.apache.commons.jxpath.util;
    018
    019    import java.beans.IndexedPropertyDescriptor;
    020    import java.beans.PropertyDescriptor;
    021    import java.lang.reflect.Array;
    022    import java.lang.reflect.InvocationTargetException;
    023    import java.lang.reflect.Method;
    024    import java.lang.reflect.Modifier;
    025    import java.util.ArrayList;
    026    import java.util.Collection;
    027    import java.util.Collections;
    028    import java.util.HashMap;
    029    import java.util.Iterator;
    030    import java.util.List;
    031    import java.util.Map;
    032
    033    import org.apache.commons.jxpath.Container;
    034    import org.apache.commons.jxpath.DynamicPropertyHandler;
    035    import org.apache.commons.jxpath.JXPathException;
    036
    037    /**
    038     * Collection and property access utilities.
    039     *
    040     * @author Dmitri Plotnikov
    041     * @version $Revision: 670728 $ $Date: 2008-06-23 15:12:44 -0500 (Mon, 23 Jun 2008) $
    042     */
    043    public class ValueUtils {
    044        private static Map dynamicPropertyHandlerMap = new HashMap();
    045        private static final int UNKNOWN_LENGTH_MAX_COUNT = 16000;
    046
    047        /**
    048         * Returns true if the object is an array or a Collection.
    049         * @param value to test
    050         * @return boolean
    051         */
    052        public static boolean isCollection(Object value) {
    053            value = getValue(value);
    054            if (value == null) {
    055                return false;
    056            }
    057            if (value.getClass().isArray()) {
    058                return true;
    059            }
    060            if (value instanceof Collection) {
    061                return true;
    062            }
    063            return false;
    064        }
    065
    066        /**
    067         * Returns 1 if the type is a collection,
    068         * -1 if it is definitely not
    069         * and 0 if it may be a collection in some cases.
    070         * @param clazz to test
    071         * @return int
    072         */
    073        public static int getCollectionHint(Class clazz) {
    074            if (clazz.isArray()) {
    075                return 1;
    076            }
    077
    078            if (Collection.class.isAssignableFrom(clazz)) {
    079                return 1;
    080            }
    081
    082            if (clazz.isPrimitive()) {
    083                return -1;
    084            }
    085
    086            if (clazz.isInterface()) {
    087                return 0;
    088            }
    089
    090            if (Modifier.isFinal(clazz.getModifiers())) {
    091                return -1;
    092            }
    093
    094            return 0;
    095        }
    096
    097        /**
    098         * If there is a regular non-indexed read method for this property,
    099         * uses this method to obtain the collection and then returns its
    100         * length.
    101         * Otherwise, attempts to guess the length of the collection by
    102         * calling the indexed get method repeatedly.  The method is supposed
    103         * to throw an exception if the index is out of bounds.
    104         * @param object collection
    105         * @param pd IndexedPropertyDescriptor
    106         * @return int
    107         */
    108        public static int getIndexedPropertyLength(Object object,
    109                IndexedPropertyDescriptor pd) {
    110            if (pd.getReadMethod() != null) {
    111                return getLength(getValue(object, pd));
    112            }
    113
    114            Method readMethod = pd.getIndexedReadMethod();
    115            if (readMethod == null) {
    116                throw new JXPathException(
    117                    "No indexed read method for property " + pd.getName());
    118            }
    119
    120            for (int i = 0; i < UNKNOWN_LENGTH_MAX_COUNT; i++) {
    121                try {
    122                    readMethod.invoke(object, new Object[] { new Integer(i)});
    123                }
    124                catch (Throwable t) {
    125                    return i;
    126                }
    127            }
    128
    129            throw new JXPathException(
    130                "Cannot determine the length of the indexed property "
    131                    + pd.getName());
    132        }
    133
    134        /**
    135         * Returns the length of the supplied collection. If the supplied object
    136         * is not a collection, returns 1. If collection is null, returns 0.
    137         * @param collection to check
    138         * @return int
    139         */
    140        public static int getLength(Object collection) {
    141            if (collection == null) {
    142                return 0;
    143            }
    144            collection = getValue(collection);
    145            if (collection.getClass().isArray()) {
    146                return Array.getLength(collection);
    147            }
    148            if (collection instanceof Collection) {
    149                return ((Collection) collection).size();
    150            }
    151            return 1;
    152        }
    153
    154        /**
    155         * Returns an iterator for the supplied collection. If the argument
    156         * is null, returns an empty iterator. If the argument is not
    157         * a collection, returns an iterator that produces just that one object.
    158         * @param collection to iterate
    159         * @return Iterator
    160         */
    161        public static Iterator iterate(Object collection) {
    162            if (collection == null) {
    163                return Collections.EMPTY_LIST.iterator();
    164            }
    165            if (collection.getClass().isArray()) {
    166                int length = Array.getLength(collection);
    167                if (length == 0) {
    168                    return Collections.EMPTY_LIST.iterator();
    169                }
    170                ArrayList list = new ArrayList();
    171                for (int i = 0; i < length; i++) {
    172                    list.add(Array.get(collection, i));
    173                }
    174                return list.iterator();
    175            }
    176            if (collection instanceof Collection) {
    177                return ((Collection) collection).iterator();
    178            }
    179            return Collections.singletonList(collection).iterator();
    180        }
    181
    182        /**
    183         * Grows the collection if necessary to the specified size. Returns
    184         * the new, expanded collection.
    185         * @param collection to expand
    186         * @param size desired size
    187         * @return collection or array
    188         */
    189        public static Object expandCollection(Object collection, int size) {
    190            if (collection == null) {
    191                return null;
    192            }
    193            if (size < getLength(collection)) {
    194                throw new JXPathException("adjustment of " + collection
    195                        + " to size " + size + " is not an expansion");
    196            }
    197            if (collection.getClass().isArray()) {
    198                Object bigger =
    199                    Array.newInstance(
    200                        collection.getClass().getComponentType(),
    201                        size);
    202                System.arraycopy(
    203                    collection,
    204                    0,
    205                    bigger,
    206                    0,
    207                    Array.getLength(collection));
    208                return bigger;
    209            }
    210            if (collection instanceof Collection) {
    211                while (((Collection) collection).size() < size) {
    212                    ((Collection) collection).add(null);
    213                }
    214                return collection;
    215            }
    216            throw new JXPathException(
    217                "Cannot turn "
    218                    + collection.getClass().getName()
    219                    + " into a collection of size "
    220                    + size);
    221        }
    222
    223        /**
    224         * Remove the index'th element from the supplied collection.
    225         * @param collection to edit
    226         * @param index int
    227         * @return the resulting collection
    228         */
    229        public static Object remove(Object collection, int index) {
    230            collection = getValue(collection);
    231            if (collection == null) {
    232                return null;
    233            }
    234            if (index >= getLength(collection)) {
    235                throw new JXPathException("No such element at index " + index);
    236            }
    237            if (collection.getClass().isArray()) {
    238                int length = Array.getLength(collection);
    239                Object smaller =
    240                    Array.newInstance(
    241                        collection.getClass().getComponentType(),
    242                        length - 1);
    243                if (index > 0) {
    244                    System.arraycopy(collection, 0, smaller, 0, index);
    245                }
    246                if (index < length - 1) {
    247                    System.arraycopy(
    248                        collection,
    249                        index + 1,
    250                        smaller,
    251                        index,
    252                        length - index - 1);
    253                }
    254                return smaller;
    255            }
    256            if (collection instanceof List) {
    257                int size = ((List) collection).size();
    258                if (index < size) {
    259                    ((List) collection).remove(index);
    260                }
    261                return collection;
    262            }
    263            if (collection instanceof Collection) {
    264                Iterator it = ((Collection) collection).iterator();
    265                for (int i = 0; i < index; i++) {
    266                    if (!it.hasNext()) {
    267                        break;
    268                    }
    269                    it.next();
    270                }
    271                if (it.hasNext()) {
    272                    it.next();
    273                    it.remove();
    274                }
    275                return collection;
    276            }
    277            throw new JXPathException(
    278                "Cannot remove "
    279                    + collection.getClass().getName()
    280                    + "["
    281                    + index
    282                    + "]");
    283        }
    284
    285        /**
    286         * Returns the index'th element of the supplied collection.
    287         * @param collection to read
    288         * @param index int
    289         * @return collection[index]
    290         */
    291        public static Object getValue(Object collection, int index) {
    292            collection = getValue(collection);
    293            Object value = collection;
    294            if (collection != null) {
    295                if (collection.getClass().isArray()) {
    296                    if (index < 0 || index >= Array.getLength(collection)) {
    297                        return null;
    298                    }
    299                    value = Array.get(collection, index);
    300                }
    301                else if (collection instanceof List) {
    302                    if (index < 0 || index >= ((List) collection).size()) {
    303                        return null;
    304                    }
    305                    value = ((List) collection).get(index);
    306                }
    307                else if (collection instanceof Collection) {
    308                    int i = 0;
    309                    Iterator it = ((Collection) collection).iterator();
    310                    for (; i < index; i++) {
    311                        it.next();
    312                    }
    313                    if (it.hasNext()) {
    314                        value = it.next();
    315                    }
    316                    else {
    317                        value = null;
    318                    }
    319                }
    320            }
    321            return value;
    322        }
    323
    324        /**
    325         * Modifies the index'th element of the supplied collection.
    326         * Converts the value to the required type if necessary.
    327         * @param collection to edit
    328         * @param index to replace
    329         * @param value new value
    330         */
    331        public static void setValue(Object collection, int index, Object value) {
    332            collection = getValue(collection);
    333            if (collection != null) {
    334                if (collection.getClass().isArray()) {
    335                    Array.set(
    336                        collection,
    337                        index,
    338                        convert(value, collection.getClass().getComponentType()));
    339                }
    340                else if (collection instanceof List) {
    341                    ((List) collection).set(index, value);
    342                }
    343                else if (collection instanceof Collection) {
    344                    throw new UnsupportedOperationException(
    345                            "Cannot set value of an element of a "
    346                                    + collection.getClass().getName());
    347                }
    348            }
    349        }
    350
    351        /**
    352         * Returns the value of the bean's property represented by
    353         * the supplied property descriptor.
    354         * @param bean to read
    355         * @param propertyDescriptor indicating what to read
    356         * @return Object value
    357         */
    358        public static Object getValue(Object bean,
    359                PropertyDescriptor propertyDescriptor) {
    360            Object value;
    361            try {
    362                Method method =
    363                    getAccessibleMethod(propertyDescriptor.getReadMethod());
    364                if (method == null) {
    365                    throw new JXPathException("No read method");
    366                }
    367                value = method.invoke(bean, new Object[0]);
    368            }
    369            catch (Exception ex) {
    370                throw new JXPathException(
    371                    "Cannot access property: "
    372                        + (bean == null ? "null" : bean.getClass().getName())
    373                        + "."
    374                        + propertyDescriptor.getName(),
    375                    ex);
    376            }
    377            return value;
    378        }
    379
    380        /**
    381         * Modifies the value of the bean's property represented by
    382         * the supplied property descriptor.
    383         * @param bean to read
    384         * @param propertyDescriptor indicating what to read
    385         * @param value to set
    386         */
    387        public static void setValue(Object bean,
    388                PropertyDescriptor propertyDescriptor, Object value) {
    389            try {
    390                Method method =
    391                    getAccessibleMethod(propertyDescriptor.getWriteMethod());
    392                if (method == null) {
    393                    throw new JXPathException("No write method");
    394                }
    395                value = convert(value, propertyDescriptor.getPropertyType());
    396                method.invoke(bean, new Object[] { value });
    397            }
    398            catch (Exception ex) {
    399                throw new JXPathException(
    400                    "Cannot modify property: "
    401                        + (bean == null ? "null" : bean.getClass().getName())
    402                        + "."
    403                        + propertyDescriptor.getName(),
    404                    ex);
    405            }
    406        }
    407
    408        /**
    409         * Convert value to type.
    410         * @param value Object
    411         * @param type destination
    412         * @return conversion result
    413         */
    414        private static Object convert(Object value, Class type) {
    415            try {
    416                return TypeUtils.convert(value, type);
    417            }
    418            catch (Exception ex) {
    419                throw new JXPathException(
    420                    "Cannot convert value of class "
    421                        + (value == null ? "null" : value.getClass().getName())
    422                        + " to type "
    423                        + type,
    424                    ex);
    425            }
    426        }
    427
    428        /**
    429         * Returns the index'th element of the bean's property represented by
    430         * the supplied property descriptor.
    431         * @param bean to read
    432         * @param propertyDescriptor indicating what to read
    433         * @param index int
    434         * @return Object
    435         */
    436        public static Object getValue(Object bean,
    437                PropertyDescriptor propertyDescriptor, int index) {
    438            if (propertyDescriptor instanceof IndexedPropertyDescriptor) {
    439                try {
    440                    IndexedPropertyDescriptor ipd =
    441                        (IndexedPropertyDescriptor) propertyDescriptor;
    442                    Method method = ipd.getIndexedReadMethod();
    443                    if (method != null) {
    444                        return method.invoke(
    445                            bean,
    446                            new Object[] { new Integer(index)});
    447                    }
    448                }
    449                catch (InvocationTargetException ex) {
    450                    Throwable t = ex.getTargetException();
    451                    if (t instanceof IndexOutOfBoundsException) {
    452                        return null;
    453                    }
    454                    throw new JXPathException(
    455                        "Cannot access property: " + propertyDescriptor.getName(),
    456                        t);
    457                }
    458                catch (Throwable ex) {
    459                    throw new JXPathException(
    460                        "Cannot access property: " + propertyDescriptor.getName(),
    461                        ex);
    462                }
    463            }
    464
    465            // We will fall through if there is no indexed read
    466
    467            return getValue(getValue(bean, propertyDescriptor), index);
    468        }
    469
    470        /**
    471         * Modifies the index'th element of the bean's property represented by
    472         * the supplied property descriptor. Converts the value to the required
    473         * type if necessary.
    474         * @param bean to edit
    475         * @param propertyDescriptor indicating what to set
    476         * @param index int
    477         * @param value to set
    478         */
    479        public static void setValue(Object bean,
    480                PropertyDescriptor propertyDescriptor, int index, Object value) {
    481            if (propertyDescriptor instanceof IndexedPropertyDescriptor) {
    482                try {
    483                    IndexedPropertyDescriptor ipd =
    484                        (IndexedPropertyDescriptor) propertyDescriptor;
    485                    Method method = ipd.getIndexedWriteMethod();
    486                    if (method != null) {
    487                        method.invoke(
    488                            bean,
    489                            new Object[] {
    490                                new Integer(index),
    491                                convert(value, ipd.getIndexedPropertyType())});
    492                        return;
    493                    }
    494                }
    495                catch (Exception ex) {
    496                    throw new RuntimeException(
    497                        "Cannot access property: "
    498                            + propertyDescriptor.getName()
    499                            + ", "
    500                            + ex.getMessage());
    501                }
    502            }
    503            // We will fall through if there is no indexed read
    504            Object collection = getValue(bean, propertyDescriptor);
    505            if (isCollection(collection)) {
    506                setValue(collection, index, value);
    507            }
    508            else if (index == 0) {
    509                setValue(bean, propertyDescriptor, value);
    510            }
    511            else {
    512                throw new RuntimeException(
    513                    "Not a collection: " + propertyDescriptor.getName());
    514            }
    515        }
    516
    517        /**
    518         * If the parameter is a container, opens the container and
    519         * return the contents.  The method is recursive.
    520         * @param object to read
    521         * @return Object
    522         */
    523        public static Object getValue(Object object) {
    524            while (object instanceof Container) {
    525                object = ((Container) object).getValue();
    526            }
    527            return object;
    528        }
    529
    530        /**
    531         * Returns a shared instance of the dynamic property handler class
    532         * returned by <code>getDynamicPropertyHandlerClass()</code>.
    533         * @param clazz to handle
    534         * @return DynamicPropertyHandler
    535         */
    536        public static DynamicPropertyHandler getDynamicPropertyHandler(Class clazz) {
    537            DynamicPropertyHandler handler =
    538                (DynamicPropertyHandler) dynamicPropertyHandlerMap.get(clazz);
    539            if (handler == null) {
    540                try {
    541                    handler = (DynamicPropertyHandler) clazz.newInstance();
    542                }
    543                catch (Exception ex) {
    544                    throw new JXPathException(
    545                        "Cannot allocate dynamic property handler of class "
    546                            + clazz.getName(),
    547                        ex);
    548                }
    549                dynamicPropertyHandlerMap.put(clazz, handler);
    550            }
    551            return handler;
    552        }
    553
    554        // -------------------------------------------------------- Private Methods
    555        //
    556        //  The rest of the code in this file was copied FROM
    557        //  org.apache.commons.beanutils.PropertyUtil. We don't want to introduce
    558        //  a dependency on BeanUtils yet - DP.
    559        //
    560
    561        /**
    562         * Return an accessible method (that is, one that can be invoked via
    563         * reflection) that implements the specified Method.  If no such method
    564         * can be found, return <code>null</code>.
    565         *
    566         * @param method The method that we wish to call
    567         * @return Method
    568         */
    569        public static Method getAccessibleMethod(Method method) {
    570
    571            // Make sure we have a method to check
    572            if (method == null) {
    573                return (null);
    574            }
    575
    576            // If the requested method is not public we cannot call it
    577            if (!Modifier.isPublic(method.getModifiers())) {
    578                return (null);
    579            }
    580
    581            // If the declaring class is public, we are done
    582            Class clazz = method.getDeclaringClass();
    583            if (Modifier.isPublic(clazz.getModifiers())) {
    584                return (method);
    585            }
    586
    587            String name = method.getName();
    588            Class[] parameterTypes = method.getParameterTypes();
    589            while (clazz != null) {
    590                // Check the implemented interfaces and subinterfaces
    591                Method aMethod = getAccessibleMethodFromInterfaceNest(clazz,
    592                        name, parameterTypes);
    593                if (aMethod != null) {
    594                    return aMethod;
    595                }
    596
    597                clazz = clazz.getSuperclass();
    598                if (clazz != null && Modifier.isPublic(clazz.getModifiers())) {
    599                    try {
    600                        return clazz.getDeclaredMethod(name, parameterTypes);
    601                    }
    602                    catch (NoSuchMethodException e) { //NOPMD
    603                        //ignore
    604                    }
    605                }
    606            }
    607            return null;
    608        }
    609
    610        /**
    611         * Return an accessible method (that is, one that can be invoked via
    612         * reflection) that implements the specified method, by scanning through
    613         * all implemented interfaces and subinterfaces.  If no such Method
    614         * can be found, return <code>null</code>.
    615         *
    616         * @param clazz Parent class for the interfaces to be checked
    617         * @param methodName Method name of the method we wish to call
    618         * @param parameterTypes The parameter type signatures
    619         * @return Method
    620         */
    621        private static Method getAccessibleMethodFromInterfaceNest(Class clazz,
    622                String methodName, Class[] parameterTypes) {
    623
    624            Method method = null;
    625
    626            // Check the implemented interfaces of the parent class
    627            Class[] interfaces = clazz.getInterfaces();
    628            for (int i = 0; i < interfaces.length; i++) {
    629
    630                // Is this interface public?
    631                if (!Modifier.isPublic(interfaces[i].getModifiers())) {
    632                    continue;
    633                }
    634
    635                // Does the method exist on this interface?
    636                try {
    637                    method =
    638                        interfaces[i].getDeclaredMethod(methodName, parameterTypes);
    639                }
    640                catch (NoSuchMethodException e) { //NOPMD
    641                    //ignore
    642                }
    643                if (method != null) {
    644                    break;
    645                }
    646
    647                // Recursively check our parent interfaces
    648                method =
    649                    getAccessibleMethodFromInterfaceNest(
    650                        interfaces[i],
    651                        methodName,
    652                        parameterTypes);
    653                if (method != null) {
    654                    break;
    655                }
    656            }
    657
    658            // Return whatever we have found
    659            return (method);
    660        }
    661    }