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  
18  package org.apache.commons.jxpath.util;
19  
20  import java.beans.IndexedPropertyDescriptor;
21  import java.beans.PropertyDescriptor;
22  import java.lang.reflect.Array;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.lang.reflect.Modifier;
26  import java.util.ArrayList;
27  import java.util.Collection;
28  import java.util.Collections;
29  import java.util.HashMap;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.commons.jxpath.Container;
35  import org.apache.commons.jxpath.DynamicPropertyHandler;
36  import org.apache.commons.jxpath.JXPathException;
37  
38  /**
39   * Collection and property access utilities.
40   */
41  public class ValueUtils {
42  
43      private static Map<Class, DynamicPropertyHandler> dynamicPropertyHandlerMap = new HashMap<>();
44      private static final int UNKNOWN_LENGTH_MAX_COUNT = 16000;
45  
46      /**
47       * Convert value to type.
48       *
49       * @param value Object
50       * @param type  destination
51       * @return conversion result
52       */
53      private static Object convert(final Object value, final Class type) {
54          try {
55              return TypeUtils.convert(value, type);
56          } catch (final Exception ex) {
57              throw new JXPathException("Cannot convert value of class " + (value == null ? "null" : value.getClass().getName()) + " to type " + type, ex);
58          }
59      }
60  
61      /**
62       * Grows the collection if necessary to the specified size. Returns the new, expanded collection.
63       *
64       * @param collection to expand
65       * @param size       desired size
66       * @return collection or array
67       */
68      public static Object expandCollection(final Object collection, final int size) {
69          if (collection == null) {
70              return null;
71          }
72          if (size < getLength(collection)) {
73              throw new JXPathException("adjustment of " + collection + " to size " + size + " is not an expansion");
74          }
75          if (collection.getClass().isArray()) {
76              final Object bigger = Array.newInstance(collection.getClass().getComponentType(), size);
77              System.arraycopy(collection, 0, bigger, 0, Array.getLength(collection));
78              return bigger;
79          }
80          if (collection instanceof Collection) {
81              while (((Collection) collection).size() < size) {
82                  ((Collection) collection).add(null);
83              }
84              return collection;
85          }
86          throw new JXPathException("Cannot turn " + collection.getClass().getName() + " into a collection of size " + size);
87      }
88  
89      /**
90       * 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
91       * {@code null}.
92       *
93       * @param method The method that we wish to call
94       * @return Method
95       */
96      public static Method getAccessibleMethod(final Method method) {
97          // Make sure we have a method to check
98          if (method == null) {
99              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 }