001    // Copyright 2006-2007 Daniel Gredler
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    //     http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package net.sf.beanform.util;
016    
017    import java.beans.BeanInfo;
018    import java.beans.IntrospectionException;
019    import java.beans.Introspector;
020    import java.beans.PropertyDescriptor;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.lang.reflect.Field;
024    import java.lang.reflect.InvocationTargetException;
025    import java.lang.reflect.Method;
026    
027    import org.apache.commons.logging.Log;
028    import org.apache.commons.logging.LogFactory;
029    import org.apache.hivemind.ApplicationRuntimeException;
030    import org.apache.hivemind.util.Defense;
031    import org.apache.tapestry.request.IUploadFile;
032    
033    /**
034     * Utility class for reflection-related operations.
035     *
036     * @author Daniel Gredler
037     */
038    public final class ReflectionUtils {
039    
040        private final static Log LOG = LogFactory.getLog( ReflectionUtils.class );
041    
042        ReflectionUtils() {
043            // Empty.
044        }
045    
046        public static void setFieldValue( Object object, String fieldName, Object value )
047        throws NoSuchFieldException, IllegalAccessException {
048            Defense.notNull( object, "object" );
049            Defense.notNull( fieldName, "fieldName" );
050            for( Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass() ) {
051                for( Field field : clazz.getDeclaredFields() ) {
052                    if( field.getName().equals( fieldName ) ) {
053                        field.setAccessible( true );
054                        field.set( object, value );
055                        return;
056                    }
057                }
058            }
059            throw new NoSuchFieldException( "Unable to find field '" + object.getClass().getName() + "#" + fieldName + "'." );
060        }
061    
062        public static Object getFieldValue( Object object, String fieldName )
063        throws NoSuchFieldException, IllegalAccessException {
064            Defense.notNull( object, "object" );
065            Defense.notNull( fieldName, "fieldName" );
066            for( Class clazz = object.getClass(); clazz != null; clazz = clazz.getSuperclass() ) {
067                for( Field field : clazz.getDeclaredFields() ) {
068                    if( field.getName().equals( fieldName ) ) {
069                        field.setAccessible( true );
070                        Object value = field.get( object );
071                        return value;
072                    }
073                }
074            }
075            throw new NoSuchFieldException( "Unable to find field '" + object.getClass().getName() + "#" + fieldName + "'." );
076        }
077    
078        public static Field getField( Class clazz, String... names )
079        throws NoSuchFieldException {
080            return getFieldRecursive( clazz, names, 0 );
081        }
082    
083        private static Field getFieldRecursive( Class clazz, String[] names, int level )
084        throws NoSuchFieldException {
085            Field f = clazz.getDeclaredField( names[ level ] );
086            boolean lastLevel = ( names.length == level + 1 );
087            if( lastLevel ) return f;
088            else return getFieldRecursive( f.getType(), names, level + 1 );
089        }
090    
091        public static Object getPropertyValue( Object object, String... names )
092        throws IllegalAccessException, InvocationTargetException {
093            return getPropertyValueRecursive( object, names, 0 );
094        }
095    
096        private static Object getPropertyValueRecursive( Object object, String[] names, int level )
097        throws IllegalAccessException, InvocationTargetException {
098            if( object == null ) return null;
099            Class clazz = object.getClass();
100            PropertyDescriptor pd = getDescriptor( clazz, names[ level ] );
101            object = pd.getReadMethod().invoke( object );
102            boolean lastLevel = ( names.length == level + 1 );
103            if( lastLevel ) return object;
104            else return getPropertyValueRecursive( object, names, level + 1 );
105        }
106    
107        public static void setPropertyValue( Object value, Object object, String... names )
108        throws IllegalAccessException, InvocationTargetException, InstantiationException {
109            setPropertyValueRecursive( value, object, names, 0 );
110        }
111    
112        private static void setPropertyValueRecursive( Object value, Object object, String[] names, int level )
113        throws IllegalAccessException, InvocationTargetException, InstantiationException {
114            Class clazz = object.getClass();
115            PropertyDescriptor pd = getDescriptor( clazz, names[ level ] );
116            boolean lastLevel = ( names.length == level + 1 );
117            if( lastLevel ) {
118                Method setter = pd.getWriteMethod();
119                Class type = setter.getParameterTypes()[ 0 ];
120                value = convertToType( value, type );
121                setter.invoke( object, value );
122            }
123            else {
124                Object nextObject = pd.getReadMethod().invoke( object );
125                if( nextObject == null ) {
126                    if( value != null ) {
127                        nextObject = pd.getReadMethod().getReturnType().newInstance();
128                        pd.getWriteMethod().invoke( object, nextObject );
129                    }
130                    else {
131                        if( LOG.isDebugEnabled() ) {
132                            StringBuilder msg = new StringBuilder( "Aborting attempt to set property '" );
133                            for( String name : names ) msg.append( name ).append( '.' );
134                            msg.deleteCharAt( msg.length() - 1 );
135                            msg.append( "' to null: the '" ).append( names[ level ] );
136                            msg.append( "' portion of the property is null." );
137                            LOG.debug( msg );
138                        }
139                        return;
140                    }
141                }
142                setPropertyValueRecursive( value, nextObject, names, level + 1 );
143            }
144        }
145    
146        public static PropertyDescriptor getPropertyDescriptor( Class clazz, String... names ) {
147            return getPropertyDescriptorRecursive( clazz, names, 0 );
148        }
149    
150        private static PropertyDescriptor getPropertyDescriptorRecursive( Class clazz, String[] names, int level ) {
151            PropertyDescriptor pd = getDescriptor( clazz, names[ level ] );
152            boolean lastLevel = ( names.length == level + 1 );
153            if( lastLevel ) return pd;
154            else return getPropertyDescriptorRecursive( pd.getPropertyType(), names, level + 1 );
155        }
156    
157        private static PropertyDescriptor getDescriptor( Class clazz, String name ) {
158            try {
159                BeanInfo info = Introspector.getBeanInfo( clazz, errorTrigger );
160                PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
161                for( PropertyDescriptor descriptor : descriptors ) {
162                    if( descriptor.getName().equals( name ) ) {
163                        return descriptor;
164                    }
165                }
166            }
167            catch( IntrospectionException e ) {
168                String msg = ReflectionUtilsMessages.cantFindProperty( name, clazz );
169                throw new ApplicationRuntimeException( msg, e );
170            }
171            String msg = ReflectionUtilsMessages.cantFindProperty( name, clazz );
172            throw new ApplicationRuntimeException( msg );
173        }
174    
175        /**
176         * Converts the specified value to the specified type. Our current use case only
177         * requires that we convert from strings to numbers and from IUploadFile's to byte[]'s,
178         * so this is <b>not</b> a generic type conversion method. If this method is given
179         * a conversion that it is not meant to handle, it just returns the original value.
180         */
181        private static Object convertToType( Object value, Class type ) {
182            if( value != null && type.isInstance( value ) == false ) {
183                // x -> y
184                if( value instanceof String ) {
185                    // string -> y
186                    String s = (String) value;
187                    if( Short.class.equals( type ) || short.class.equals( type ) ) {
188                        // string -> short
189                        value = Short.parseShort( s );
190                    }
191                    else if( Integer.class.equals( type ) || int.class.equals( type ) ) {
192                        // string -> integer
193                        value = Integer.parseInt( s );
194                    }
195                    else if( Long.class.equals( type ) || long.class.equals( type ) ) {
196                        // string -> long
197                        value = Long.parseLong( s );
198                    }
199                    else if( Float.class.equals( type ) || float.class.equals( type ) ) {
200                        // string -> float
201                        value = Float.parseFloat( s );
202                    }
203                    else if( Double.class.equals( type ) || double.class.equals( type ) ) {
204                        // string -> double
205                        value = Double.parseDouble( s );
206                    }
207                }
208                else if( value instanceof IUploadFile && byte[].class.equals( type ) ) {
209                    // IUploadFile -> byte[]
210                    IUploadFile file = (IUploadFile) value;
211                    long size = file.getSize();
212                    if( size < 1 ) {
213                        value = null;
214                    }
215                    else if( size > Integer.MAX_VALUE ) {
216                        String msg = "File is too large; the maximum allowable size is " + Integer.MAX_VALUE + " bytes.";
217                        throw new ApplicationRuntimeException( msg );
218                    }
219                    else {
220                        byte[] bytes = new byte[ (int) size ];
221                        InputStream input = file.getStream();
222                        try {
223                            int read = input.read( bytes );
224                            if( read != size ) {
225                                String msg = "Error reading file; expected " + size + " bytes, but got " + read + ".";
226                                throw new ApplicationRuntimeException( msg );
227                            }
228                        }
229                        catch( IOException e ) {
230                            String msg = "Error reading file: " + e.getMessage();
231                            throw new ApplicationRuntimeException( msg, e );
232                        }
233                        value = bytes;
234                    }
235                }
236            }
237            return value;
238        }
239    
240        // ------------------------------------------------------------------------------------------------------------ //
241        // ------------------------------------------- unit testing support ------------------------------------------- //
242        // ------------------------------------------------------------------------------------------------------------ //
243    
244        private static Class errorTrigger = null;
245    
246        /**
247         * Calling this method will cause subsequent {@link #getDescriptor(Class, String)} calls to result
248         * in <tt>IntrospectionException</tt>s that will allow us to test error-handling functionality.
249         */
250        static void triggerIntrospectionException() {
251            if( errorTrigger != null ) errorTrigger = null;
252            else errorTrigger = BeanInfo.class;
253        }
254    
255    }