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 */
017package org.apache.commons.jxpath.util;
018
019import java.beans.IndexedPropertyDescriptor;
020import java.beans.PropertyDescriptor;
021import java.lang.reflect.Array;
022import java.lang.reflect.InvocationTargetException;
023import java.lang.reflect.Method;
024import java.lang.reflect.Modifier;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Map;
032
033import org.apache.commons.jxpath.Container;
034import org.apache.commons.jxpath.DynamicPropertyHandler;
035import 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 22:12:44 +0200 (Mo, 23 Jun 2008) $
042 */
043public 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}