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