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}