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 */
016package org.joda.beans.impl.reflection;
017
018import java.beans.IntrospectionException;
019import java.beans.PropertyDescriptor;
020import java.lang.annotation.Annotation;
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.Method;
023import java.lang.reflect.Type;
024import java.util.Arrays;
025import java.util.List;
026
027import org.joda.beans.Bean;
028import org.joda.beans.MetaBean;
029import org.joda.beans.Property;
030import org.joda.beans.PropertyReadWrite;
031import org.joda.beans.impl.BasicMetaProperty;
032import 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 */
046public 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 <P>  the property type
063     * @param beanType  the bean type, not null
064     * @param propertyName  the property name, not empty
065     * @return the property, not null
066     */
067    public static <P> ReflectiveMetaProperty<P> of(Class<? extends Bean> beanType, String propertyName) {
068        return new ReflectiveMetaProperty<P>(beanType, propertyName);
069    }
070
071    /**
072     * Constructor using {@code PropertyDescriptor} to find the get and set methods.
073     * 
074     * @param beanType  the bean type, not null
075     * @param propertyName  the property name, not empty
076     */
077    @SuppressWarnings("unchecked")
078    private ReflectiveMetaProperty(Class<? extends Bean> beanType, String propertyName) {
079        super(propertyName);
080        PropertyDescriptor descriptor;
081        try {
082            descriptor = new PropertyDescriptor(propertyName, beanType);
083        } catch (IntrospectionException ex) {
084            throw new NoSuchFieldError("Invalid property: " + propertyName + ": " + ex.getMessage());
085        }
086        Method readMethod = descriptor.getReadMethod();
087        Method writeMethod = descriptor.getWriteMethod();
088        if (readMethod == null && writeMethod == null) {
089            throw new NoSuchFieldError("Invalid property: " + propertyName + ": Both read and write methods are missing");
090        }
091        this.declaringType = (readMethod != null ? readMethod.getDeclaringClass() : writeMethod.getDeclaringClass());
092        this.propertyType = (Class<P>) descriptor.getPropertyType();
093        this.readMethod = readMethod;
094        this.writeMethod = writeMethod;
095    }
096
097    /**
098     * Sets the meta-bean, necessary due to ordering restrictions during loading.
099     * @param metaBean  the meta-bean, not null
100     */
101    void setMetaBean(MetaBean metaBean) {
102        this.metaBean = metaBean;
103    }
104
105    //-----------------------------------------------------------------------
106    @Override
107    public Property<P> createProperty(Bean bean) {
108        return BasicProperty.of(bean, this);
109    }
110
111    @Override
112    public MetaBean metaBean() {
113        return metaBean;
114    }
115
116    @Override
117    public Class<?> declaringType() {
118        return declaringType;
119    }
120
121    @Override
122    public Class<P> propertyType() {
123        return propertyType;
124    }
125
126    @Override
127    public Type propertyGenericType() {
128        if (readMethod != null) {
129            return readMethod.getGenericReturnType();
130        }
131        return writeMethod.getGenericParameterTypes()[0];
132    }
133
134    @Override
135    public PropertyReadWrite readWrite() {
136        return (readMethod == null ? PropertyReadWrite.WRITE_ONLY :
137                (writeMethod == null ? PropertyReadWrite.READ_ONLY : PropertyReadWrite.READ_WRITE));
138    }
139
140    @Override
141    public List<Annotation> annotations() {
142        if (readMethod != null) {
143            return Arrays.asList(readMethod.getDeclaredAnnotations());
144        }
145        return Arrays.asList(writeMethod.getDeclaredAnnotations());
146    }
147
148    //-----------------------------------------------------------------------
149    @Override
150    @SuppressWarnings("unchecked")
151    public P get(Bean bean) {
152        if (readWrite().isReadable() == false) {
153            throw new UnsupportedOperationException("Property cannot be read: " + name());
154        }
155        try {
156            return (P) readMethod.invoke(bean, (Object[]) null);
157        } catch (IllegalArgumentException ex) {
158            throw new UnsupportedOperationException("Property cannot be read: " + name(), ex);
159        } catch (IllegalAccessException ex) {
160            throw new UnsupportedOperationException("Property cannot be read: " + name(), ex);
161        } catch (InvocationTargetException ex) {
162            if (ex.getCause() instanceof RuntimeException) {
163                throw (RuntimeException) ex.getCause();
164            }
165            throw new RuntimeException(ex);
166        }
167    }
168
169    @Override
170    public void set(Bean bean, Object value) {
171        if (readWrite().isWritable() == false) {
172            throw new UnsupportedOperationException("Property cannot be written: " + name());
173        }
174        try {
175            writeMethod.invoke(bean, value);
176        } catch (IllegalArgumentException ex) {
177            if (value == null && writeMethod.getParameterTypes()[0].isPrimitive()) {
178                throw new NullPointerException("Property cannot be written: " + name() + ": Cannot store null in primitive");
179            }
180            if (propertyType.isInstance(value) == false) {
181                throw new ClassCastException("Property cannot be written: " + name() + ": Invalid type: " + value.getClass().getName());
182            }
183            throw new UnsupportedOperationException("Property cannot be written: " + name(), ex);
184        } catch (IllegalAccessException ex) {
185            throw new UnsupportedOperationException("Property cannot be written: " + name(), ex);
186        } catch (InvocationTargetException ex) {
187            if (ex.getCause() instanceof RuntimeException) {
188                throw (RuntimeException) ex.getCause();
189            }
190            throw new RuntimeException(ex);
191        }
192    }
193
194}