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