001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.util; 017 018import java.beans.PropertyEditor; 019import java.lang.reflect.Array; 020import java.lang.reflect.InvocationTargetException; 021import java.lang.reflect.Method; 022import java.lang.reflect.ParameterizedType; 023import java.lang.reflect.Type; 024import java.util.List; 025import java.util.Map; 026 027import org.apache.log4j.Logger; 028import org.kuali.rice.krad.datadictionary.Copyable; 029import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry; 030import org.kuali.rice.krad.util.KRADUtils; 031 032/** 033 * Represents a property reference in a path expression, for use in implementing 034 * {@link ObjectPathExpressionParser.PathEntry}. 035 * 036 * <p> 037 * This class defers the actual resolution of property references nodes in a path expression until 038 * the transition between parse nodes. This facilitates traversal to the final node in the path. 039 * </p> 040 * 041 * @author Kuali Rice Team (rice.collab@kuali.org) 042 * @version 2.4 043 * @see ObjectPathExpressionParser#parsePathExpression(Object, String, PathEntry) 044 */ 045public class ObjectPropertyReference { 046 047 /** 048 * Primitive default values 049 */ 050 private static final boolean DEFAULT_BOOLEAN = false; 051 private static final byte DEFAULT_BYTE = 0; 052 private static final short DEFAULT_SHORT = 0; 053 private static final int DEFAULT_INT = 0; 054 private static final long DEFAULT_LONG = 0L; 055 private static final float DEFAULT_FLOAT = 0.0f; 056 private static final double DEFAULT_DOUBLE = 0.0d; 057 private static final char DEFAULT_CHAR = '\u0000'; 058 059 /** 060 * Log4j logger. 061 */ 062 private static final Logger LOG = Logger.getLogger(ObjectPropertyReference.class); 063 064 /** 065 * Reference for single use. 066 */ 067 private static final ThreadLocal<ObjectPropertyReference> TL_BUILDER_REF = new ThreadLocal<ObjectPropertyReference>(); 068 069 /** 070 * Reference for single use. 071 */ 072 private static final ThreadLocal<Boolean> TL_WARN = new ThreadLocal<Boolean>(); 073 074 /** 075 * Singleton reference path entry, to be used when parsing for looking up a bean property 076 * without modifying. 077 */ 078 private static final ReferencePathEntry LOOKUP_REF_PATH_ENTRY = new ReferencePathEntry(false); 079 080 /** 081 * Singleton reference path entry, to be used when parsing for modifying the bean property. 082 */ 083 private static final ReferencePathEntry MUTATE_REF_PATH_ENTRY = new ReferencePathEntry(true); 084 085 /** 086 * Internal path entry implementation. 087 */ 088 private static final class ReferencePathEntry implements PathEntry { 089 090 /** 091 * Determines whether or not {@link ObjectPropertyReference#initialize(Object, Class)} 092 * should be used to create an object when a property reference resolves to null. 093 */ 094 private final boolean grow; 095 096 /** 097 * Internal private constructor. 098 */ 099 private ReferencePathEntry(boolean grow) { 100 this.grow = grow; 101 } 102 103 /** 104 * Transition from one path entry to the next while parsing a bean property expression. 105 * 106 * {@inheritDoc} 107 */ 108 @Override 109 public Object parse(String parentPath, Object node, String next) { 110 ObjectPropertyReference current = (ObjectPropertyReference) node; 111 112 // At the initial parse node, copy to a new property reference. 113 // Otherwise, we will modify the existing reference to reduce object construction 114 // due to object reference parsing. 115 if (next == null) { 116 ObjectPropertyReference resolved = new ObjectPropertyReference(); 117 resolved.rootBean = current.bean; 118 resolved.rootPath = current.rootPath; 119 resolved.bean = current.bean; 120 resolved.beanClass = current.beanClass; 121 resolved.beanType = current.beanType; 122 resolved.name = null; 123 resolved.parentPath = null; 124 return resolved; 125 } 126 127 // Get the property type and value from the current node reference. 128 // These will become the bean and bean class after transition. 129 Class<?> beanClass = current.getPropertyType(); 130 Object bean = current.get(); 131 if (bean instanceof Copyable) { 132 bean = CopyUtils.unwrap((Copyable) bean); 133 if (!beanClass.isInstance(bean)) { 134 beanClass = bean.getClass(); 135 } 136 } 137 138 // Determine the parameterized property type, if applicable. 139 // This facilitates type conversion when setting/getting typed collections. 140 Type beanType; 141 Method readMethod = ObjectPropertyUtils.getReadMethod(current.getImplClass(), current.name); 142 if (readMethod == null) { 143 beanType = beanClass; 144 } else { 145 beanType = readMethod.getGenericReturnType(); 146 } 147 148 // When parsing for a set() operation, automatically initialize values. 149 if (grow) { 150 Object newBean = initialize(bean, beanClass); 151 if (newBean != bean) { 152 current.set(newBean); 153 Object verify; 154 assert (verify = current.get()) == newBean : verify + " != " + newBean; 155 bean = newBean; 156 } 157 } 158 159 // Modify the current reference to represent the next parse node, and return. 160 current.bean = bean; 161 current.beanClass = beanClass; 162 current.beanType = beanType; 163 current.name = next; 164 current.parentPath = parentPath; 165 166 return current; 167 } 168 } 169 170 /** 171 * Get the property value for a specific bean property of a known bean class. 172 * 173 * @param propertyValue existing property value 174 * @param propertyType the property type to initialize if the existing value is null 175 * @return The property value for the specific bean property on the given bean. 176 */ 177 private static Object initialize(Object propertyValue, Class<?> propertyType) { 178 Object returnValue = propertyValue; 179 180 if (propertyValue == null) { 181 if (List.class.equals(propertyType)) { 182 returnValue = new java.util.LinkedList<Object>(); 183 184 } else if (Map.class.equals(propertyType)) { 185 returnValue = new java.util.HashMap<Object, Object>(); 186 187 } else if (!String.class.equals(propertyType)) { 188 try { 189 returnValue = propertyType.newInstance(); 190 } catch (InstantiationException e) { 191 throw new IllegalStateException("Failed to create new object for setting property value", e); 192 } catch (IllegalAccessException e) { 193 throw new IllegalStateException("Failed to create new object for setting property value", e); 194 } 195 } 196 } 197 198 return returnValue; 199 } 200 201 /** 202 * Get a property value from an array. 203 * 204 * <p> 205 * NOTE: This method is null and bounds-safe. When the property name does not represent a valid 206 * array index, or the array is null, then null is returned. 207 * </p> 208 * 209 * @param array The array. 210 * @param name The name of the property value. 211 * @return The property value for the named entry in the array. When name is 'size' or 'length', 212 * then the length of the array is returned, otherwise the property name is converted to 213 * an integer and used as the array index. 214 */ 215 private static Object getArray(Object array, String name) { 216 if (array == null) { 217 return null; 218 } 219 220 for (int i = 0; i < name.length(); i++) { 221 if (!Character.isDigit(name.charAt(i))) { 222 return null; 223 } 224 } 225 226 int i = Integer.parseInt(name); 227 228 if (i >= Array.getLength(array)) { 229 return null; 230 } 231 232 return Array.get(array, i); 233 } 234 235 /** 236 * Set a property value in an array. 237 * 238 * @param array The array. 239 * @param name A string representation of the index in the array. 240 * @param value The property value to set in the array. 241 */ 242 private static void setArray(Object array, String name, Object value) { 243 Array.set(array, Integer.parseInt(name), value); 244 } 245 246 /** 247 * Get a property value from an list. 248 * 249 * <p> 250 * NOTE: This method is null and bounds-safe. When the property name does not represent a valid 251 * list index, or the list is null, then null is returned. 252 * </p> 253 * 254 * @param list The list. 255 * @param name The name of the property value. 256 * @return The property value for the named entry in the list. When name is 'size' or 'length', 257 * then the length of the list is returned, otherwise the property name is converted to 258 * an integer and used as the list index. 259 */ 260 private static Object getList(List<?> list, String name) { 261 int length; 262 if (list == null) { 263 length = 0; 264 } else { 265 length = list.size(); 266 } 267 268 for (int i = 0; i < name.length(); i++) { 269 if (!Character.isDigit(name.charAt(i))) { 270 return null; 271 } 272 } 273 274 int i = Integer.parseInt(name); 275 if (i >= length) { 276 return null; 277 } 278 279 return list.get(i); 280 } 281 282 /** 283 * Set a property value in a list. 284 * 285 * @param list The list. 286 * @param name A string representation of the list index. 287 * @param value The value to add to the list. 288 */ 289 @SuppressWarnings("unchecked") 290 private static void setList(List<?> list, String name, Object value) { 291 int i = Integer.parseInt(name); 292 while (i >= list.size()) { 293 list.add(null); 294 } 295 ((List<Object>) list).set(i, value); 296 } 297 298 /** 299 * Get a property value from an map. 300 * 301 * @param map The map. 302 * @param name The name of the property value. 303 * @return The property value for the named entry in the map. 304 */ 305 private static Object getMap(Map<?, ?> map, String name) { 306 if (map != null && map.containsKey(name)) { 307 return map.get(name); 308 } 309 return null; 310 } 311 312 /** 313 * Determine if a warning should be logged on when an invalid property is encountered 314 * on the current thread. 315 * @return True to log warnings when invalid properties are encountered, false to ignore 316 * invalid properties. 317 */ 318 public static boolean isWarning() { 319 return Boolean.TRUE.equals(TL_WARN.get()); 320 } 321 322 /** 323 * Indicate whether or not a warning should be logged on when an invalid property is encountered 324 * on the current thread. 325 * @param warning True to log warnings when invalid properties are encountered, false to ignore 326 * invalid properties. 327 */ 328 public static void setWarning(boolean warning) { 329 if (warning) { 330 TL_WARN.set(true); 331 } else { 332 TL_WARN.remove(); 333 } 334 } 335 336 /** 337 * Resolve a path expression on a bean. 338 * 339 * @param bean The bean. 340 * @param beanClass The bean class. 341 * @param propertyPath The property path expression. 342 * @param grow True to create objects while traversing the path, false to traverse class 343 * structure only when referring to null. 344 * @return A reference to the final parse node involved in parsing the path expression. 345 */ 346 public static ObjectPropertyReference resolvePath(Object bean, Class<?> beanClass, String propertyPath, boolean grow) { 347 if (ObjectPathExpressionParser.isPath(propertyPath)) { 348 349 // Parse the path expression. This requires a new reference object since object read 350 // methods could potentially call this method recursively. 351 ObjectPropertyReference reference = new ObjectPropertyReference(); 352 reference.beanClass = beanClass; 353 reference.rootPath = propertyPath; 354 if (bean instanceof Copyable) { 355 reference.bean = CopyUtils.unwrap((Copyable) bean); 356 reference.rootBean = reference.bean; 357 if (!(beanClass.isInstance(reference.bean))) { 358 reference.beanClass = reference.bean.getClass(); 359 } 360 } else { 361 reference.bean = bean; 362 reference.rootBean = bean; 363 } 364 365 ObjectPropertyReference resolved = (ObjectPropertyReference) ObjectPathExpressionParser 366 .parsePathExpression(reference, propertyPath, 367 grow ? MUTATE_REF_PATH_ENTRY : LOOKUP_REF_PATH_ENTRY); 368 369 reference.bean = resolved.bean; 370 reference.beanClass = resolved.beanClass; 371 reference.beanType = resolved.beanType; 372 reference.name = resolved.name; 373 return reference; 374 375 } else { 376 377 return resolveProperty(bean, beanClass, propertyPath); 378 379 } 380 } 381 382 /** 383 * Get a single-use reference for resolving a property on a bean. 384 * 385 * <p> 386 * When using this method, the property name will be treated as-is, and will not be resolved as 387 * a path expression. 388 * </p> 389 * 390 * @param bean The bean. 391 * @param beanClass The bean class. 392 * @param propertyPath The property path. 393 * @return A single-use reference to the final parse node involved in parsing the path 394 * expression. Note that the reference returned by this method will be reused and 395 * modified by the next call, so should not be set to a variable. 396 */ 397 public static ObjectPropertyReference resolveProperty(Object bean, Class<?> beanClass, String propertyPath) { 398 ObjectPropertyReference reference = TL_BUILDER_REF.get(); 399 if (reference == null) { 400 reference = new ObjectPropertyReference(); 401 TL_BUILDER_REF.set(reference); 402 } 403 reference.beanClass = beanClass; 404 if (bean instanceof Copyable) { 405 reference.bean = CopyUtils.unwrap((Copyable) bean); 406 if (!(beanClass.isInstance(reference.bean)) && reference.bean != null) { 407 reference.beanClass = reference.bean.getClass(); 408 } 409 } else { 410 reference.bean = bean; 411 } 412 reference.rootBean = reference.bean; 413 reference.rootPath = propertyPath; 414 reference.beanType = reference.beanClass; 415 reference.name = propertyPath; 416 return reference; 417 } 418 419 /** 420 * The root bean, may be null for traversing only class data. 421 */ 422 private Object rootBean; 423 424 /** 425 * The bean, may be null for traversing only class data. 426 */ 427 private Object bean; 428 429 /** 430 * The bean class. 431 */ 432 private Class<?> beanClass; 433 434 /** 435 * The bean type. 436 */ 437 private Type beanType; 438 439 /** 440 * The property name. 441 */ 442 private String name; 443 444 /** 445 * The parent property path. 446 */ 447 private String parentPath; 448 449 /** 450 * The root property path. 451 */ 452 private String rootPath; 453 454 /** 455 * Internal private constructor. 456 */ 457 private ObjectPropertyReference() {} 458 459 /** 460 * Convert a string property value to the targeted property type. 461 * 462 * @param propertyValue The string property value. 463 * @return The property value, converted to the property type. 464 */ 465 private Object convertStringToPropertyType(String propertyValue) { 466 Class<?> propertyType = getPropertyType(); 467 468 // TODO: these methods, and their inversions (below) need to be either support escaping 469 // or be removed. Both have been included for equivalence with previous BeanWrapper 470 // implementation. 471 if (List.class.equals(propertyType)) { 472 return KRADUtils.convertStringParameterToList(propertyValue); 473 474 } else if (Map.class.equals(propertyType)) { 475 return KRADUtils.convertStringParameterToMap(propertyValue); 476 477 } else { 478 479 PropertyEditor editor = ObjectPropertyUtils.getPropertyEditor(rootBean, rootPath); 480 if (editor == null) { 481 throw new IllegalArgumentException("No property editor available for converting '" + propertyValue 482 + "' to " + propertyType); 483 } 484 485 editor.setAsText((String) propertyValue); 486 return editor.getValue(); 487 } 488 489 } 490 491 /** 492 * Convert a property value to a string. 493 * 494 * @param propertyValue The property value. 495 * @return The property value, converted to a string. 496 */ 497 private Object convertPropertyValueToString(Object propertyValue) { 498 499 // TODO: these methods, and their inversions (above) need to be either support escaping 500 // or be removed. Both have been included for equivalence with previous BeanWrapper 501 // implementation. 502 // FIXME: Where are these conversions used? Can they be removed? 503 if (propertyValue instanceof List) { 504 StringBuilder listStringBuilder = new StringBuilder(); 505 for (Object item : (List<?>) propertyValue) { 506 if (listStringBuilder.length() > 0) { 507 listStringBuilder.append(','); 508 } 509 listStringBuilder.append((String) item); 510 } 511 return listStringBuilder.toString(); 512 513 } else if (propertyValue instanceof Map) { 514 @SuppressWarnings("unchecked") 515 Map<String, String> mapPropertyValue = (Map<String, String>) propertyValue; 516 return KRADUtils.buildMapParameterString(mapPropertyValue); 517 518 } else { 519 520 PropertyEditor editor = ObjectPropertyUtils 521 .getPropertyEditor(ObjectPropertyUtils.getPrimitiveType(propertyValue.getClass())); 522 if (editor == null) { 523 throw new IllegalArgumentException("No property editor available for converting '" + propertyValue 524 + "' from " + propertyValue.getClass()); 525 } 526 527 editor.setValue(propertyValue); 528 return editor.getAsText(); 529 } 530 } 531 532 /** 533 * Convert a property value to the targeted property type. 534 * 535 * @param propertyValue The property value. 536 * @return The property value, converted to the property type. 537 */ 538 private Object convertToPropertyType(Object propertyValue) { 539 Class<?> propertyType = getPropertyType(); 540 541 if (propertyValue == null) { 542 return primitiveDefault(propertyType); 543 } 544 545 if (propertyType.isInstance(propertyValue)) { 546 return propertyValue; 547 } 548 549 if (propertyValue instanceof String) { 550 return convertStringToPropertyType((String) propertyValue); 551 } 552 553 if (propertyType.equals(String.class)) { 554 return convertPropertyValueToString(propertyValue); 555 } 556 557 return propertyValue; 558 } 559 560 /** 561 * Get default values for primitives 562 * 563 * @param object The property value. 564 * @return The default value for the Object type passed 565 */ 566 private Object primitiveDefault(Class<?> object) { 567 if (!object.isPrimitive()){ 568 return null; 569 } else if (object.equals(boolean.class)) { 570 return DEFAULT_BOOLEAN; 571 } else if (object.equals(byte.class)) { 572 return DEFAULT_BYTE; 573 } else if (object.equals(char.class)) { 574 return DEFAULT_CHAR; 575 } else if (object.equals(short.class)) { 576 return DEFAULT_SHORT; 577 } else if (object.equals(int.class)) { 578 return DEFAULT_INT; 579 } else if (object.equals(long.class)) { 580 return DEFAULT_LONG; 581 } else if (object.equals(float.class)) { 582 return DEFAULT_FLOAT; 583 } else if (object.equals(double.class)) { 584 return DEFAULT_DOUBLE; 585 } 586 587 return null; 588 } 589 590 /** 591 * Get the bean. 592 * @return The bean 593 */ 594 public Object getBean() { 595 return this.bean; 596 } 597 598 /** 599 * Get the bean class. 600 * 601 * <p> 602 * The bean class may be a super-class of the bean, and is likely to be an abstract class or 603 * interface. 604 * </p> 605 * 606 * @return The bean class. It is expected that the value returned by {@link #getBean()} is 607 * either null, or that {@link #getBeanClass()}.{@link Class#isInstance(Object) 608 * isInstance(}{@link #getBean()}{@link Class#isInstance(Object) )} will always return 609 * true. 610 */ 611 public Class<?> getBeanClass() { 612 return this.beanClass; 613 } 614 615 /** 616 * Get the bean implementation class. 617 * 618 * @return The the bean implementation class. The class returned by this method should always be 619 * the same class or a subclass of the class returned by {@link #getBeanClass()}. When 620 * {@link #getBean()} returns a non-null value it is expected that {@link #getBean()}. 621 * {@link Object#getClass() getClass()} == {@link #getImplClass()}. 622 */ 623 public Class<?> getImplClass() { 624 assert bean == null || beanClass.isInstance(bean) : bean + " is not a " + beanClass; 625 return bean == null ? beanClass : bean.getClass(); 626 } 627 628 /** 629 * Get the property name. 630 * 631 * @return The property name. 632 */ 633 public String getName() { 634 return this.name; 635 } 636 637 /** 638 * Determine if a list or array property is readable. 639 * 640 * @return True if the property is a list or array, and is readable, false if not. 641 */ 642 private boolean isListOrArrayAndCanReadOrWrite() { 643 Class<?> implClass = getImplClass(); 644 645 if (!implClass.isArray() && !List.class.isAssignableFrom(implClass)) { 646 return false; 647 } 648 649 if (name.length() == 0) { 650 return false; 651 } 652 653 for (int i = 0; i < name.length(); i++) { 654 if (!Character.isDigit(name.charAt(i))) { 655 return false; 656 } 657 } 658 659 return true; 660 } 661 662 /** 663 * Determine if a list or array property is readable. 664 * 665 * @return True if the property is a list or array, and is readable, false if not. 666 */ 667 private Boolean canReadOrWriteSimple() { 668 if (name == null) { 669 // self reference 670 return true; 671 } 672 673 Class<?> implClass = getImplClass(); 674 675 if (implClass == null) { 676 return false; 677 } 678 679 if (isListOrArrayAndCanReadOrWrite()) { 680 return true; 681 } 682 683 if (Map.class.isAssignableFrom(implClass)) { 684 return true; 685 } 686 687 return null; 688 } 689 690 /** 691 * Determine if the bean property is readable. 692 * 693 * @return True if the property is readable, false if not. 694 */ 695 public boolean canRead() { 696 Boolean simple = canReadOrWriteSimple(); 697 698 if (simple != null) { 699 return simple; 700 } 701 702 return ObjectPropertyUtils.getReadMethod(getImplClass(), name) != null; 703 } 704 705 /** 706 * Determine if the property is writable. 707 * 708 * @return True if the property is writable, false if not. 709 */ 710 public boolean canWrite() { 711 Boolean simple = canReadOrWriteSimple(); 712 713 if (simple != null) { 714 return simple; 715 } 716 717 return ObjectPropertyUtils.getWriteMethod(getImplClass(), name) != null; 718 } 719 720 /** 721 * Get the property value for a specific bean property of a known bean class. 722 * 723 * @return The property value for the specific bean property on the given bean. 724 */ 725 public Object getFromReadMethod() { 726 Class<?> implClass = getImplClass(); 727 728 Method readMethod = ObjectPropertyUtils.getReadMethod(implClass, name); 729 730 if (readMethod == null) { 731 if (isWarning()) { 732 IllegalArgumentException missingPropertyException = new IllegalArgumentException("No property name '" 733 + name + "' is readable on " + 734 (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass)); 735 LOG.warn(missingPropertyException); 736 } 737 738 return null; 739 } 740 741 try { 742 return readMethod.invoke(bean); 743 } catch (IllegalAccessException e) { 744 throw new IllegalArgumentException("Illegal access invoking property read method " + readMethod, e); 745 } catch (InvocationTargetException e) { 746 Throwable cause = e.getCause(); 747 if (cause instanceof RuntimeException) { 748 throw (RuntimeException) cause; 749 } else if (cause instanceof Error) { 750 throw (Error) cause; 751 } 752 throw new IllegalStateException("Unexpected invocation target exception invoking property read method " 753 + readMethod, e); 754 } 755 } 756 757 /** 758 * Get the property value for a specific bean property of a known bean class. 759 * 760 * @return The property value for the specific bean property on the given bean. 761 */ 762 public Object get() { 763 if (name == null) { 764 return bean; 765 } 766 767 Class<?> implClass = getImplClass(); 768 769 if (implClass == null || bean == null) { 770 return null; 771 772 } else if (implClass.isArray()) { 773 return getArray(bean, name); 774 775 } else if (List.class.isAssignableFrom(implClass)) { 776 return getList((List<?>) bean, name); 777 778 } else if (Map.class.isAssignableFrom(implClass)) { 779 return getMap((Map<?, ?>) bean, name); 780 781 } else { 782 return getFromReadMethod(); 783 } 784 } 785 786 /** 787 * Get the type of a specific property on a collection. 788 * 789 * @return The type of the referenced element in the collection, if non-null. When null, the 790 * parameterized type of the collection will be returned, or Object if the collection is 791 * not parameterized. If this is not a reference to an indexed collection, the null is 792 * returned. 793 */ 794 private Class<?> getCollectionPropertyType() { 795 Class<?> implClass = getImplClass(); 796 boolean isMap = Map.class.isAssignableFrom(implClass); 797 boolean isList = List.class.isAssignableFrom(implClass); 798 799 Object refBean; 800 801 if (isMap) { 802 refBean = getMap((Map<?, ?>) bean, name); 803 } else if (isList) { 804 refBean = getList((List<?>) bean, name); 805 } else { 806 return null; 807 } 808 809 if (refBean != null) { 810 return refBean.getClass(); 811 } 812 813 if (beanType instanceof ParameterizedType) { 814 ParameterizedType parameterizedType = (ParameterizedType) beanType; 815 Type valueType = parameterizedType.getActualTypeArguments()[isList ? 0 : 1]; 816 817 if (valueType instanceof Class) { 818 return (Class<?>) valueType; 819 } 820 } 821 822 return Object.class; 823 } 824 825 /** 826 * Get the type of a specific property on a given bean class. 827 * 828 * @return The type of the specific property on the given bean class. 829 */ 830 private Class<?> getPropertyTypeFromReadOrWriteMethod() { 831 Class<?> implClass = getImplClass(); 832 833 Method readMethod = ObjectPropertyUtils.getReadMethod(implClass, name); 834 Method writeMethod; 835 836 if (readMethod == null) { 837 838 writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name); 839 assert writeMethod == null || writeMethod.getParameterTypes().length == 1 : "Invalid write method " 840 + writeMethod; 841 842 if (writeMethod == null && isWarning()) { 843 IllegalArgumentException missingPropertyException = new IllegalArgumentException("No property name '" 844 + name + "' is readable or writable on " + 845 (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass)); 846 LOG.warn(missingPropertyException); 847 } 848 849 return writeMethod == null ? null : writeMethod.getParameterTypes()[0]; 850 851 } else { 852 Class<?> returnType = readMethod.getReturnType(); 853 assert (writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name)) == null 854 || writeMethod.getParameterTypes()[0].isAssignableFrom(returnType) : "Property types don't match " 855 + readMethod + " " + writeMethod; 856 return returnType; 857 } 858 } 859 860 /** 861 * Get the type of a specific property on the implementation class. 862 * 863 * @return The type of the specific property on the implementation class. 864 */ 865 public Class<?> getPropertyType() { 866 Class<?> implClass = getImplClass(); 867 868 if (implClass == null) { 869 return null; 870 } 871 872 if (name == null) { 873 // self reference 874 return getImplClass(); 875 } 876 877 Class<?> propertyType = getCollectionPropertyType(); 878 879 if (propertyType != null) { 880 return propertyType; 881 } else { 882 return getPropertyTypeFromReadOrWriteMethod(); 883 } 884 } 885 886 /** 887 * Set the property to a specific value using the property's write method. 888 * 889 * @param propertyValue The property value. 890 */ 891 private void setUsingWriteMethod(Object propertyValue) { 892 Class<?> implClass = getImplClass(); 893 Method writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name); 894 895 if (writeMethod == null) { 896 throw new IllegalArgumentException("No property name '" + name + "' is writable on " + 897 (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass)); 898 } 899 900 try { 901 writeMethod.invoke(bean, propertyValue); 902 } catch (IllegalAccessException e) { 903 throw new IllegalArgumentException("Illegal access invoking property write method " + writeMethod, e); 904 } catch (InvocationTargetException e) { 905 Throwable cause = e.getCause(); 906 if (cause instanceof RuntimeException) { 907 throw (RuntimeException) cause; 908 } else if (cause instanceof Error) { 909 throw (Error) cause; 910 } 911 throw new IllegalStateException( 912 "Unexpected invocation target exception invoking property write method " 913 + writeMethod, e); 914 } 915 } 916 917 /** 918 * Set the property to a specific value. 919 * 920 * @param propertyValue The property value. 921 */ 922 public void set(Object propertyValue) { 923 if (name == null) { 924 throw new IllegalArgumentException("Cannot modify a self-reference"); 925 } 926 927 if (bean == null) { 928 throw new IllegalArgumentException("Reference is null"); 929 } 930 931 propertyValue = convertToPropertyType(propertyValue); 932 933 Class<?> implClass = getImplClass(); 934 935 if (implClass == null) { 936 throw new IllegalArgumentException("No property name '" + name + "' is writable on " + beanClass); 937 } 938 939 if (implClass.isArray()) { 940 setArray(bean, name, propertyValue); 941 942 } else if (List.class.isAssignableFrom(implClass)) { 943 setList((List<?>) bean, name, propertyValue); 944 945 } else if (Map.class.isAssignableFrom(implClass)) { 946 @SuppressWarnings("unchecked") 947 Map<Object, Object> uncheckedMap = (Map<Object, Object>) bean; 948 uncheckedMap.put(name, propertyValue); 949 950 } else { 951 setUsingWriteMethod(propertyValue); 952 } 953 } 954 955}