001    /*
002     *  Copyright 2001-2010 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;
017    
018    import java.beans.IntrospectionException;
019    import java.beans.PropertyDescriptor;
020    import java.lang.reflect.InvocationTargetException;
021    import java.lang.reflect.Method;
022    
023    /**
024     * A meta-property implemented using a {@code PropertyDescriptor}.
025     * <p>
026     * The property descriptor class is part of the JDK JavaBean standard.
027     * It provides access to get and set a property on a bean.
028     * 
029     * @param <B>  the type of the bean
030     * @param <P>  the type of the property content
031     * @author Stephen Colebourne
032     */
033    public final class StandardMetaProperty<B, P> implements MetaProperty<B, P> {
034    
035        /** The name of the property. */
036        private final String name;
037        /** The type of the property. */
038        private final Class<P> propertyClass;
039        /** The type of the bean. */
040        private final Class<B> beanClass;
041        /** The read method. */
042        private final Method readMethod;
043        /** The write method. */
044        private final Method writeMethod;
045    
046        /**
047         * Factory to create a meta-property avoiding duplicate generics.
048         * 
049         * @param beanType  the bean type
050         * @param propertyName  the property name
051         */
052        public static <B, P> StandardMetaProperty<B, P> of(Class<B> beanType, String propertyName) {
053            return new StandardMetaProperty<B, P>(beanType, propertyName);
054        }
055    
056        /**
057         * Constructor using {@code PropertyDescriptor} to find the get and set methods.
058         * 
059         * @param beanType  the bean type, not null
060         * @param propertyName  the property name, not null
061         */
062        @SuppressWarnings("unchecked")
063        private StandardMetaProperty(Class<B> beanType, String propertyName) {
064            PropertyDescriptor descriptor;
065            try {
066                descriptor = new PropertyDescriptor(propertyName, beanType);
067            } catch (IntrospectionException ex) {
068                throw new NoSuchFieldError("Invalid property: " + propertyName + ": " + ex.getMessage());
069            }
070            Method readMethod = descriptor.getReadMethod();
071            Method writeMethod = descriptor.getWriteMethod();
072            if (readMethod == null && writeMethod == null) {
073                throw new NoSuchFieldError("Invalid property: " + propertyName + ": Both read and write methods are missing");
074            }
075            this.name = descriptor.getName();
076            this.propertyClass = (Class<P>) descriptor.getPropertyType();
077            this.readMethod = readMethod;
078            this.writeMethod = writeMethod;
079            this.beanClass = beanType;
080        }
081    
082        //-----------------------------------------------------------------------
083        @Override
084        public Property<B, P> createProperty(B bean) {
085            return StandardProperty.of(bean, this);
086        }
087    
088        @Override
089        public String name() {
090            return name;
091        }
092    
093        @Override
094        public Class<P> propertyClass() {
095            return propertyClass;
096        }
097    
098        @Override
099        public Class<B> beanClass() {
100            return beanClass;
101        }
102    
103        @Override
104        public ReadWriteProperty readWrite() {
105            return (readMethod == null ? ReadWriteProperty.WRITE_ONLY :
106                    (writeMethod == null ? ReadWriteProperty.READ_ONLY : ReadWriteProperty.READ_WRITE));
107        }
108    
109        //-----------------------------------------------------------------------
110        /**
111         * Gets the value of the bound property for the provided bean.
112         * <p>
113         * This is the equivalent to calling <code>getFoo()</code> on the bean itself.
114         * However some implementations of this interface may not require an actual get method.
115         * 
116         * @param bean  the bean to query, not null
117         * @return the value of the property on the bound bean
118         * @throws UnsupportedOperationException if the property is write-only
119         */
120        @SuppressWarnings("unchecked")
121        public P get(B bean) {
122            if (readWrite().isReadable() == false) {
123                throw new UnsupportedOperationException("Property cannot be read: " + name);
124            }
125            try {
126                return (P) readMethod.invoke(bean, (Object[]) null);
127            } catch (IllegalArgumentException ex) {
128                throw new UnsupportedOperationException("Property cannot be read: " + name, ex);
129            } catch (IllegalAccessException ex) {
130                throw new UnsupportedOperationException("Property cannot be read: " + name, ex);
131            } catch (InvocationTargetException ex) {
132                if (ex.getCause() instanceof RuntimeException) {
133                    throw (RuntimeException) ex.getCause();
134                }
135                throw new RuntimeException(ex);
136            }
137        }
138    
139        /**
140         * Sets the value of the bound property on the provided bean.
141         * <p>
142         * This is the equivalent to calling <code>setFoo()</code> on the bean itself.
143         * However some implementations of this interface may not require an actual set method.
144         * 
145         * @param bean  the bean to update, not null
146         * @param value  the value to set into the property on the bound bean
147         * @throws UnsupportedOperationException if the property is read-only
148         */
149        public void set(B bean, P value) {
150            if (readWrite().isWritable() == false) {
151                throw new UnsupportedOperationException("Property cannot be written: " + name);
152            }
153            try {
154                writeMethod.invoke(bean, value);
155            } catch (IllegalArgumentException ex) {
156                if (value == null && writeMethod.getParameterTypes()[0].isPrimitive()) {
157                    throw new NullPointerException("Property cannot be written: " + name + ": Cannot store null in primitive");
158                }
159                throw new UnsupportedOperationException("Property cannot be written: " + name, ex);
160            } catch (IllegalAccessException ex) {
161                throw new UnsupportedOperationException("Property cannot be written: " + name, ex);
162            } catch (InvocationTargetException ex) {
163                if (ex.getCause() instanceof RuntimeException) {
164                    throw (RuntimeException) ex.getCause();
165                }
166                throw new RuntimeException(ex);
167            }
168        }
169    
170        //-----------------------------------------------------------------------
171        /**
172         * Returns a string that summarizes the property.
173         * 
174         * @return a summary string, never null
175         */
176        @Override
177        public String toString() {
178            return "MetaProperty:" + name;
179        }
180    
181    }