001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.jxpath.util; 019 020import java.beans.IndexedPropertyDescriptor; 021import java.beans.PropertyDescriptor; 022import java.lang.reflect.Array; 023import java.lang.reflect.InvocationTargetException; 024import java.lang.reflect.Method; 025import java.lang.reflect.Modifier; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Map; 033 034import org.apache.commons.jxpath.Container; 035import org.apache.commons.jxpath.DynamicPropertyHandler; 036import org.apache.commons.jxpath.JXPathException; 037 038/** 039 * Collection and property access utilities. 040 */ 041public class ValueUtils { 042 043 private static Map<Class, DynamicPropertyHandler> dynamicPropertyHandlerMap = new HashMap<>(); 044 private static final int UNKNOWN_LENGTH_MAX_COUNT = 16000; 045 046 /** 047 * Convert value to type. 048 * 049 * @param value Object 050 * @param type destination 051 * @return conversion result 052 */ 053 private static Object convert(final Object value, final Class type) { 054 try { 055 return TypeUtils.convert(value, type); 056 } catch (final Exception ex) { 057 throw new JXPathException("Cannot convert value of class " + (value == null ? "null" : value.getClass().getName()) + " to type " + type, ex); 058 } 059 } 060 061 /** 062 * Grows the collection if necessary to the specified size. Returns the new, expanded collection. 063 * 064 * @param collection to expand 065 * @param size desired size 066 * @return collection or array 067 */ 068 public static Object expandCollection(final Object collection, final int size) { 069 if (collection == null) { 070 return null; 071 } 072 if (size < getLength(collection)) { 073 throw new JXPathException("adjustment of " + collection + " to size " + size + " is not an expansion"); 074 } 075 if (collection.getClass().isArray()) { 076 final Object bigger = Array.newInstance(collection.getClass().getComponentType(), size); 077 System.arraycopy(collection, 0, bigger, 0, Array.getLength(collection)); 078 return bigger; 079 } 080 if (collection instanceof Collection) { 081 while (((Collection) collection).size() < size) { 082 ((Collection) collection).add(null); 083 } 084 return collection; 085 } 086 throw new JXPathException("Cannot turn " + collection.getClass().getName() + " into a collection of size " + size); 087 } 088 089 /** 090 * Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified Method. If no such method can be found, return 091 * {@code null}. 092 * 093 * @param method The method that we wish to call 094 * @return Method 095 */ 096 public static Method getAccessibleMethod(final Method method) { 097 // Make sure we have a method to check 098 if (method == null) { 099 return null; 100 } 101 // If the requested method is not public we cannot call it 102 if (!Modifier.isPublic(method.getModifiers())) { 103 return null; 104 } 105 // If the declaring class is public, we are done 106 Class clazz = method.getDeclaringClass(); 107 if (Modifier.isPublic(clazz.getModifiers())) { 108 return method; 109 } 110 final String name = method.getName(); 111 final Class[] parameterTypes = method.getParameterTypes(); 112 while (clazz != null) { 113 // Check the implemented interfaces and subinterfaces 114 final Method aMethod = getAccessibleMethodFromInterfaceNest(clazz, name, parameterTypes); 115 if (aMethod != null) { 116 return aMethod; 117 } 118 clazz = clazz.getSuperclass(); 119 if (clazz != null && Modifier.isPublic(clazz.getModifiers())) { 120 try { 121 return clazz.getDeclaredMethod(name, parameterTypes); 122 } catch (final NoSuchMethodException ignore) { // NOPMD 123 // ignore 124 } 125 } 126 } 127 return null; 128 } 129 130 /** 131 * Gets an accessible method (that is, one that can be invoked via reflection) that implements the specified method, by scanning through all implemented 132 * interfaces and subinterfaces. If no such Method can be found, return {@code null}. 133 * 134 * @param clazz Parent class for the interfaces to be checked 135 * @param methodName Method name of the method we wish to call 136 * @param parameterTypes The parameter type signatures 137 * @return Method 138 */ 139 private static Method getAccessibleMethodFromInterfaceNest(final Class clazz, final String methodName, final Class[] parameterTypes) { 140 Method method = null; 141 // Check the implemented interfaces of the parent class 142 final Class[] interfaces = clazz.getInterfaces(); 143 for (final Class element : interfaces) { 144 // Is this interface public? 145 if (!Modifier.isPublic(element.getModifiers())) { 146 continue; 147 } 148 // Does the method exist on this interface? 149 try { 150 method = element.getDeclaredMethod(methodName, parameterTypes); 151 } catch (final NoSuchMethodException ignore) { // NOPMD 152 // ignore 153 } 154 if (method != null) { 155 break; 156 } 157 // Recursively check our parent interfaces 158 method = getAccessibleMethodFromInterfaceNest(element, methodName, parameterTypes); 159 if (method != null) { 160 break; 161 } 162 } 163 // Return whatever we have found 164 return method; 165 } 166 167 /** 168 * Returns 1 if the type is a collection, -1 if it is definitely not and 0 if it may be a collection in some cases. 169 * 170 * @param clazz to test 171 * @return int 172 */ 173 public static int getCollectionHint(final Class clazz) { 174 if (clazz.isArray()) { 175 return 1; 176 } 177 if (Collection.class.isAssignableFrom(clazz)) { 178 return 1; 179 } 180 if (clazz.isPrimitive()) { 181 return -1; 182 } 183 if (clazz.isInterface()) { 184 return 0; 185 } 186 if (Modifier.isFinal(clazz.getModifiers())) { 187 return -1; 188 } 189 return 0; 190 } 191 192 /** 193 * Returns a shared instance of the dynamic property handler class returned by {@code getDynamicPropertyHandlerClass()}. 194 * 195 * @param clazz to handle 196 * @return DynamicPropertyHandler 197 */ 198 public static DynamicPropertyHandler getDynamicPropertyHandler(final Class clazz) { 199 return dynamicPropertyHandlerMap.computeIfAbsent(clazz, k -> { 200 try { 201 return (DynamicPropertyHandler) clazz.getConstructor().newInstance(); 202 } catch (final Exception ex) { 203 throw new JXPathException("Cannot allocate dynamic property handler of class " + clazz.getName(), ex); 204 } 205 }); 206 } 207 208 /** 209 * If there is a regular non-indexed read method for this property, uses this method to obtain the collection and then returns its length. Otherwise, 210 * attempts to guess the length of the collection by calling the indexed get method repeatedly. The method is supposed to throw an exception if the index is 211 * out of bounds. 212 * 213 * @param object collection 214 * @param pd IndexedPropertyDescriptor 215 * @return int 216 */ 217 public static int getIndexedPropertyLength(final Object object, final IndexedPropertyDescriptor pd) { 218 if (pd.getReadMethod() != null) { 219 return getLength(getValue(object, pd)); 220 } 221 final Method readMethod = pd.getIndexedReadMethod(); 222 if (readMethod == null) { 223 throw new JXPathException("No indexed read method for property " + pd.getName()); 224 } 225 for (int i = 0; i < UNKNOWN_LENGTH_MAX_COUNT; i++) { 226 try { 227 readMethod.invoke(object, Integer.valueOf(i)); 228 } catch (final Throwable t) { 229 return i; 230 } 231 } 232 throw new JXPathException("Cannot determine the length of the indexed property " + pd.getName()); 233 } 234 235 /** 236 * Returns the length of the supplied collection. If the supplied object is not a collection, returns 1. If collection is null, returns 0. 237 * 238 * @param collection to check 239 * @return int 240 */ 241 public static int getLength(Object collection) { 242 if (collection == null) { 243 return 0; 244 } 245 collection = getValue(collection); 246 if (collection.getClass().isArray()) { 247 return Array.getLength(collection); 248 } 249 if (collection instanceof Collection) { 250 return ((Collection) collection).size(); 251 } 252 return 1; 253 } 254 255 /** 256 * If the parameter is a container, opens the container and return the contents. The method is recursive. 257 * 258 * @param object to read 259 * @return Object 260 */ 261 public static Object getValue(Object object) { 262 while (object instanceof Container) { 263 object = ((Container) object).getValue(); 264 } 265 return object; 266 } 267 268 /** 269 * Returns the index'th element of the supplied collection. 270 * 271 * @param collection to read 272 * @param index int 273 * @return collection[index] 274 */ 275 public static Object getValue(Object collection, final int index) { 276 collection = getValue(collection); 277 Object value = collection; 278 if (collection != null) { 279 if (collection.getClass().isArray()) { 280 if (index < 0 || index >= Array.getLength(collection)) { 281 return null; 282 } 283 value = Array.get(collection, index); 284 } else if (collection instanceof List) { 285 if (index < 0 || index >= ((List) collection).size()) { 286 return null; 287 } 288 value = ((List) collection).get(index); 289 } else if (collection instanceof Collection) { 290 if (index < 0 || index >= ((Collection) collection).size()) { 291 return null; 292 } 293 int i = 0; 294 final Iterator it = ((Collection) collection).iterator(); 295 for (; i < index; i++) { 296 it.next(); 297 } 298 if (it.hasNext()) { 299 value = it.next(); 300 } else { 301 value = null; 302 } 303 } 304 } 305 return value; 306 } 307 308 /** 309 * Returns the value of the bean's property represented by the supplied property descriptor. 310 * 311 * @param bean to read 312 * @param propertyDescriptor indicating what to read 313 * @return Object value 314 */ 315 public static Object getValue(final Object bean, final PropertyDescriptor propertyDescriptor) { 316 Object value; 317 try { 318 final Method method = getAccessibleMethod(propertyDescriptor.getReadMethod()); 319 if (method == null) { 320 throw new JXPathException("No read method"); 321 } 322 value = method.invoke(bean); 323 } catch (final Exception ex) { 324 throw new JXPathException("Cannot access property: " + (bean == null ? "null" : bean.getClass().getName()) + "." + propertyDescriptor.getName(), 325 ex); 326 } 327 return value; 328 } 329 330 /** 331 * Returns the index'th element of the bean's property represented by the supplied property descriptor. 332 * 333 * @param bean to read 334 * @param propertyDescriptor indicating what to read 335 * @param index int 336 * @return Object 337 */ 338 public static Object getValue(final Object bean, final PropertyDescriptor propertyDescriptor, final int index) { 339 if (propertyDescriptor instanceof IndexedPropertyDescriptor) { 340 try { 341 final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) propertyDescriptor; 342 final Method method = ipd.getIndexedReadMethod(); 343 if (method != null) { 344 return method.invoke(bean, Integer.valueOf(index)); 345 } 346 } catch (final InvocationTargetException ex) { 347 final Throwable t = ex.getTargetException(); 348 if (t instanceof IndexOutOfBoundsException) { 349 return null; 350 } 351 throw new JXPathException("Cannot access property: " + propertyDescriptor.getName(), t); 352 } catch (final Throwable ex) { 353 throw new JXPathException("Cannot access property: " + propertyDescriptor.getName(), ex); 354 } 355 } 356 // We will fall through if there is no indexed read 357 return getValue(getValue(bean, propertyDescriptor), index); 358 } 359 360 /** 361 * Returns true if the object is an array or a Collection. 362 * 363 * @param value to test 364 * @return boolean 365 */ 366 public static boolean isCollection(Object value) { 367 value = getValue(value); 368 if (value == null) { 369 return false; 370 } 371 if (value.getClass().isArray()) { 372 return true; 373 } 374 return value instanceof Collection; 375 } 376 377 /** 378 * Returns an iterator for the supplied collection. If the argument is null, returns an empty iterator. If the argument is not a collection, returns an 379 * iterator that produces just that one object. 380 * 381 * @param collection to iterate 382 * @return Iterator 383 */ 384 public static Iterator iterate(final Object collection) { 385 if (collection == null) { 386 return Collections.EMPTY_LIST.iterator(); 387 } 388 if (collection.getClass().isArray()) { 389 final int length = Array.getLength(collection); 390 if (length == 0) { 391 return Collections.EMPTY_LIST.iterator(); 392 } 393 final ArrayList list = new ArrayList(); 394 for (int i = 0; i < length; i++) { 395 list.add(Array.get(collection, i)); 396 } 397 return list.iterator(); 398 } 399 if (collection instanceof Collection) { 400 return ((Collection) collection).iterator(); 401 } 402 return Collections.singletonList(collection).iterator(); 403 } 404 405 /** 406 * Remove the index'th element from the supplied collection. 407 * 408 * @param collection to edit 409 * @param index int 410 * @return the resulting collection 411 */ 412 public static Object remove(Object collection, final int index) { 413 collection = getValue(collection); 414 if (collection == null) { 415 return null; 416 } 417 if (index >= getLength(collection)) { 418 throw new JXPathException("No such element at index " + index); 419 } 420 if (collection.getClass().isArray()) { 421 final int length = Array.getLength(collection); 422 final Object smaller = Array.newInstance(collection.getClass().getComponentType(), length - 1); 423 if (index > 0) { 424 System.arraycopy(collection, 0, smaller, 0, index); 425 } 426 if (index < length - 1) { 427 System.arraycopy(collection, index + 1, smaller, index, length - index - 1); 428 } 429 return smaller; 430 } 431 if (collection instanceof List) { 432 final int size = ((List) collection).size(); 433 if (index < size) { 434 ((List) collection).remove(index); 435 } 436 return collection; 437 } 438 if (collection instanceof Collection) { 439 final Iterator it = ((Collection) collection).iterator(); 440 for (int i = 0; i < index; i++) { 441 if (!it.hasNext()) { 442 break; 443 } 444 it.next(); 445 } 446 if (it.hasNext()) { 447 it.next(); 448 it.remove(); 449 } 450 return collection; 451 } 452 throw new JXPathException("Cannot remove " + collection.getClass().getName() + "[" + index + "]"); 453 } 454 455 /** 456 * Modifies the index'th element of the supplied collection. Converts the value to the required type if necessary. 457 * 458 * @param collection to edit 459 * @param index to replace 460 * @param value new value 461 */ 462 public static void setValue(Object collection, final int index, final Object value) { 463 collection = getValue(collection); 464 if (collection != null) { 465 if (collection.getClass().isArray()) { 466 Array.set(collection, index, convert(value, collection.getClass().getComponentType())); 467 } else if (collection instanceof List) { 468 ((List) collection).set(index, value); 469 } else if (collection instanceof Collection) { 470 throw new UnsupportedOperationException("Cannot set value of an element of a " + collection.getClass().getName()); 471 } 472 } 473 } 474 // 475 // The rest of the code in this file was copied FROM 476 // org.apache.commons.beanutils.PropertyUtil. We don't want to introduce 477 // a dependency on BeanUtils yet - DP. 478 // 479 480 /** 481 * Modifies the index'th element of the bean's property represented by the supplied property descriptor. Converts the value to the required type if 482 * necessary. 483 * 484 * @param bean to edit 485 * @param propertyDescriptor indicating what to set 486 * @param index int 487 * @param value to set 488 */ 489 public static void setValue(final Object bean, final PropertyDescriptor propertyDescriptor, final int index, final Object value) { 490 if (propertyDescriptor instanceof IndexedPropertyDescriptor) { 491 try { 492 final IndexedPropertyDescriptor ipd = (IndexedPropertyDescriptor) propertyDescriptor; 493 final Method method = ipd.getIndexedWriteMethod(); 494 if (method != null) { 495 method.invoke(bean, Integer.valueOf(index), convert(value, ipd.getIndexedPropertyType())); 496 return; 497 } 498 } catch (final Exception ex) { 499 throw new IllegalArgumentException("Cannot access property: " + propertyDescriptor.getName() + ", " + ex.getMessage()); 500 } 501 } 502 // We will fall through if there is no indexed read 503 final Object collection = getValue(bean, propertyDescriptor); 504 if (isCollection(collection)) { 505 setValue(collection, index, value); 506 } else if (index == 0) { 507 setValue(bean, propertyDescriptor, value); 508 } else { 509 throw new IllegalArgumentException("Not a collection: " + propertyDescriptor.getName()); 510 } 511 } 512 513 /** 514 * Modifies the value of the bean's property represented by the supplied property descriptor. 515 * 516 * @param bean to read 517 * @param propertyDescriptor indicating what to read 518 * @param value to set 519 */ 520 public static void setValue(final Object bean, final PropertyDescriptor propertyDescriptor, Object value) { 521 try { 522 final Method method = getAccessibleMethod(propertyDescriptor.getWriteMethod()); 523 if (method == null) { 524 throw new JXPathException("No write method"); 525 } 526 value = convert(value, propertyDescriptor.getPropertyType()); 527 method.invoke(bean, value); 528 } catch (final Exception ex) { 529 throw new JXPathException("Cannot modify property: " + (bean == null ? "null" : bean.getClass().getName()) + "." + propertyDescriptor.getName(), 530 ex); 531 } 532 } 533 534 /** 535 * Constructs a new instance. 536 * 537 * @deprecated Will be private in the next major version. 538 */ 539 @Deprecated 540 public ValueUtils() { 541 // empty 542 } 543}