View Javadoc
1   /**
2    * Copyright 2005-2015 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.util;
17  
18  import java.beans.PropertyEditor;
19  import java.lang.reflect.Array;
20  import java.lang.reflect.InvocationTargetException;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.ParameterizedType;
23  import java.lang.reflect.Type;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.log4j.Logger;
28  import org.kuali.rice.krad.datadictionary.Copyable;
29  import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry;
30  import org.kuali.rice.krad.util.KRADUtils;
31  
32  /**
33   * Represents a property reference in a path expression, for use in implementing
34   * {@link ObjectPathExpressionParser.PathEntry}.
35   * 
36   * <p>
37   * This class defers the actual resolution of property references nodes in a path expression until
38   * the transition between parse nodes. This facilitates traversal to the final node in the path.
39   * </p>
40   * 
41   * @author Kuali Rice Team (rice.collab@kuali.org)
42   * @version 2.4
43   * @see ObjectPathExpressionParser#parsePathExpression(Object, String, PathEntry)
44   */
45  public class ObjectPropertyReference {
46  
47      /**
48       * Primitive default values
49       */
50      private static final boolean DEFAULT_BOOLEAN = false;
51      private static final byte DEFAULT_BYTE = 0;
52      private static final short DEFAULT_SHORT = 0;
53      private static final int DEFAULT_INT = 0;
54      private static final long DEFAULT_LONG = 0L;
55      private static final float DEFAULT_FLOAT = 0.0f;
56      private static final double DEFAULT_DOUBLE = 0.0d;
57      private static final char DEFAULT_CHAR = '\u0000';
58  
59      /**
60       * Log4j logger.
61       */
62      private static final Logger LOG = Logger.getLogger(ObjectPropertyReference.class);
63  
64      /**
65       * Reference for single use.
66       */
67      private static final ThreadLocal<ObjectPropertyReference> TL_BUILDER_REF = new ThreadLocal<ObjectPropertyReference>();
68  
69      /**
70       * Reference for single use.
71       */
72      private static final ThreadLocal<Boolean> TL_WARN = new ThreadLocal<Boolean>();
73  
74      /**
75       * Singleton reference path entry, to be used when parsing for looking up a bean property
76       * without modifying.
77       */
78      private static final ReferencePathEntry LOOKUP_REF_PATH_ENTRY = new ReferencePathEntry(false);
79  
80      /**
81       * Singleton reference path entry, to be used when parsing for modifying the bean property.
82       */
83      private static final ReferencePathEntry MUTATE_REF_PATH_ENTRY = new ReferencePathEntry(true);
84  
85      /**
86       * Internal path entry implementation.
87       */
88      private static final class ReferencePathEntry implements PathEntry {
89  
90          /**
91           * Determines whether or not {@link ObjectPropertyReference#initialize(Object, Class)}
92           * should be used to create an object when a property reference resolves to null.
93           */
94          private final boolean grow;
95  
96          /**
97           * Internal private constructor.
98           */
99          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 }