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.BeanInfo; 019import java.beans.IntrospectionException; 020import java.beans.Introspector; 021import java.beans.PropertyDescriptor; 022import java.beans.PropertyEditor; 023import java.beans.PropertyEditorManager; 024import java.lang.annotation.Annotation; 025import java.lang.reflect.Field; 026import java.lang.reflect.Method; 027import java.lang.reflect.Modifier; 028import java.lang.reflect.ParameterizedType; 029import java.lang.reflect.Type; 030import java.lang.reflect.WildcardType; 031import java.util.ArrayList; 032import java.util.Collection; 033import java.util.Collections; 034import java.util.HashSet; 035import java.util.LinkedHashMap; 036import java.util.LinkedHashSet; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.Map; 040import java.util.Map.Entry; 041import java.util.Queue; 042import java.util.Set; 043import java.util.WeakHashMap; 044 045import org.apache.commons.lang.StringUtils; 046import org.apache.log4j.Logger; 047import org.kuali.rice.krad.service.DataDictionaryService; 048import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 049import org.kuali.rice.krad.uif.UifConstants; 050import org.kuali.rice.krad.uif.lifecycle.ViewPostMetadata; 051import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry; 052import org.kuali.rice.krad.uif.view.ViewModel; 053import org.springframework.beans.BeanWrapper; 054import org.springframework.beans.PropertyEditorRegistry; 055import org.springframework.web.context.request.RequestAttributes; 056import org.springframework.web.context.request.RequestContextHolder; 057 058/** 059 * Utility methods to get/set property values and working with objects. 060 * 061 * @author Kuali Rice Team (rice.collab@kuali.org) 062 */ 063public final class ObjectPropertyUtils { 064 private static final Logger LOG = Logger.getLogger(ObjectPropertyUtils.class); 065 066 /** 067 * Internal metadata cache. 068 * 069 * <p> 070 * NOTE: WeakHashMap is used as the internal cache representation. Since class objects are used 071 * as the keys, this allows property descriptors to stay in cache until the class loader is 072 * unloaded, but will not prevent the class loader itself from unloading. PropertyDescriptor 073 * instances do not hold hard references back to the classes they refer to, so weak value 074 * maintenance is not necessary. 075 * </p> 076 */ 077 private static final Map<Class<?>, ObjectPropertyMetadata> METADATA_CACHE = Collections 078 .synchronizedMap(new WeakHashMap<Class<?>, ObjectPropertyMetadata>(2048)); 079 080 /** 081 * Get a mapping of property descriptors by property name for a bean class. 082 * 083 * @param beanClass The bean class. 084 * @return A mapping of all property descriptors for the bean class, by property name. 085 */ 086 public static Map<String, PropertyDescriptor> getPropertyDescriptors(Class<?> beanClass) { 087 return getMetadata(beanClass).propertyDescriptors; 088 } 089 090 /** 091 * Get a property descriptor from a class by property name. 092 * 093 * @param beanClass The bean class. 094 * @param propertyName The bean property name. 095 * @return The property descriptor named on the bean class. 096 */ 097 public static PropertyDescriptor getPropertyDescriptor(Class<?> beanClass, String propertyName) { 098 if (propertyName == null) { 099 throw new IllegalArgumentException("Null property name"); 100 } 101 102 PropertyDescriptor propertyDescriptor = getPropertyDescriptors(beanClass).get(propertyName); 103 if (propertyDescriptor != null) { 104 return propertyDescriptor; 105 } else { 106 throw new IllegalArgumentException("Property " + propertyName + " not found for bean " + beanClass); 107 } 108 } 109 110 /** 111 * Registers a default set of property editors for use with KRAD in a given property editor registry. 112 * 113 * @param registry property editor registry 114 */ 115 public static void registerPropertyEditors(PropertyEditorRegistry registry) { 116 DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService(); 117 Map<Class<?>, String> propertyEditorMap = dataDictionaryService.getPropertyEditorMap(); 118 119 if (propertyEditorMap == null) { 120 LOG.warn("No propertyEditorMap defined in data dictionary"); 121 return; 122 } 123 124 for (Entry<Class<?>, String> propertyEditorEntry : propertyEditorMap.entrySet()) { 125 PropertyEditor editor = (PropertyEditor) dataDictionaryService.getDataDictionary().getDictionaryPrototype( 126 propertyEditorEntry.getValue()); 127 registry.registerCustomEditor(propertyEditorEntry.getKey(), editor); 128 129 if (LOG.isDebugEnabled()) { 130 LOG.debug("registered " + propertyEditorEntry); 131 } 132 } 133 } 134 135 /** 136 * Gets the names of all readable properties for the bean class. 137 * 138 * @param beanClass The bean class. 139 * @return set of property names 140 */ 141 public static Set<String> getReadablePropertyNames(Class<?> beanClass) { 142 return getMetadata(beanClass).readMethods.keySet(); 143 } 144 145 /** 146 * Get the read method for a specific property on a bean class. 147 * 148 * @param beanClass The bean class. 149 * @param propertyName The property name. 150 * @return The read method for the property. 151 */ 152 public static Method getReadMethod(Class<?> beanClass, String propertyName) { 153 return getMetadata(beanClass).readMethods.get(propertyName); 154 } 155 156 /** 157 * Get the read method for a specific property on a bean class. 158 * 159 * @param beanClass The bean class. 160 * @param propertyName The property name. 161 * @return The read method for the property. 162 */ 163 public static Method getWriteMethod(Class<?> beanClass, String propertyName) { 164 return getMetadata(beanClass).writeMethods.get(propertyName); 165 } 166 167 /** 168 * Copy properties from a string map to an object. 169 * 170 * @param properties The string map. The keys of this map must be valid property path 171 * expressions in the context of the target object. The values are the string 172 * representations of the target bean properties. 173 * @param object The target object, to copy the property values to. 174 * @see ObjectPathExpressionParser 175 */ 176 public static void copyPropertiesToObject(Map<String, String> properties, Object object) { 177 for (Map.Entry<String, String> property : properties.entrySet()) { 178 setPropertyValue(object, property.getKey(), property.getValue()); 179 } 180 } 181 182 /** 183 * Get the type of a bean property. 184 * 185 * <p> 186 * Note that this method does not instantiate the bean class before performing introspection, so 187 * will not dynamic initialization behavior into account. When dynamic initialization is needed 188 * to accurate inspect the inferred property type, use {@link #getPropertyType(Object, String)} 189 * instead of this method. This method is, however, intended for use on the implementation 190 * class; to avoid instantiation simply to infer the property type, consider overriding the 191 * return type on the property read method. 192 * </p> 193 * 194 * @param beanClass The bean class. 195 * @param propertyPath A valid property path expression in the context of the bean class. 196 * @return The property type referred to by the provided bean class and property path. 197 * @see ObjectPathExpressionParser 198 */ 199 public static Class<?> getPropertyType(Class<?> beanClass, String propertyPath) { 200 try { 201 ObjectPropertyReference.setWarning(true); 202 return ObjectPropertyReference.resolvePath(null, beanClass, propertyPath, false).getPropertyType(); 203 } finally { 204 ObjectPropertyReference.setWarning(false); 205 } 206 } 207 208 /** 209 * Get the type of a bean property. 210 * 211 * @param object The bean instance. Use {@link #getPropertyType(Class, String)} to look up 212 * property types when an instance is not available. 213 * @param propertyPath A valid property path expression in the context of the bean. 214 * @return The property type referred to by the provided bean and property path. 215 * @see ObjectPathExpressionParser 216 */ 217 public static Class<?> getPropertyType(Object object, String propertyPath) { 218 try { 219 ObjectPropertyReference.setWarning(true); 220 return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false) 221 .getPropertyType(); 222 } finally { 223 ObjectPropertyReference.setWarning(false); 224 } 225 } 226 227 /** 228 * Gets the property names by property type, based on the read methods. 229 * 230 * @param bean The bean. 231 * @param propertyType The return type of the read method on the property. 232 * @return list of property names 233 */ 234 public static Set<String> getReadablePropertyNamesByType(Object bean, Class<?> propertyType) { 235 return getReadablePropertyNamesByType(bean.getClass(), propertyType); 236 } 237 238 /** 239 * Gets the property names by property type, based on the read methods. 240 * 241 * @param beanClass The bean class. 242 * @param propertyType The return type of the read method on the property. 243 * @return list of property names 244 */ 245 public static Set<String> getReadablePropertyNamesByType(Class<?> beanClass, Class<?> propertyType) { 246 return getMetadata(beanClass).getReadablePropertyNamesByType(propertyType); 247 } 248 249 /** 250 * Gets the property names by annotation type, based on the read methods. 251 * 252 * @param bean The bean. 253 * @param annotationType The type of an annotation on the return type. 254 * @return list of property names 255 */ 256 public static Set<String> getReadablePropertyNamesByAnnotationType(Object bean, 257 Class<? extends Annotation> annotationType) { 258 return getReadablePropertyNamesByAnnotationType(bean.getClass(), annotationType); 259 } 260 261 /** 262 * Gets the property names by annotation type, based on the read methods. 263 * 264 * @param beanClass The bean class. 265 * @param annotationType The type of an annotation on the return type. 266 * @return list of property names 267 */ 268 public static Set<String> getReadablePropertyNamesByAnnotationType( 269 Class<?> beanClass, Class<? extends Annotation> annotationType) { 270 return getMetadata(beanClass).getReadablePropertyNamesByAnnotationType(annotationType); 271 } 272 273 /** 274 * Gets the property names by collection type, based on the read methods. 275 * 276 * @param bean The bean. 277 * @param collectionType The type of elements in a collection or array. 278 * @return list of property names 279 */ 280 public static Set<String> getReadablePropertyNamesByCollectionType(Object bean, Class<?> collectionType) { 281 return getReadablePropertyNamesByCollectionType(bean.getClass(), collectionType); 282 } 283 284 /** 285 * Gets the property names for the given object that are writable 286 * 287 * @param bean object to get writable property names for 288 * @return set of property names 289 */ 290 public static Set<String> getWritablePropertyNames(Object bean) { 291 return getMetadata(bean.getClass()).getWritablePropertyNames(); 292 } 293 294 /** 295 * Gets the property names by collection type, based on the read methods. 296 * 297 * @param beanClass The bean class. 298 * @param collectionType The type of elements in a collection or array. 299 * @return list of property names 300 */ 301 public static Set<String> getReadablePropertyNamesByCollectionType(Class<?> beanClass, Class<?> collectionType) { 302 return getMetadata(beanClass).getReadablePropertyNamesByCollectionType(collectionType); 303 } 304 305 /** 306 * Look up a property value. 307 * 308 * @param <T> property type 309 * @param object The bean instance to look up a property value for. 310 * @param propertyPath A valid property path expression in the context of the bean. 311 * @return The value of the property referred to by the provided bean and property path. 312 * @see ObjectPathExpressionParser 313 */ 314 @SuppressWarnings("unchecked") 315 public static <T extends Object> T getPropertyValue(Object object, String propertyPath) { 316 boolean trace = ProcessLogger.isTraceActive() && object != null; 317 if (trace) { 318 // May be uncommented for debugging high execution count 319 // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":r:" + propertyPath, "", 1000); 320 ProcessLogger.countBegin("bean-property-read"); 321 } 322 323 try { 324 ObjectPropertyReference.setWarning(true); 325 326 return (T) ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).get(); 327 328 } catch (RuntimeException e) { 329 throw new IllegalArgumentException("Error getting property '" + propertyPath + "' from " + object, e); 330 } finally { 331 ObjectPropertyReference.setWarning(false); 332 if (trace) { 333 ProcessLogger.countEnd("bean-property-read", object.getClass().getSimpleName() + ":" + propertyPath); 334 } 335 } 336 337 } 338 339 /** 340 * Looks up a property value, then convert to text using a registered property editor. 341 * 342 * @param bean bean instance to look up a property value for 343 * @param path property path relative to the bean 344 * @return The property value, converted to text using a registered property editor. 345 */ 346 public static String getPropertyValueAsText(Object bean, String path) { 347 Object propertyValue = getPropertyValue(bean, path); 348 PropertyEditor editor = getPropertyEditor(bean, path); 349 350 if (editor == null) { 351 return propertyValue == null ? null : propertyValue.toString(); 352 } else { 353 editor.setValue(propertyValue); 354 return editor.getAsText(); 355 } 356 } 357 358 /** 359 * Gets the property editor registry configured for the active request. 360 */ 361 public static PropertyEditorRegistry getPropertyEditorRegistry() { 362 RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); 363 364 PropertyEditorRegistry registry = null; 365 if (attributes != null) { 366 registry = (PropertyEditorRegistry) attributes 367 .getAttribute(UifConstants.PROPERTY_EDITOR_REGISTRY, RequestAttributes.SCOPE_REQUEST); 368 } 369 370 return registry; 371 } 372 373 /** 374 * Convert to a primitive type if available. 375 * 376 * @param type The type to convert. 377 * @return A primitive type, if available, that corresponds to the type. 378 */ 379 public static Class<?> getPrimitiveType(Class<?> type) { 380 if (Byte.class.equals(type)) { 381 return Byte.TYPE; 382 } else if (Short.class.equals(type)) { 383 return Short.TYPE; 384 } else if (Integer.class.equals(type)) { 385 return Integer.TYPE; 386 } else if (Long.class.equals(type)) { 387 return Long.TYPE; 388 } else if (Boolean.class.equals(type)) { 389 return Boolean.TYPE; 390 } else if (Float.class.equals(type)) { 391 return Float.TYPE; 392 } else if (Double.class.equals(type)) { 393 return Double.TYPE; 394 } 395 396 return type; 397 } 398 399 /** 400 * Gets a property editor given a specific bean and property path. 401 * 402 * @param bean The bean instance. 403 * @param path The property path. 404 * @return property editor 405 */ 406 public static PropertyEditor getPropertyEditor(Object bean, String path) { 407 Class<?> propertyType = getPrimitiveType(getPropertyType(bean, path)); 408 409 PropertyEditor editor = null; 410 411 PropertyEditorRegistry registry = getPropertyEditorRegistry(); 412 if (registry != null) { 413 editor = registry.findCustomEditor(propertyType, path); 414 415 if (editor != null && editor != registry.findCustomEditor(propertyType, null)) { 416 return editor; 417 } 418 419 if (registry instanceof BeanWrapper 420 && bean == ((BeanWrapper) registry).getWrappedInstance() 421 && (bean instanceof ViewModel)) { 422 423 ViewModel viewModel = (ViewModel) bean; 424 ViewPostMetadata viewPostMetadata = viewModel.getViewPostMetadata(); 425 PropertyEditor editorFromView = viewPostMetadata == null ? null : viewPostMetadata.getFieldEditor(path); 426 427 if (editorFromView != null) { 428 registry.registerCustomEditor(propertyType, path, editorFromView); 429 editor = registry.findCustomEditor(propertyType, path); 430 } 431 } 432 } 433 434 if (editor != null) { 435 return editor; 436 } 437 438 return getPropertyEditor(propertyType); 439 } 440 441 /** 442 * Get a property editor given a property type. 443 * 444 * @param propertyType The property type to look up an editor for. 445 * @param path The property path, if applicable. 446 * @return property editor 447 */ 448 public static PropertyEditor getPropertyEditor(Class<?> propertyType) { 449 PropertyEditorRegistry registry = getPropertyEditorRegistry(); 450 PropertyEditor editor = null; 451 452 if (registry != null) { 453 editor = registry.findCustomEditor(propertyType, null); 454 } else { 455 456 DataDictionaryService dataDictionaryService = KRADServiceLocatorWeb.getDataDictionaryService(); 457 Map<Class<?>, String> editorMap = dataDictionaryService.getPropertyEditorMap(); 458 String editorPrototypeName = editorMap == null ? null : editorMap.get(propertyType); 459 460 if (editorPrototypeName != null) { 461 editor = (PropertyEditor) dataDictionaryService.getDataDictionary().getDictionaryPrototype(editorPrototypeName); 462 } 463 } 464 465 if (editor == null && propertyType != null) { 466 // Fall back to default beans lookup 467 editor = PropertyEditorManager.findEditor(propertyType); 468 } 469 470 return editor; 471 } 472 473 /** 474 * Initialize a property value. 475 * 476 * <p> 477 * Upon returning from this method, the property referred to by the provided bean and property 478 * path will have been initialized with a default instance of the indicated property type. 479 * </p> 480 * 481 * @param object The bean instance to initialize a property value for. 482 * @param propertyPath A valid property path expression in the context of the bean. 483 * @see #getPropertyType(Object, String) 484 * @see #setPropertyValue(Object, String, Object) 485 * @see ObjectPathExpressionParser 486 */ 487 public static void initializeProperty(Object object, String propertyPath) { 488 Class<?> propertyType = getPropertyType(object, propertyPath); 489 try { 490 setPropertyValue(object, propertyPath, propertyType.newInstance()); 491 } catch (InstantiationException e) { 492 // just set the value to null 493 setPropertyValue(object, propertyPath, null); 494 } catch (IllegalAccessException e) { 495 throw new IllegalArgumentException("Unable to set new instance for property: " + propertyPath, e); 496 } 497 } 498 499 /** 500 * Modify a property value. 501 * 502 * <p> 503 * Upon returning from this method, the property referred to by the provided bean and property 504 * path will have been populated with property value provided. If the propertyValue does not 505 * match the type of the indicated property, then type conversion will be attempted using 506 * {@link PropertyEditorManager}. 507 * </p> 508 * 509 * @param object The bean instance to initialize a property value for. 510 * @param propertyPath A valid property path expression in the context of the bean. 511 * @param propertyValue The value to populate value in the property referred to by the provided 512 * bean and property path. 513 * @see ObjectPathExpressionParser 514 * @throws RuntimeException If the property path is not valid in the context of the bean 515 * provided. 516 */ 517 public static void setPropertyValue(Object object, String propertyPath, Object propertyValue) { 518 if (ProcessLogger.isTraceActive() && object != null) { 519 // May be uncommented for debugging high execution count 520 // ProcessLogger.ntrace(object.getClass().getSimpleName() + ":w:" + propertyPath + ":", "", 1000); 521 ProcessLogger.countBegin("bean-property-write"); 522 } 523 524 try { 525 ObjectPropertyReference.setWarning(true); 526 527 ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, true).set(propertyValue); 528 529 } catch (RuntimeException e) { 530 throw new IllegalArgumentException( 531 "Error setting property '" + propertyPath + "' on " + object + " with " + propertyValue, e); 532 } finally { 533 ObjectPropertyReference.setWarning(false); 534 535 if (ProcessLogger.isTraceActive() && object != null) { 536 ProcessLogger.countEnd("bean-property-write", object.getClass().getSimpleName() + ":" + propertyPath); 537 } 538 } 539 540 } 541 542 /** 543 * Modify a property value. 544 * 545 * <p> 546 * Upon returning from this method, the property referred to by the provided bean and property 547 * path will have been populated with property value provided. If the propertyValue does not 548 * match the type of the indicated property, then type conversion will be attempted using 549 * {@link PropertyEditorManager}. 550 * </p> 551 * 552 * @param object The bean instance to initialize a property value for. 553 * @param propertyPath A property path expression in the context of the bean. 554 * @param propertyValue The value to populate value in the property referred to by the provided 555 * bean and property path. 556 * @param ignoreUnknown True if invalid property values should be ignored, false to throw a 557 * RuntimeException if the property reference is invalid. 558 * @see ObjectPathExpressionParser 559 */ 560 public static void setPropertyValue(Object object, String propertyPath, Object propertyValue, boolean ignoreUnknown) { 561 try { 562 setPropertyValue(object, propertyPath, propertyValue); 563 } catch (RuntimeException e) { 564 // only throw exception if they have indicated to not ignore unknown 565 if (!ignoreUnknown) { 566 throw e; 567 } 568 if (LOG.isTraceEnabled()) { 569 LOG.trace("Ignoring exception thrown during setting of property '" + propertyPath + "': " 570 + e.getLocalizedMessage()); 571 } 572 } 573 } 574 575 /** 576 * Determine if a property is readable. 577 * 578 * @param object The bean instance to initialize a property value for. 579 * @param propertyPath A property path expression in the context of the bean. 580 * @return True if the path expression resolves to a valid readable property reference in the 581 * context of the bean provided. 582 */ 583 public static boolean isReadableProperty(Object object, String propertyPath) { 584 return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canRead(); 585 } 586 587 /** 588 * Determine if a property is writable. 589 * 590 * @param object The bean instance to initialize a property value for. 591 * @param propertyPath A property path expression in the context of the bean. 592 * @return True if the path expression resolves to a valid writable property reference in the 593 * context of the bean provided. 594 */ 595 public static boolean isWritableProperty(Object object, String propertyPath) { 596 return ObjectPropertyReference.resolvePath(object, object.getClass(), propertyPath, false).canWrite(); 597 } 598 599 /** 600 * Returns an List of {@code Field} objects reflecting all the fields 601 * declared by the class or interface represented by this 602 * {@code Class} object. This includes public, protected, default 603 * (package) access, and private fields, and includes inherited fields. 604 * 605 * @param fields A list of {@code Field} objects which gets returned. 606 * @param type Type of class or interface for which fields are returned. 607 * @param stopAt The Superclass upto which the inherited fields are to be included 608 * @return List of all fields 609 */ 610 public static List<Field> getAllFields(List<Field> fields, Class<?> type, Class<?> stopAt) { 611 for (Field field : type.getDeclaredFields()) { 612 fields.add(field); 613 } 614 615 if (type.getSuperclass() != null && !type.getName().equals(stopAt.getName())) { 616 fields = getAllFields(fields, type.getSuperclass(), stopAt); 617 } 618 619 return fields; 620 } 621 622 /** 623 * Get the best known component type for a generic type. 624 * 625 * <p> 626 * When the type is not parameterized or has no explicitly defined parameters, {@link Object} is 627 * returned. 628 * </p> 629 * 630 * <p> 631 * When the type has multiple parameters, the right-most parameter is considered the component 632 * type. This facilitates identifying the value type of a Map. 633 * </p> 634 * 635 * @param type The generic collection or map type. 636 * @return component or value type, resolved from the generic type 637 */ 638 public static Type getComponentType(Type type) { 639 if (!(type instanceof ParameterizedType)) { 640 return Object.class; 641 } 642 643 ParameterizedType parameterizedType = (ParameterizedType) type; 644 Type[] params = parameterizedType.getActualTypeArguments(); 645 if (params.length == 0) { 646 return Object.class; 647 } 648 649 Type valueType = params[params.length - 1]; 650 return valueType; 651 } 652 653 /** 654 * Get the upper bound of a generic type. 655 * 656 * <p> 657 * When the type is a class, the class is returned. 658 * </p> 659 * 660 * <p> 661 * When the type is a wildcard, and the upper bound is a class, the upper bound of the wildcard 662 * is returned. 663 * </p> 664 * 665 * <p> 666 * If the type has not been explicitly defined at compile time, {@link Object} is returned. 667 * </p> 668 * 669 * @param valueType The generic collection or map type. 670 * @return component or value type, resolved from the generic type 671 */ 672 public static Class<?> getUpperBound(Type valueType) { 673 if (valueType instanceof WildcardType) { 674 Type[] upperBounds = ((WildcardType) valueType).getUpperBounds(); 675 676 if (upperBounds.length >= 1) { 677 valueType = upperBounds[0]; 678 } 679 } 680 681 if (valueType instanceof ParameterizedType) { 682 valueType = ((ParameterizedType) valueType).getRawType(); 683 } 684 685 if (valueType instanceof Class) { 686 return (Class<?>) valueType; 687 } 688 689 return Object.class; 690 } 691 692 /** 693 * Locate the generic type declaration for a given target class in the generic type hierarchy of 694 * the source class. 695 * 696 * @param sourceClass The class representing the generic type hierarchy to scan. 697 * @param targetClass The class representing the generic type declaration to locate within the 698 * source class' hierarchy. 699 * @return The generic type representing the target class within the source class' generic 700 * hierarchy. 701 */ 702 public static Type findGenericType(Class<?> sourceClass, Class<?> targetClass) { 703 if (!targetClass.isAssignableFrom(sourceClass)) { 704 throw new IllegalArgumentException(targetClass + " is not assignable from " + sourceClass); 705 } 706 707 if (sourceClass.equals(targetClass)) { 708 return sourceClass; 709 } 710 711 @SuppressWarnings("unchecked") 712 Queue<Type> typeQueue = RecycleUtils.getInstance(LinkedList.class); 713 typeQueue.offer(sourceClass); 714 while (!typeQueue.isEmpty()) { 715 Type type = typeQueue.poll(); 716 717 Class<?> upperBound = getUpperBound(type); 718 if (targetClass.equals(upperBound)) { 719 return type; 720 } 721 722 Type genericSuper = upperBound.getGenericSuperclass(); 723 if (genericSuper != null) { 724 typeQueue.offer(genericSuper); 725 } 726 727 Type[] genericInterfaces = upperBound.getGenericInterfaces(); 728 for (int i=0; i<genericInterfaces.length; i++) { 729 if (genericInterfaces[i] != null) { 730 typeQueue.offer(genericInterfaces[i]); 731 } 732 } 733 } 734 735 throw new IllegalStateException(targetClass + " is assignable from " + sourceClass 736 + " but could not be found in the generic type hierarchy"); 737 } 738 739 /** 740 * Split parse path entry for supporting {@link ObjectPropertyUtils#splitPropertyPath(String)}. 741 * 742 * @author Kuali Rice Team (rice.collab@kuali.org) 743 */ 744 private static class SplitPropertyPathEntry implements PathEntry { 745 746 /** 747 * Invoked at each path separator on the path. 748 * 749 * <p> 750 * Note that {@link ObjectPathExpressionParser} strips quotes and brackets then treats 751 * list/map references as property names. However 752 * {@link ObjectPropertyUtils#splitPropertyPath(String)} expects that a list/map reference 753 * will be part of the path expression, as a reference to a specific element in a list or 754 * map related to the bean. Therefore, this method needs to rejoin these list/map references 755 * before returning. 756 * </p> 757 * 758 * @param parentPath The portion of the path leading up to the current node. 759 * @param node The list of property names in the path. 760 * @param next The next property name being parsed. 761 * 762 * {@inheritDoc} 763 */ 764 @Override 765 public List<String> parse(String parentPath, Object node, String next) { 766 if (next == null) { 767 return new ArrayList<String>(); 768 } 769 770 @SuppressWarnings("unchecked") 771 List<String> rv = (List<String>) node; 772 // First node, or no path separator in parent path. 773 if (rv.isEmpty()) { 774 rv.add(next); 775 return rv; 776 } 777 778 rejoinTrailingIndexReference(rv, parentPath); 779 rv.add(next); 780 781 return rv; 782 } 783 } 784 785 private static final SplitPropertyPathEntry SPLIT_PROPERTY_PATH_ENTRY = new SplitPropertyPathEntry(); 786 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 787 788 /** 789 * Helper method for splitting property paths with bracketed index references. 790 * 791 * <p> 792 * Since the parser treats index references as separate tokens, they need to be rejoined in order 793 * to be maintained as a single indexed property reference. This method handles that operation. 794 * </p> 795 * 796 * @param tokenList The list of tokens being parsed. 797 * @param path The portion of the path expression represented by the token list. 798 */ 799 private static void rejoinTrailingIndexReference(List<String> tokenList, String path) { 800 int lastIndex = tokenList.size() - 1; 801 String lastToken = tokenList.get(lastIndex); 802 String lastParentToken = path.substring(path.lastIndexOf('.') + 1); 803 804 if (!lastToken.equals(lastParentToken) && lastIndex > 0) { 805 806 // read back one more token and "concatenate" by 807 // recreating the subexpression as a substring of 808 // the parent path 809 String prevToken = tokenList.get(--lastIndex); 810 811 // parent path index of last prevToken. 812 int iopt = path.lastIndexOf(prevToken, path.lastIndexOf(lastToken)); 813 814 String fullToken = path.substring(iopt); 815 tokenList.remove(lastIndex); // remove first entry 816 tokenList.set(lastIndex, fullToken); // replace send with concatenation 817 } 818 } 819 820 /** 821 * Splits the given property path into a string of property names that make up the path. 822 * 823 * @param path property path to split 824 * @return string array of names, starting from the top parent 825 * @see SplitPropertyPathEntry#parse(String, Object, String) 826 */ 827 public static String[] splitPropertyPath(String path) { 828 List<String> split = ObjectPathExpressionParser.parsePathExpression(null, path, SPLIT_PROPERTY_PATH_ENTRY); 829 if (split == null || split.isEmpty()) { 830 return EMPTY_STRING_ARRAY; 831 } 832 833 rejoinTrailingIndexReference(split, path); 834 835 return split.toArray(new String[split.size()]); 836 } 837 838 /** 839 * Returns the tail of a given property path (if nested, the nested path). 840 * 841 * <p>For example, if path is "nested1.foo", this will return "foo". If path is just "foo", "foo" will be 842 * returned.</p> 843 * 844 * @param path path to return tail for 845 * @return String tail of path 846 */ 847 public static String getPathTail(String path) { 848 String[] propertyPaths = splitPropertyPath(path); 849 850 return propertyPaths[propertyPaths.length - 1]; 851 } 852 853 /** 854 * Removes the tail of the path from the return path. 855 * 856 * <p>For example, if path is "nested1.foo", this will return "nested1". If path is just "foo", "" will be 857 * returned.</p> 858 * 859 * @param path path to remove tail from 860 * @return String path with tail removed (may be empty string) 861 */ 862 public static String removePathTail(String path) { 863 String[] propertyPaths = splitPropertyPath(path); 864 865 return StringUtils.join(propertyPaths, ".", 0, propertyPaths.length - 1); 866 } 867 868 /** 869 * Removes any collection references from a property path, making it more useful for referring 870 * to metadata related to the property. 871 * @param path A property path expression. 872 * @return The path, with collection references removed. 873 */ 874 public static String getCanonicalPath(String path) { 875 if (path == null || path.indexOf('[') == -1) { 876 return path; 877 } 878 879 // The path has at least one left bracket, so will need to be modified 880 // copy it to a mutable StringBuilder 881 StringBuilder pathBuilder = new StringBuilder(path); 882 883 int bracketCount = 0; 884 int leftBracketPos = -1; 885 for (int i = 0; i < pathBuilder.length(); i++) { 886 char c = pathBuilder.charAt(i); 887 888 if (c == '[') { 889 bracketCount++; 890 if (bracketCount == 1) 891 leftBracketPos = i; 892 } 893 894 if (c == ']') { 895 bracketCount--; 896 897 if (bracketCount < 0) { 898 throw new IllegalArgumentException("Unmatched ']' at " + i + " " + pathBuilder); 899 } 900 901 if (bracketCount == 0) { 902 pathBuilder.delete(leftBracketPos, i + 1); 903 i -= i + 1 - leftBracketPos; 904 leftBracketPos = -1; 905 } 906 } 907 } 908 909 if (bracketCount > 0) { 910 throw new IllegalArgumentException("Unmatched '[' at " + leftBracketPos + " " + pathBuilder); 911 } 912 913 return pathBuilder.toString(); 914 } 915 916 /** 917 * Private constructor - utility class only. 918 */ 919 private ObjectPropertyUtils() {} 920 921 /** 922 * Infer the read method based on method name. 923 * 924 * @param beanClass The bean class. 925 * @param propertyName The property name. 926 * @return The read method for the property. 927 */ 928 private static Method getReadMethodByName(Class<?> beanClass, String propertyName) { 929 930 try { 931 return beanClass.getMethod("get" + Character.toUpperCase(propertyName.charAt(0)) 932 + propertyName.substring(1)); 933 } catch (SecurityException e) { 934 // Ignore 935 } catch (NoSuchMethodException e) { 936 // Ignore 937 } 938 939 try { 940 Method readMethod = beanClass.getMethod("is" 941 + Character.toUpperCase(propertyName.charAt(0)) 942 + propertyName.substring(1)); 943 944 if (readMethod.getReturnType() == Boolean.class || readMethod.getReturnType() == Boolean.TYPE) { 945 return readMethod; 946 } 947 } catch (SecurityException e) { 948 // Ignore 949 } catch (NoSuchMethodException e) { 950 // Ignore 951 } 952 953 return null; 954 } 955 956 /** 957 * Get the cached metadata for a bean class. 958 * 959 * @param beanClass The bean class. 960 * @return cached metadata for beanClass 961 */ 962 private static ObjectPropertyMetadata getMetadata(Class<?> beanClass) { 963 ObjectPropertyMetadata metadata = METADATA_CACHE.get(beanClass); 964 965 if (metadata == null) { 966 metadata = new ObjectPropertyMetadata(beanClass); 967 METADATA_CACHE.put(beanClass, metadata); 968 } 969 970 return metadata; 971 } 972 973 /** 974 * Stores property metadata related to a bean class, for reducing introspection and reflection 975 * overhead. 976 * 977 * @author Kuali Rice Team (rice.collab@kuali.org) 978 */ 979 private static class ObjectPropertyMetadata { 980 981 private final Map<String, PropertyDescriptor> propertyDescriptors; 982 private final Map<String, Method> readMethods; 983 private final Map<String, Method> writeMethods; 984 private final Map<Class<?>, Set<String>> readablePropertyNamesByPropertyType = 985 Collections.synchronizedMap(new WeakHashMap<Class<?>, Set<String>>()); 986 private final Map<Class<?>, Set<String>> readablePropertyNamesByAnnotationType = 987 Collections.synchronizedMap(new WeakHashMap<Class<?>, Set<String>>()); 988 private final Map<Class<?>, Set<String>> readablePropertyNamesByCollectionType = 989 Collections.synchronizedMap(new WeakHashMap<Class<?>, Set<String>>()); 990 991 /** 992 * Gets the property names by type, based on the read methods. 993 * 994 * @param propertyType The return type of the read method on the property. 995 * @return list of property names 996 */ 997 private Set<String> getReadablePropertyNamesByType(Class<?> propertyType) { 998 Set<String> propertyNames = readablePropertyNamesByPropertyType.get(propertyType); 999 if (propertyNames != null) { 1000 return propertyNames; 1001 } 1002 1003 propertyNames = new LinkedHashSet<String>(); 1004 for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) { 1005 Method readMethod = readMethodEntry.getValue(); 1006 if (readMethod != null && propertyType.isAssignableFrom(readMethod.getReturnType())) { 1007 propertyNames.add(readMethodEntry.getKey()); 1008 } 1009 } 1010 1011 propertyNames = Collections.unmodifiableSet(propertyNames); 1012 readablePropertyNamesByPropertyType.put(propertyType, propertyNames); 1013 1014 return propertyNames; 1015 } 1016 1017 /** 1018 * Gets the property names by annotation type, based on the read methods. 1019 * 1020 * @param annotationType The type of an annotation on the return type. 1021 * @return list of property names 1022 */ 1023 private Set<String> getReadablePropertyNamesByAnnotationType(Class<? extends Annotation> annotationType) { 1024 Set<String> propertyNames = readablePropertyNamesByAnnotationType.get(annotationType); 1025 if (propertyNames != null) { 1026 return propertyNames; 1027 } 1028 1029 propertyNames = new LinkedHashSet<String>(); 1030 for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) { 1031 Method readMethod = readMethodEntry.getValue(); 1032 if (readMethod != null && readMethod.isAnnotationPresent(annotationType)) { 1033 propertyNames.add(readMethodEntry.getKey()); 1034 } 1035 } 1036 1037 propertyNames = Collections.unmodifiableSet(propertyNames); 1038 readablePropertyNamesByPropertyType.put(annotationType, propertyNames); 1039 1040 return propertyNames; 1041 } 1042 1043 /** 1044 * Gets the property names by collection type, based on the read methods. 1045 * 1046 * @param collectionType The type of elements in a collection or array. 1047 * @return list of property names 1048 */ 1049 private Set<String> getReadablePropertyNamesByCollectionType(Class<?> collectionType) { 1050 Set<String> propertyNames = readablePropertyNamesByCollectionType.get(collectionType); 1051 if (propertyNames != null) { 1052 return propertyNames; 1053 } 1054 1055 propertyNames = new LinkedHashSet<String>(); 1056 for (Entry<String, Method> readMethodEntry : readMethods.entrySet()) { 1057 Method readMethod = readMethodEntry.getValue(); 1058 if (readMethod == null) { 1059 continue; 1060 } 1061 1062 Class<?> propertyClass = readMethod.getReturnType(); 1063 if (propertyClass.isArray() && collectionType.isAssignableFrom(propertyClass.getComponentType())) { 1064 propertyNames.add(readMethodEntry.getKey()); 1065 continue; 1066 } 1067 1068 boolean isCollection = Collection.class.isAssignableFrom(propertyClass); 1069 boolean isMap = Map.class.isAssignableFrom(propertyClass); 1070 if (!isCollection && !isMap) { 1071 continue; 1072 } 1073 1074 if (collectionType.equals(Object.class)) { 1075 propertyNames.add(readMethodEntry.getKey()); 1076 continue; 1077 } 1078 1079 Type propertyType = readMethodEntry.getValue().getGenericReturnType(); 1080 if (propertyType instanceof ParameterizedType) { 1081 ParameterizedType parameterizedType = (ParameterizedType) propertyType; 1082 Type valueType = parameterizedType.getActualTypeArguments()[isCollection ? 0 : 1]; 1083 1084 if (valueType instanceof WildcardType) { 1085 Type[] upperBounds = ((WildcardType) valueType).getUpperBounds(); 1086 1087 if (upperBounds.length >= 1) { 1088 valueType = upperBounds[0]; 1089 } 1090 } 1091 1092 if (valueType instanceof Class && 1093 collectionType.isAssignableFrom((Class<?>) valueType)) { 1094 propertyNames.add(readMethodEntry.getKey()); 1095 } 1096 } 1097 } 1098 1099 propertyNames = Collections.unmodifiableSet(propertyNames); 1100 readablePropertyNamesByCollectionType.put(collectionType, propertyNames); 1101 1102 return propertyNames; 1103 } 1104 1105 /** 1106 * Gets the property names that are writable for the metadata class. 1107 * 1108 * @return set of writable property names 1109 */ 1110 private Set<String> getWritablePropertyNames() { 1111 Set<String> writablePropertyNames = new HashSet<String>(); 1112 1113 for (Entry<String, Method> writeMethodEntry : writeMethods.entrySet()) { 1114 writablePropertyNames.add(writeMethodEntry.getKey()); 1115 } 1116 1117 return writablePropertyNames; 1118 } 1119 1120 /** 1121 * Creates a new metadata wrapper for a bean class. 1122 * 1123 * @param beanClass The bean class. 1124 */ 1125 private ObjectPropertyMetadata(Class<?> beanClass) { 1126 if (beanClass == null) { 1127 throw new RuntimeException("Class to retrieve property from was null"); 1128 } 1129 1130 BeanInfo beanInfo; 1131 try { 1132 beanInfo = Introspector.getBeanInfo(beanClass); 1133 } catch (IntrospectionException e) { 1134 LOG.warn("Bean Info not found for bean " + beanClass, e); 1135 beanInfo = null; 1136 } 1137 1138 Map<String, PropertyDescriptor> mutablePropertyDescriptorMap = new LinkedHashMap<String, PropertyDescriptor>(); 1139 Map<String, Method> mutableReadMethodMap = new LinkedHashMap<String, Method>(); 1140 Map<String, Method> mutableWriteMethodMap = new LinkedHashMap<String, Method>(); 1141 1142 if (beanInfo != null) { 1143 for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) { 1144 String propertyName = propertyDescriptor.getName(); 1145 1146 mutablePropertyDescriptorMap.put(propertyName, propertyDescriptor); 1147 Method readMethod = propertyDescriptor.getReadMethod(); 1148 if (readMethod == null) { 1149 readMethod = getReadMethodByName(beanClass, propertyName); 1150 } 1151 1152 // checking for bridge methods http://www.znetdevelopment.com/blogs/2012/04/11/java-bean-introspector-and-covariantgeneric-returns/ 1153 if (readMethod != null && readMethod.isBridge()) { 1154 readMethod = getCorrectedReadMethod(beanClass, readMethod); 1155 } 1156 1157 mutableReadMethodMap.put(propertyName, readMethod); 1158 1159 Method writeMethod = propertyDescriptor.getWriteMethod(); 1160 assert writeMethod == null 1161 || (writeMethod.getParameterTypes().length == 1 && writeMethod.getParameterTypes()[0] != null) : writeMethod; 1162 mutableWriteMethodMap.put(propertyName, writeMethod); 1163 } 1164 } 1165 1166 propertyDescriptors = Collections.unmodifiableMap(mutablePropertyDescriptorMap); 1167 readMethods = Collections.unmodifiableMap(mutableReadMethodMap); 1168 writeMethods = Collections.unmodifiableMap(mutableWriteMethodMap); 1169 } 1170 1171 /** 1172 * Workaround for a JDK6 Introspector issue (see KULRICE-12334) that results in getters for interface types 1173 * being returned instead of same named getters for concrete implementation types (depending on the Method order 1174 * returned by reflection on the beanClass. 1175 * 1176 * <p>Note that this doesn't cover all cases, see ObjectPropertyUtilsTest.testGetterInInterfaceOrSuperHasWiderType 1177 * for details.</p> 1178 * 1179 * @param beanClass the class of the bean being inspected 1180 * @param readMethod the read method being double-checked 1181 * @return the corrected read Method 1182 */ 1183 private Method getCorrectedReadMethod(Class<?> beanClass, Method readMethod) { 1184 if (readMethod != null 1185 && !readMethod.getReturnType().isPrimitive() 1186 && isAbstractClassOrInterface(readMethod.getReturnType())) { 1187 1188 Method implReadMethod = null; 1189 1190 try { 1191 implReadMethod = beanClass.getMethod(readMethod.getName(), readMethod.getParameterTypes()); 1192 } catch (NoSuchMethodException e) { 1193 // if readMethod != null, this should not happen according to the javadocs for Class.getMethod() 1194 } 1195 1196 if (implReadMethod != null && isSubClass(implReadMethod.getReturnType(), readMethod.getReturnType())) { 1197 return implReadMethod; 1198 } 1199 } 1200 1201 return readMethod; 1202 } 1203 1204 // we assume a non-null arg 1205 private boolean isAbstractClassOrInterface(Class<?> clazz) { 1206 return clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers()); 1207 } 1208 1209 // we assume non-null args 1210 private boolean isSubClass(Class<?> childClassCandidate, Class<?> parentClassCandidate) { 1211 // if A != B and A >= B then A > B 1212 return parentClassCandidate != childClassCandidate && parentClassCandidate.isAssignableFrom(childClassCandidate); 1213 } 1214 } 1215}