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