001    /*
002     *  Copyright 2001-2013 Stephen Colebourne
003     *
004     *  Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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     */
016    package org.joda.beans.impl.reflection;
017    
018    import java.beans.IntrospectionException;
019    import java.beans.PropertyDescriptor;
020    import java.lang.annotation.Annotation;
021    import java.lang.reflect.InvocationTargetException;
022    import java.lang.reflect.Method;
023    import java.lang.reflect.Type;
024    import java.util.Arrays;
025    import java.util.List;
026    
027    import org.joda.beans.Bean;
028    import org.joda.beans.MetaBean;
029    import org.joda.beans.Property;
030    import org.joda.beans.PropertyReadWrite;
031    import org.joda.beans.impl.BasicMetaProperty;
032    import org.joda.beans.impl.BasicProperty;
033    
034    /**
035     * A meta-property implemented using a {@code PropertyDescriptor}.
036     * <p>
037     * The property descriptor class is part of the JDK JavaBean standard.
038     * It provides access to get and set a property on a bean.
039     * <p>
040     * Instances of this class should be declared as a static constant on the bean,
041     * one for each property, followed by a {@code ReflectiveMetaBean} declaration.
042     * 
043     * @param <P>  the type of the property content
044     * @author Stephen Colebourne
045     */
046    public final class ReflectiveMetaProperty<P> extends BasicMetaProperty<P> {
047    
048        /** The meta-bean. */
049        private volatile MetaBean metaBean;
050        /** The declaring type. */
051        private final Class<?> declaringType;
052        /** The type of the property. */
053        private final Class<P> propertyType;
054        /** The read method. */
055        private final Method readMethod;
056        /** The write method. */
057        private final Method writeMethod;
058    
059        /**
060         * Factory to create a meta-property avoiding duplicate generics.
061         * 
062         * @param beanType  the bean type, not null
063         * @param propertyName  the property name, not empty
064         */
065        public static <P> ReflectiveMetaProperty<P> of(Class<? extends Bean> beanType, String propertyName) {
066            return new ReflectiveMetaProperty<P>(beanType, propertyName);
067        }
068    
069        /**
070         * Constructor using {@code PropertyDescriptor} to find the get and set methods.
071         * 
072         * @param beanType  the bean type, not null
073         * @param propertyName  the property name, not empty
074         */
075        @SuppressWarnings("unchecked")
076        private ReflectiveMetaProperty(Class<? extends Bean> beanType, String propertyName) {
077            super(propertyName);
078            PropertyDescriptor descriptor;
079            try {
080                descriptor = new PropertyDescriptor(propertyName, beanType);
081            } catch (IntrospectionException ex) {
082                throw new NoSuchFieldError("Invalid property: " + propertyName + ": " + ex.getMessage());
083            }
084            Method readMethod = descriptor.getReadMethod();
085            Method writeMethod = descriptor.getWriteMethod();
086            if (readMethod == null && writeMethod == null) {
087                throw new NoSuchFieldError("Invalid property: " + propertyName + ": Both read and write methods are missing");
088            }
089            this.declaringType = (readMethod != null ? readMethod.getDeclaringClass() : writeMethod.getDeclaringClass());
090            this.propertyType = (Class<P>) descriptor.getPropertyType();
091            this.readMethod = readMethod;
092            this.writeMethod = writeMethod;
093        }
094    
095        /**
096         * Sets the meta-bean, necessary due to ordering restrictions during loading.
097         * @param metaBean  the meta-bean, not null
098         */
099        void setMetaBean(MetaBean metaBean) {
100            this.metaBean = metaBean;
101        }
102    
103        //-----------------------------------------------------------------------
104        @Override
105        public Property<P> createProperty(Bean bean) {
106            return BasicProperty.of(bean, this);
107        }
108    
109        @Override
110        public MetaBean metaBean() {
111            return metaBean;
112        }
113    
114        @Override
115        public Class<?> declaringType() {
116            return declaringType;
117        }
118    
119        @Override
120        public Class<P> propertyType() {
121            return propertyType;
122        }
123    
124        @Override
125        public Type propertyGenericType() {
126            if (readMethod != null) {
127                return readMethod.getGenericReturnType();
128            }
129            return writeMethod.getGenericParameterTypes()[0];
130        }
131    
132        @Override
133        public PropertyReadWrite readWrite() {
134            return (readMethod == null ? PropertyReadWrite.WRITE_ONLY :
135                    (writeMethod == null ? PropertyReadWrite.READ_ONLY : PropertyReadWrite.READ_WRITE));
136        }
137    
138        @Override
139        public List<Annotation> annotations() {
140            if (readMethod != null) {
141                return Arrays.asList(readMethod.getDeclaredAnnotations());
142            }
143            return Arrays.asList(writeMethod.getDeclaredAnnotations());
144        }
145    
146        //-----------------------------------------------------------------------
147        @Override
148        @SuppressWarnings("unchecked")
149        public P get(Bean bean) {
150            if (readWrite().isReadable() == false) {
151                throw new UnsupportedOperationException("Property cannot be read: " + name());
152            }
153            try {
154                return (P) readMethod.invoke(bean, (Object[]) null);
155            } catch (IllegalArgumentException ex) {
156                throw new UnsupportedOperationException("Property cannot be read: " + name(), ex);
157            } catch (IllegalAccessException ex) {
158                throw new UnsupportedOperationException("Property cannot be read: " + name(), ex);
159            } catch (InvocationTargetException ex) {
160                if (ex.getCause() instanceof RuntimeException) {
161                    throw (RuntimeException) ex.getCause();
162                }
163                throw new RuntimeException(ex);
164            }
165        }
166    
167        @Override
168        public void set(Bean bean, Object value) {
169            if (readWrite().isWritable() == false) {
170                throw new UnsupportedOperationException("Property cannot be written: " + name());
171            }
172            try {
173                writeMethod.invoke(bean, value);
174            } catch (IllegalArgumentException ex) {
175                if (value == null && writeMethod.getParameterTypes()[0].isPrimitive()) {
176                    throw new NullPointerException("Property cannot be written: " + name() + ": Cannot store null in primitive");
177                }
178                if (propertyType.isInstance(value) == false) {
179                    throw new ClassCastException("Property cannot be written: " + name() + ": Invalid type: " + value.getClass().getName());
180                }
181                throw new UnsupportedOperationException("Property cannot be written: " + name(), ex);
182            } catch (IllegalAccessException ex) {
183                throw new UnsupportedOperationException("Property cannot be written: " + name(), ex);
184            } catch (InvocationTargetException ex) {
185                if (ex.getCause() instanceof RuntimeException) {
186                    throw (RuntimeException) ex.getCause();
187                }
188                throw new RuntimeException(ex);
189            }
190        }
191    
192    }