View Javadoc
1   /**
2    * Copyright 2005-2014 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.beans.PropertyEditorManager;
20  import java.lang.reflect.Array;
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.ParameterizedType;
24  import java.lang.reflect.Type;
25  import java.util.List;
26  import java.util.Map;
27  
28  import org.apache.log4j.Logger;
29  import org.kuali.rice.krad.datadictionary.Copyable;
30  import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry;
31  import org.kuali.rice.krad.util.KRADUtils;
32  
33  /**
34   * Represents a property reference in a path expression, for use in implementing
35   * {@link ObjectPathExpressionParser.PathEntry}.
36   * 
37   * <p>
38   * This class defers the actual resolution of property references nodes in a path expression until
39   * the transition between parse nodes. This facilitates traversal to the final node in the path.
40   * </p>
41   * 
42   * @author Kuali Rice Team (rice.collab@kuali.org)
43   * @version 2.4
44   * @see ObjectPathExpressionParser#parsePathExpression(Object, String, PathEntry)
45   */
46  public class ObjectPropertyReference {
47  
48      /**
49       * Log4j logger.
50       */
51      private static final Logger LOG = Logger.getLogger(ObjectPropertyReference.class);
52  
53      /**
54       * Reference for single use.
55       */
56      private static final ThreadLocal<ObjectPropertyReference> TL_BUILDER_REF = new ThreadLocal<ObjectPropertyReference>();
57  
58      /**
59       * Reference for single use.
60       */
61      private static final ThreadLocal<Boolean> TL_WARN = new ThreadLocal<Boolean>();
62  
63      /**
64       * Singleton reference path entry, to be used when parsing for looking up a bean property
65       * without modifying.
66       */
67      private static final ReferencePathEntry LOOKUP_REF_PATH_ENTRY = new ReferencePathEntry(false);
68  
69      /**
70       * Singleton reference path entry, to be used when parsing for modifying the bean property.
71       */
72      private static final ReferencePathEntry MUTATE_REF_PATH_ENTRY = new ReferencePathEntry(true);
73  
74      /**
75       * Internal path entry implementation.
76       */
77      private static final class ReferencePathEntry implements PathEntry {
78  
79          /**
80           * Determines whether or not {@link ObjectPropertyReference#initialize(Object, Class)}
81           * should be used to create an object when a property reference resolves to null.
82           */
83          private final boolean grow;
84  
85          /**
86           * Internal private constructor.
87           */
88          private ReferencePathEntry(boolean grow) {
89              this.grow = grow;
90          }
91  
92          /**
93           * Transition from one path entry to the next while parsing a bean property expression.
94           * 
95           * {@inheritDoc}
96           */
97          @Override
98          public Object parse(String parentPath, Object node, String next) {
99              ObjectPropertyReference current = (ObjectPropertyReference) node;
100 
101             // At the initial parse node, copy to a new property reference.
102             // Otherwise, we will modify the existing reference to reduce object construction
103             // due to object reference parsing.
104             if (next == null) {
105                 ObjectPropertyReference resolved = new ObjectPropertyReference();
106                 resolved.bean = current.bean;
107                 resolved.beanClass = current.beanClass;
108                 resolved.beanType = current.beanType;
109                 resolved.name = null;
110                 return resolved;
111             }
112 
113             // Get the property type and value from the current node reference.
114             // These will become the bean and bean class after transition.
115             Class<?> beanClass = current.getPropertyType();
116             Object bean = current.get();
117             if (bean instanceof Copyable) {
118                 bean = CopyUtils.unwrap((Copyable) bean);
119                 if (!beanClass.isInstance(bean)) {
120                     beanClass = bean.getClass();
121                 }
122             }
123 
124             // Determine the parameterized property type, if applicable.
125             // This facilitates type conversion when setting/getting typed collections.
126             Type beanType;
127             Method readMethod = ObjectPropertyUtils.getReadMethod(current.getImplClass(), current.name);
128             if (readMethod == null) {
129                 beanType = beanClass;
130             } else {
131                 beanType = readMethod.getGenericReturnType();
132             }
133 
134             // When parsing for a set() operation, automatically initialize values.
135             if (grow) {
136                 Object newBean = initialize(bean, beanClass);
137                 if (newBean != bean) {
138                     current.set(newBean);
139                     Object verify;
140                     assert (verify = current.get()) == newBean : verify + " != " + newBean;
141                     bean = newBean;
142                 }
143             }
144 
145             // Modify the current reference to represent the next parse node, and return.
146             current.bean = bean;
147             current.beanClass = beanClass;
148             current.beanType = beanType;
149             current.name = next;
150 
151             return current;
152         }
153     }
154 
155     /**
156      * Get the property value for a specific bean property of a known bean class.
157      * 
158      * @param propertyValue existing property value
159      * @param propertyType the property type to initialize if the existing value is null
160      * @return The property value for the specific bean property on the given bean.
161      */
162     private static Object initialize(Object propertyValue, Class<?> propertyType) {
163         Object returnValue = propertyValue;
164         
165         if (propertyValue == null) {
166             if (List.class.equals(propertyType)) {
167                 returnValue = new java.util.LinkedList<Object>();
168 
169             } else if (Map.class.equals(propertyType)) {
170                 returnValue = new java.util.HashMap<Object, Object>();
171 
172             } else if (!String.class.equals(propertyType)) {
173                 try {
174                     returnValue = propertyType.newInstance();
175                 } catch (InstantiationException e) {
176                     throw new IllegalStateException("Failed to create new object for setting property value", e);
177                 } catch (IllegalAccessException e) {
178                     throw new IllegalStateException("Failed to create new object for setting property value", e);
179                 }
180             }
181         }
182         
183         return returnValue;
184     }
185 
186     /**
187      * Get a property value from an array.
188      * 
189      * <p>
190      * NOTE: This method is null and bounds-safe. When the property name does not represent a valid
191      * array index, or the array is null, then null is returned.
192      * </p>
193      * 
194      * @param array The array.
195      * @param name The name of the property value.
196      * @return The property value for the named entry in the array. When name is 'size' or 'length',
197      *         then the length of the array is returned, otherwise the property name is converted to
198      *         an integer and used as the array index.
199      */
200     private static Object getArray(Object array, String name) {
201         if (array == null) {
202             return null;
203         }
204 
205         for (int i = 0; i < name.length(); i++) {
206             if (!Character.isDigit(name.charAt(i))) {
207                 return null;
208             }
209         }
210 
211         int i = Integer.parseInt(name);
212 
213         if (i >= Array.getLength(array)) {
214             return null;
215         }
216 
217         return Array.get(array, i);
218     }
219 
220     /**
221      * Set a property value in an array.
222      * 
223      * @param array The array.
224      * @param name A string representation of the index in the array.
225      * @param value The property value to set in the array.
226      */
227     private static void setArray(Object array, String name, Object value) {
228         Array.set(array, Integer.parseInt(name), value);
229     }
230 
231     /**
232      * Get a property value from an list.
233      * 
234      * <p>
235      * NOTE: This method is null and bounds-safe. When the property name does not represent a valid
236      * list index, or the list is null, then null is returned.
237      * </p>
238      * 
239      * @param list The list.
240      * @param name The name of the property value.
241      * @return The property value for the named entry in the list. When name is 'size' or 'length',
242      *         then the length of the list is returned, otherwise the property name is converted to
243      *         an integer and used as the list index.
244      */
245     private static Object getList(List<?> list, String name) {
246         int length;
247         if (list == null) {
248             length = 0;
249         } else {
250             length = list.size();
251         }
252 
253         for (int i = 0; i < name.length(); i++) {
254             if (!Character.isDigit(name.charAt(i))) {
255                 return null;
256             }
257         }
258 
259         int i = Integer.parseInt(name);
260         if (i >= length) {
261             return null;
262         }
263 
264         return list.get(i);
265     }
266 
267     /**
268      * Set a property value in a list.
269      * 
270      * @param list The list.
271      * @param name A string representation of the list index.
272      * @param value The value to add to the list.
273      */
274     @SuppressWarnings("unchecked")
275     private static void setList(List<?> list, String name, Object value) {
276         int i = Integer.parseInt(name);
277         while (i >= list.size()) {
278             list.add(null);
279         }
280         ((List<Object>) list).set(i, value);
281     }
282 
283     /**
284      * Get a property value from an map.
285      * 
286      * @param map The map.
287      * @param name The name of the property value.
288      * @return The property value for the named entry in the map.
289      */
290     private static Object getMap(Map<?, ?> map, String name) {
291         if (map != null && map.containsKey(name)) {
292             return map.get(name);
293         }
294         return null;
295     }
296 
297     /**
298      * Determine if a warning should be logged on when an invalid property is encountered
299      * on the current thread.
300      * @return True to log warnings when invalid properties are encountered, false to ignore
301      *        invalid properties.
302      */
303     public static boolean isWarning() {
304         return Boolean.TRUE.equals(TL_WARN.get());
305     }
306 
307     /**
308      * Indicate whether or not a warning should be logged on when an invalid property is encountered
309      * on the current thread.
310      * @param warning True to log warnings when invalid properties are encountered, false to ignore
311      *        invalid properties.
312      */
313     public static void setWarning(boolean warning) {
314         if (warning) {
315             TL_WARN.set(true);
316         } else {
317             TL_WARN.remove();
318         }
319     }
320 
321     /**
322      * Resolve a path expression on a bean.
323      * 
324      * @param bean The bean.
325      * @param beanClass The bean class.
326      * @param propertyPath The property path expression.
327      * @param grow True to create objects while traversing the path, false to traverse class
328      *        structure only when referring to null.
329      * @return A reference to the final parse node involved in parsing the path expression.
330      */
331     public static ObjectPropertyReference resolvePath(Object bean, Class<?> beanClass, String propertyPath, boolean grow) {
332         if (ObjectPathExpressionParser.isPath(propertyPath)) {
333 
334             // Parse the path expression.  This requires a new reference object since object read
335             // methods could potentially call this method recursively.
336             ObjectPropertyReference reference = new ObjectPropertyReference();
337             reference.beanClass = beanClass;
338             if (bean instanceof Copyable) {
339                 reference.bean = CopyUtils.unwrap((Copyable) bean);
340                 if (!(beanClass.isInstance(reference.bean))) {
341                     reference.beanClass = reference.bean.getClass();
342                 }
343             } else {
344                 reference.bean = bean;
345             }
346 
347             ObjectPropertyReference resolved = (ObjectPropertyReference) ObjectPathExpressionParser
348                     .parsePathExpression(reference, propertyPath,
349                             grow ? MUTATE_REF_PATH_ENTRY : LOOKUP_REF_PATH_ENTRY);
350 
351             reference.bean = resolved.bean;
352             reference.beanClass = resolved.beanClass;
353             reference.beanType = resolved.beanType;
354             reference.name = resolved.name;
355             return reference;
356 
357         } else {
358 
359             return resolveProperty(bean, beanClass, propertyPath);
360 
361         }
362     }
363 
364     /**
365      * Get a single-use reference for resolving a property on a bean.
366      *
367      * <p>
368      * When using this method, the property name will be treated as-is, and will not be resolved as
369      * a path expression.
370      * </p>
371      *
372      * @param bean The bean.
373      * @param beanClass The bean class.
374      * @param propertyPath The property path.
375      * @return A single-use reference to the final parse node involved in parsing the path
376      *         expression. Note that the reference returned by this method will be reused and
377      *         modified by the next call, so should not be set to a variable.
378      */
379     public static ObjectPropertyReference resolveProperty(Object bean, Class<?> beanClass, String propertyPath) {
380         ObjectPropertyReference reference = TL_BUILDER_REF.get();
381         if (reference == null) {
382             reference = new ObjectPropertyReference();
383             TL_BUILDER_REF.set(reference);
384         }
385         reference.beanClass = beanClass;
386         if (bean instanceof Copyable) {
387             reference.bean = CopyUtils.unwrap((Copyable) bean);
388             if (!(beanClass.isInstance(reference.bean)) && reference.bean != null) {
389                 reference.beanClass = reference.bean.getClass();
390             }
391         } else {
392             reference.bean = bean;
393         }
394         reference.beanType = reference.beanClass;
395         reference.name = propertyPath;
396         return reference;
397     }
398 
399     /**
400      * Convert to a primitive type if available.
401      * 
402      * @param type The type to convert.
403      * @return A primitive type, if available, that corresponds to the type.
404      */
405     private static Class<?> getPrimitiveType(Class<?> type) {
406         if (Byte.class.equals(type)) {
407             return Byte.TYPE;
408 
409         } else if (Short.class.equals(type)) {
410             return Short.TYPE;
411 
412         } else if (Integer.class.equals(type)) {
413             return Integer.TYPE;
414 
415         } else if (Long.class.equals(type)) {
416             return Long.TYPE;
417 
418         } else if (Boolean.class.equals(type)) {
419             return Boolean.TYPE;
420 
421         } else if (Float.class.equals(type)) {
422             return Float.TYPE;
423 
424         } else if (Double.class.equals(type)) {
425             return Double.TYPE;
426         }
427 
428         return type;
429     }
430 
431     /**
432      * The bean, may be null for traversing only class data.
433      */
434     private Object bean;
435 
436     /**
437      * The bean class.
438      */
439     private Class<?> beanClass;
440 
441     /**
442      * The bean type.
443      */
444     private Type beanType;
445 
446     /**
447      * The property name.
448      */
449     private String name;
450 
451     /**
452      * Internal private constructor.
453      */
454     private ObjectPropertyReference() {}
455 
456     /**
457      * Convert a string property value to the targeted property type.
458      * 
459      * @param propertyValue The string property value.
460      * @return The property value, converted to the property type.
461      */
462     private Object convertStringToPropertyType(String propertyValue) {
463         Class<?> propertyType = getPropertyType();
464 
465         // TODO: these methods, and their inversions (below) need to be either support escaping
466         // or be removed.  Both have been included for equivalence with previous BeanWrapper
467         // implementation.
468         if (List.class.equals(propertyType)) {
469             return KRADUtils.convertStringParameterToList(propertyValue);
470 
471         } else if (Map.class.equals(propertyType)) {
472             return KRADUtils.convertStringParameterToMap(propertyValue);
473 
474         } else {
475 
476             // TODO: Determine if a different PropertyEditor registry exists for KRAD
477             PropertyEditor editor = PropertyEditorManager
478                     .findEditor(getPrimitiveType(propertyType));
479             if (editor == null) {
480                 throw new IllegalArgumentException("No property editor available for converting '" + propertyValue
481                         + "' to " + propertyType);
482             }
483 
484             editor.setAsText((String) propertyValue);
485             return editor.getValue();
486         }
487 
488     }
489     
490     /**
491      * Convert a property value to a string.
492      * 
493      * @param propertyValue The property value.
494      * @return The property value, converted to a string.
495      */
496     private Object convertPropertyValueToString(Object propertyValue) {
497 
498         // TODO: these methods, and their inversions (above) need to be either support escaping
499         // or be removed.  Both have been included for equivalence with previous BeanWrapper
500         // implementation.
501         // FIXME: Where are these conversions used?  Can they be removed?
502         if (propertyValue instanceof List) {
503             StringBuilder listStringBuilder = new StringBuilder();
504             for (Object item : (List<?>) propertyValue) {
505                 if (listStringBuilder.length() > 0) {
506                     listStringBuilder.append(',');
507                 }
508                 listStringBuilder.append((String) item);
509             }
510             return listStringBuilder.toString();
511 
512         } else if (propertyValue instanceof Map) {
513             @SuppressWarnings("unchecked")
514             Map<String, String> mapPropertyValue = (Map<String, String>) propertyValue;
515             return KRADUtils.buildMapParameterString(mapPropertyValue);
516 
517         } else {
518 
519             // TODO: Determine if a different PropertyEditor registry exists for KRAD
520             PropertyEditor editor = PropertyEditorManager
521                     .findEditor(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 || propertyType.isInstance(propertyValue)) {
542             return propertyValue;
543         }
544 
545         if (propertyValue instanceof String) {
546             return convertStringToPropertyType((String) propertyValue);
547         }
548         
549         if (propertyType.equals(String.class)) {
550             return convertPropertyValueToString(propertyValue);
551         }
552 
553         return propertyValue;
554     }
555 
556     /**
557      * Get the bean.
558      * @return The bean
559      */
560     public Object getBean() {
561         return this.bean;
562     }
563 
564     /**
565      * Get the bean class.
566      * 
567      * <p>
568      * The bean class may be a super-class of the bean, and is likely to be an abstract class or
569      * interface.
570      * </p>
571      * 
572      * @return The bean class. It is expected that the value returned by {@link #getBean()} is
573      *         either null, or that {@link #getBeanClass()}.{@link Class#isInstance(Object)
574      *         isInstance(}{@link #getBean()}{@link Class#isInstance(Object) )} will always return
575      *         true.
576      */
577     public Class<?> getBeanClass() {
578         return this.beanClass;
579     }
580 
581     /**
582      * Get the bean implementation class.
583      * 
584      * @return The the bean implementation class. The class returned by this method should always be
585      *         the same class or a subclass of the class returned by {@link #getBeanClass()}. When
586      *         {@link #getBean()} returns a non-null value it is expected that {@link #getBean()}.
587      *         {@link Object#getClass() getClass()} == {@link #getImplClass()}.
588      */
589     public Class<?> getImplClass() {
590         assert bean == null || beanClass.isInstance(bean) : bean + " is not a " + beanClass;
591         return bean == null ? beanClass : bean.getClass();
592     }
593 
594     /**
595      * Get the property name.
596      * 
597      * @return The property name.
598      */
599     public String getName() {
600         return this.name;
601     }
602     
603     /**
604      * Determine if a list or array property is readable.
605      * 
606      * @return True if the property is a list or array, and is readable, false if not.
607      */
608     private boolean isListOrArrayAndCanReadOrWrite() {
609         Class<?> implClass = getImplClass();
610 
611         if (!implClass.isArray() && !List.class.isAssignableFrom(implClass)) {
612             return false;
613         }
614         
615         if (name.length() == 0) {
616             return false;
617         }
618 
619         for (int i = 0; i < name.length(); i++) {
620             if (!Character.isDigit(name.charAt(i))) {
621                 return false;
622             }
623         }
624 
625         return true;
626     }
627     
628     /**
629      * Determine if a list or array property is readable.
630      * 
631      * @return True if the property is a list or array, and is readable, false if not.
632      */
633     private Boolean canReadOrWriteSimple() {
634         if (name == null) {
635             // self reference
636             return true;
637         }
638 
639         Class<?> implClass = getImplClass();
640         
641         if (implClass == null) {
642             return false;
643         }
644 
645         if (isListOrArrayAndCanReadOrWrite()) {
646             return true;
647         }
648         
649         if (Map.class.isAssignableFrom(implClass)) {
650             return true;
651         }
652 
653         return null;
654     }
655 
656     /**
657      * Determine if the bean property is readable.
658      * 
659      * @return True if the property is readable, false if not.
660      */
661     public boolean canRead() {
662         Boolean simple = canReadOrWriteSimple();
663         
664         if (simple != null) {
665             return simple;
666         }
667 
668         return ObjectPropertyUtils.getReadMethod(getImplClass(), name) != null;
669     }
670 
671     /**
672      * Determine if the property is writable.
673      * 
674      * @return True if the property is writable, false if not.
675      */
676     public boolean canWrite() {
677         Boolean simple = canReadOrWriteSimple();
678         
679         if (simple != null) {
680             return simple;
681         }
682 
683         return ObjectPropertyUtils.getWriteMethod(getImplClass(), name) != null;
684     }
685     
686     /**
687      * Get the property value for a specific bean property of a known bean class.
688      * 
689      * @return The property value for the specific bean property on the given bean.
690      */
691     public Object getFromReadMethod() {
692         Class<?> implClass = getImplClass();
693 
694         Method readMethod = ObjectPropertyUtils.getReadMethod(implClass, name);
695 
696         if (readMethod == null) {
697             if (isWarning()) {
698                 IllegalArgumentException missingPropertyException = new IllegalArgumentException("No property name '"
699                         + name + "' is readable on " +
700                         (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass));
701                 LOG.warn(missingPropertyException);
702             }
703 
704             return null;
705         }
706 
707         try {
708             return readMethod.invoke(bean);
709         } catch (IllegalAccessException e) {
710             throw new IllegalArgumentException("Illegal access invoking property read method " + readMethod, e);
711         } catch (InvocationTargetException e) {
712             Throwable cause = e.getCause();
713             if (cause instanceof RuntimeException) {
714                 throw (RuntimeException) cause;
715             } else if (cause instanceof Error) {
716                 throw (Error) cause;
717             }
718             throw new IllegalStateException("Unexpected invocation target exception invoking property read method "
719                     + readMethod, e);
720         }
721     }
722 
723     /**
724      * Get the property value for a specific bean property of a known bean class.
725      * 
726      * @return The property value for the specific bean property on the given bean.
727      */
728     public Object get() {
729         if (name == null) {
730             return bean;
731         }
732 
733         Class<?> implClass = getImplClass();
734 
735         if (implClass == null || bean == null) {
736             return null;
737 
738         } else if (implClass.isArray()) {
739             return getArray(bean, name);
740         
741         } else if (List.class.isAssignableFrom(implClass)) {
742             return getList((List<?>) bean, name);
743         
744         } else if (Map.class.isAssignableFrom(implClass)) {
745             return getMap((Map<?, ?>) bean, name);
746         
747         } else {
748             return getFromReadMethod();
749         }
750     }
751 
752     /**
753      * Get the type of a specific property on a collection.
754      * 
755      * @return The type of the referenced element in the collection, if non-null. When null, the
756      *         parameterized type of the collection will be returned, or Object if the collection is
757      *         not parameterized. If this is not a reference to an indexed collection, the null is
758      *         returned.
759      */
760     private Class<?> getCollectionPropertyType() {
761         Class<?> implClass = getImplClass();
762         boolean isMap = Map.class.isAssignableFrom(implClass);
763         boolean isList = List.class.isAssignableFrom(implClass);
764 
765         Object refBean;
766 
767         if (isMap) {
768             refBean = getMap((Map<?, ?>) bean, name);
769         } else if (isList) {
770             refBean = getList((List<?>) bean, name);
771         } else {
772             return null;
773         }
774 
775         if (refBean != null) {
776             return refBean.getClass();
777         }
778 
779         if (beanType instanceof ParameterizedType) {
780             ParameterizedType parameterizedType = (ParameterizedType) beanType;
781             Type valueType = parameterizedType.getActualTypeArguments()[isList ? 0 : 1];
782 
783             if (valueType instanceof Class) {
784                 return (Class<?>) valueType;
785             }
786         }
787 
788         return Object.class;
789     }
790     
791     /**
792      * Get the type of a specific property on a given bean class.
793      * 
794      * @return The type of the specific property on the given bean class.
795      */
796     private Class<?> getPropertyTypeFromReadOrWriteMethod() {
797         Class<?> implClass = getImplClass();
798 
799         Method readMethod = ObjectPropertyUtils.getReadMethod(implClass, name);
800         Method writeMethod;
801 
802         if (readMethod == null) {
803 
804             writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name);
805             assert writeMethod == null || writeMethod.getParameterTypes().length == 1 : "Invalid write method "
806                     + writeMethod;
807 
808             if (writeMethod == null && isWarning()) {
809                 IllegalArgumentException missingPropertyException = new IllegalArgumentException("No property name '"
810                         + name + "' is readable or writable on " +
811                         (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass));
812                 LOG.warn(missingPropertyException);
813             }
814 
815             return writeMethod == null ? null : writeMethod.getParameterTypes()[0];
816 
817         } else {
818             Class<?> returnType = readMethod.getReturnType();
819             assert (writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name)) == null
820                     || writeMethod.getParameterTypes()[0].isAssignableFrom(returnType) : "Property types don't match "
821                     + readMethod + " " + writeMethod;
822             return returnType;
823         }
824     }
825     
826     /**
827      * Get the type of a specific property on the implementation class.
828      * 
829      * @return The type of the specific property on the implementation class.
830      */
831     public Class<?> getPropertyType() {
832         Class<?> implClass = getImplClass();
833 
834         if (implClass == null) {
835             return null;
836         }
837 
838         if (name == null) {
839             // self reference
840             return getImplClass();
841         }
842 
843         Class<?> propertyType = getCollectionPropertyType();
844 
845         if (propertyType != null) {
846             return propertyType;
847         } else {
848             return getPropertyTypeFromReadOrWriteMethod();
849         }
850     }
851 
852     /**
853      * Set the property to a specific value using the property's write method.
854      * 
855      * @param propertyValue The property value.
856      */
857     private void setUsingWriteMethod(Object propertyValue) {
858         Class<?> implClass = getImplClass();
859         Method writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name);
860         
861         if (writeMethod == null) {
862             throw new IllegalArgumentException("No property name '" + name + "' is writable on " +
863                     (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass));
864         }
865 
866         try {
867             writeMethod.invoke(bean, propertyValue);
868         } catch (IllegalAccessException e) {
869             throw new IllegalArgumentException("Illegal access invoking property write method " + writeMethod, e);
870         } catch (InvocationTargetException e) {
871             Throwable cause = e.getCause();
872             if (cause instanceof RuntimeException) {
873                 throw (RuntimeException) cause;
874             } else if (cause instanceof Error) {
875                 throw (Error) cause;
876             }
877             throw new IllegalStateException(
878                     "Unexpected invocation target exception invoking property write method "
879                             + writeMethod, e);
880         }
881     }
882 
883     /**
884      * Set the property to a specific value.
885      * 
886      * @param propertyValue The property value.
887      */
888     public void set(Object propertyValue) {
889         if (name == null) {
890             throw new IllegalArgumentException("Cannot modify a self-reference");
891         }
892 
893         if (bean == null) {
894             throw new IllegalArgumentException("Reference is null");
895         }
896 
897         propertyValue = convertToPropertyType(propertyValue);
898 
899         Class<?> implClass = getImplClass();
900 
901         if (implClass == null) {
902             throw new IllegalArgumentException("No property name '" + name + "' is writable on " + beanClass);
903         }
904 
905         if (implClass.isArray()) {
906             setArray(bean, name, propertyValue);
907 
908         } else if (List.class.isAssignableFrom(implClass)) {
909             setList((List<?>) bean, name, propertyValue);
910 
911         } else if (Map.class.isAssignableFrom(implClass)) {
912             @SuppressWarnings("unchecked")
913             Map<Object, Object> uncheckedMap = (Map<Object, Object>) bean;
914             uncheckedMap.put(name, propertyValue);
915 
916         } else {
917             setUsingWriteMethod(propertyValue);
918         }
919     }
920 
921 }