View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jxpath.util;
18  
19  import java.beans.IndexedPropertyDescriptor;
20  import java.beans.PropertyDescriptor;
21  import java.lang.reflect.Array;
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.lang.reflect.Modifier;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  
33  import org.apache.commons.jxpath.Container;
34  import org.apache.commons.jxpath.DynamicPropertyHandler;
35  import org.apache.commons.jxpath.JXPathException;
36  
37  /**
38   * Collection and property access utilities.
39   *
40   * @author Dmitri Plotnikov
41   * @version $Revision: 670728 $ $Date: 2008-06-23 22:12:44 +0200 (Mo, 23 Jun 2008) $
42   */
43  public class ValueUtils {
44      private static Map dynamicPropertyHandlerMap = new HashMap();
45      private static final int UNKNOWN_LENGTH_MAX_COUNT = 16000;
46  
47      /**
48       * Returns true if the object is an array or a Collection.
49       * @param value to test
50       * @return boolean
51       */
52      public static boolean isCollection(Object value) {
53          value = getValue(value);
54          if (value == null) {
55              return false;
56          }
57          if (value.getClass().isArray()) {
58              return true;
59          }
60          if (value instanceof Collection) {
61              return true;
62          }
63          return false;
64      }
65  
66      /**
67       * Returns 1 if the type is a collection,
68       * -1 if it is definitely not
69       * and 0 if it may be a collection in some cases.
70       * @param clazz to test
71       * @return int
72       */
73      public static int getCollectionHint(Class clazz) {
74          if (clazz.isArray()) {
75              return 1;
76          }
77  
78          if (Collection.class.isAssignableFrom(clazz)) {
79              return 1;
80          }
81  
82          if (clazz.isPrimitive()) {
83              return -1;
84          }
85  
86          if (clazz.isInterface()) {
87              return 0;
88          }
89  
90          if (Modifier.isFinal(clazz.getModifiers())) {
91              return -1;
92          }
93  
94          return 0;
95      }
96  
97      /**
98       * If there is a regular non-indexed read method for this property,
99       * 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 }