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    }