001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.dbutils;
018    
019    import java.beans.BeanInfo;
020    import java.beans.IntrospectionException;
021    import java.beans.Introspector;
022    import java.beans.PropertyDescriptor;
023    import java.lang.reflect.InvocationTargetException;
024    import java.lang.reflect.Method;
025    import java.sql.ResultSet;
026    import java.sql.ResultSetMetaData;
027    import java.sql.SQLException;
028    import java.sql.Timestamp;
029    import java.util.ArrayList;
030    import java.util.Arrays;
031    import java.util.HashMap;
032    import java.util.List;
033    import java.util.Map;
034    
035    /**
036     * <p>
037     * <code>BeanProcessor</code> matches column names to bean property names 
038     * and converts <code>ResultSet</code> columns into objects for those bean 
039     * properties.  Subclasses should override the methods in the processing chain
040     * to customize behavior.
041     * </p>
042     * 
043     * <p>
044     * This class is thread-safe.
045     * </p>
046     * 
047     * @see BasicRowProcessor
048     * 
049     * @since DbUtils 1.1
050     */
051    public class BeanProcessor {
052    
053        /**
054         * Special array value used by <code>mapColumnsToProperties</code> that 
055         * indicates there is no bean property that matches a column from a 
056         * <code>ResultSet</code>.
057         */
058        protected static final int PROPERTY_NOT_FOUND = -1;
059    
060        /**
061         * Set a bean's primitive properties to these defaults when SQL NULL 
062         * is returned.  These are the same as the defaults that ResultSet get* 
063         * methods return in the event of a NULL column.
064         */
065        private static final Map primitiveDefaults = new HashMap();
066    
067        static {
068            primitiveDefaults.put(Integer.TYPE, new Integer(0));
069            primitiveDefaults.put(Short.TYPE, new Short((short) 0));
070            primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
071            primitiveDefaults.put(Float.TYPE, new Float(0));
072            primitiveDefaults.put(Double.TYPE, new Double(0));
073            primitiveDefaults.put(Long.TYPE, new Long(0));
074            primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
075            primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
076        }
077    
078        /**
079         * Constructor for BeanProcessor.
080         */
081        public BeanProcessor() {
082            super();
083        }
084    
085        /**
086         * Convert a <code>ResultSet</code> row into a JavaBean.  This 
087         * implementation uses reflection and <code>BeanInfo</code> classes to 
088         * match column names to bean property names.  Properties are matched to 
089         * columns based on several factors:
090         * <br/>
091         * <ol>
092         *     <li>
093         *     The class has a writable property with the same name as a column.
094         *     The name comparison is case insensitive.
095         *     </li>
096         * 
097         *     <li>
098         *     The column type can be converted to the property's set method 
099         *     parameter type with a ResultSet.get* method.  If the conversion fails
100         *     (ie. the property was an int and the column was a Timestamp) an
101         *     SQLException is thrown.
102         *     </li>
103         * </ol>
104         * 
105         * <p>
106         * Primitive bean properties are set to their defaults when SQL NULL is
107         * returned from the <code>ResultSet</code>.  Numeric fields are set to 0
108         * and booleans are set to false.  Object bean properties are set to 
109         * <code>null</code> when SQL NULL is returned.  This is the same behavior
110         * as the <code>ResultSet</code> get* methods.
111         * </p>
112         *
113         * @param rs ResultSet that supplies the bean data
114         * @param type Class from which to create the bean instance
115         * @throws SQLException if a database access error occurs
116         * @return the newly created bean
117         */
118        public Object toBean(ResultSet rs, Class type) throws SQLException {
119    
120            PropertyDescriptor[] props = this.propertyDescriptors(type);
121    
122            ResultSetMetaData rsmd = rs.getMetaData();
123            int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
124    
125            return this.createBean(rs, type, props, columnToProperty);
126        }
127    
128        /**
129         * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans.  
130         * This implementation uses reflection and <code>BeanInfo</code> classes to 
131         * match column names to bean property names. Properties are matched to 
132         * columns based on several factors:
133         * <br/>
134         * <ol>
135         *     <li>
136         *     The class has a writable property with the same name as a column.
137         *     The name comparison is case insensitive.
138         *     </li>
139         * 
140         *     <li>
141         *     The column type can be converted to the property's set method 
142         *     parameter type with a ResultSet.get* method.  If the conversion fails
143         *     (ie. the property was an int and the column was a Timestamp) an
144         *     SQLException is thrown.
145         *     </li>
146         * </ol>
147         * 
148         * <p>
149         * Primitive bean properties are set to their defaults when SQL NULL is
150         * returned from the <code>ResultSet</code>.  Numeric fields are set to 0
151         * and booleans are set to false.  Object bean properties are set to 
152         * <code>null</code> when SQL NULL is returned.  This is the same behavior
153         * as the <code>ResultSet</code> get* methods.
154         * </p>
155         *
156         * @param rs ResultSet that supplies the bean data
157         * @param type Class from which to create the bean instance
158         * @throws SQLException if a database access error occurs
159         * @return the newly created List of beans
160         */
161        public List toBeanList(ResultSet rs, Class type) throws SQLException {
162            List results = new ArrayList();
163    
164            if (!rs.next()) {
165                return results;
166            }
167    
168            PropertyDescriptor[] props = this.propertyDescriptors(type);
169            ResultSetMetaData rsmd = rs.getMetaData();
170            int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);
171    
172            do {
173                results.add(this.createBean(rs, type, props, columnToProperty));
174            } while (rs.next());
175    
176            return results;
177        }
178    
179        /**
180         * Creates a new object and initializes its fields from the ResultSet.
181         *
182         * @param rs The result set.
183         * @param type The bean type (the return type of the object).
184         * @param props The property descriptors.
185         * @param columnToProperty The column indices in the result set.
186         * @return An initialized object.
187         * @throws SQLException if a database error occurs.
188         */
189        private Object createBean(ResultSet rs, Class type,
190                PropertyDescriptor[] props, int[] columnToProperty)
191                throws SQLException {
192    
193            Object bean = this.newInstance(type);
194    
195            for (int i = 1; i < columnToProperty.length; i++) {
196    
197                if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
198                    continue;
199                }
200    
201                PropertyDescriptor prop = props[columnToProperty[i]];
202                Class propType = prop.getPropertyType();
203    
204                Object value = this.processColumn(rs, i, propType);
205    
206                if (propType != null && value == null && propType.isPrimitive()) {
207                    value = primitiveDefaults.get(propType);
208                }
209    
210                this.callSetter(bean, prop, value);
211            }
212    
213            return bean;
214        }
215    
216        /**
217         * Calls the setter method on the target object for the given property.
218         * If no setter method exists for the property, this method does nothing.
219         * @param target The object to set the property on.
220         * @param prop The property to set.
221         * @param value The value to pass into the setter.
222         * @throws SQLException if an error occurs setting the property.
223         */
224        private void callSetter(Object target, PropertyDescriptor prop, Object value)
225                throws SQLException {
226    
227            Method setter = prop.getWriteMethod();
228    
229            if (setter == null) {
230                return;
231            }
232    
233            Class[] params = setter.getParameterTypes();
234            try {
235                // convert types for some popular ones
236                if (value != null) {
237                    if (value instanceof java.util.Date) {
238                        if (params[0].getName().equals("java.sql.Date")) {
239                            value = new java.sql.Date(((java.util.Date) value).getTime());
240                        } else
241                        if (params[0].getName().equals("java.sql.Time")) {
242                            value = new java.sql.Time(((java.util.Date) value).getTime());
243                        } else
244                        if (params[0].getName().equals("java.sql.Timestamp")) {
245                            value = new java.sql.Timestamp(((java.util.Date) value).getTime());
246                        }
247                    }
248                }
249    
250                // Don't call setter if the value object isn't the right type 
251                if (this.isCompatibleType(value, params[0])) {
252                    setter.invoke(target, new Object[] { value });
253                } else {
254                  throw new SQLException(
255                      "Cannot set " + prop.getName() + ": incompatible types.");
256                }
257    
258            } catch (IllegalArgumentException e) {
259                throw new SQLException(
260                    "Cannot set " + prop.getName() + ": " + e.getMessage());
261    
262            } catch (IllegalAccessException e) {
263                throw new SQLException(
264                    "Cannot set " + prop.getName() + ": " + e.getMessage());
265    
266            } catch (InvocationTargetException e) {
267                throw new SQLException(
268                    "Cannot set " + prop.getName() + ": " + e.getMessage());
269            }
270        }
271    
272        /**
273         * ResultSet.getObject() returns an Integer object for an INT column.  The
274         * setter method for the property might take an Integer or a primitive int.
275         * This method returns true if the value can be successfully passed into
276         * the setter method.  Remember, Method.invoke() handles the unwrapping
277         * of Integer into an int.
278         * 
279         * @param value The value to be passed into the setter method.
280         * @param type The setter's parameter type.
281         * @return boolean True if the value is compatible.
282         */
283        private boolean isCompatibleType(Object value, Class type) {
284            // Do object check first, then primitives
285            if (value == null || type.isInstance(value)) {
286                return true;
287    
288            } else if (
289                type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
290                return true;
291    
292            } else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
293                return true;
294    
295            } else if (
296                type.equals(Double.TYPE) && Double.class.isInstance(value)) {
297                return true;
298    
299            } else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
300                return true;
301    
302            } else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
303                return true;
304    
305            } else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
306                return true;
307    
308            } else if (
309                type.equals(Character.TYPE) && Character.class.isInstance(value)) {
310                return true;
311    
312            } else if (
313                type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
314                return true;
315    
316            } else {
317                return false;
318            }
319    
320        }
321    
322        /**
323         * Factory method that returns a new instance of the given Class.  This
324         * is called at the start of the bean creation process and may be 
325         * overridden to provide custom behavior like returning a cached bean
326         * instance.
327         *
328         * @param c The Class to create an object from.
329         * @return A newly created object of the Class.
330         * @throws SQLException if creation failed.
331         */
332        protected Object newInstance(Class c) throws SQLException {
333            try {
334                return c.newInstance();
335    
336            } catch (InstantiationException e) {
337                throw new SQLException(
338                    "Cannot create " + c.getName() + ": " + e.getMessage());
339    
340            } catch (IllegalAccessException e) {
341                throw new SQLException(
342                    "Cannot create " + c.getName() + ": " + e.getMessage());
343            }
344        }
345    
346        /**
347         * Returns a PropertyDescriptor[] for the given Class.
348         *
349         * @param c The Class to retrieve PropertyDescriptors for.
350         * @return A PropertyDescriptor[] describing the Class.
351         * @throws SQLException if introspection failed.
352         */
353        private PropertyDescriptor[] propertyDescriptors(Class c)
354            throws SQLException {
355            // Introspector caches BeanInfo classes for better performance
356            BeanInfo beanInfo = null;
357            try {
358                beanInfo = Introspector.getBeanInfo(c);
359    
360            } catch (IntrospectionException e) {
361                throw new SQLException(
362                    "Bean introspection failed: " + e.getMessage());
363            }
364    
365            return beanInfo.getPropertyDescriptors();
366        }
367    
368        /**
369         * The positions in the returned array represent column numbers.  The 
370         * values stored at each position represent the index in the 
371         * <code>PropertyDescriptor[]</code> for the bean property that matches 
372         * the column name.  If no bean property was found for a column, the 
373         * position is set to <code>PROPERTY_NOT_FOUND</code>.
374         * 
375         * @param rsmd The <code>ResultSetMetaData</code> containing column 
376         * information.
377         * 
378         * @param props The bean property descriptors.
379         * 
380         * @throws SQLException if a database access error occurs
381         *
382         * @return An int[] with column index to property index mappings.  The 0th 
383         * element is meaningless because JDBC column indexing starts at 1.
384         */
385        protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
386                PropertyDescriptor[] props) throws SQLException {
387    
388            int cols = rsmd.getColumnCount();
389            int columnToProperty[] = new int[cols + 1];
390            Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
391    
392            for (int col = 1; col <= cols; col++) {
393                String columnName = rsmd.getColumnName(col);
394                for (int i = 0; i < props.length; i++) {
395    
396                    if (columnName.equalsIgnoreCase(props[i].getName())) {
397                        columnToProperty[col] = i;
398                        break;
399                    }
400                }
401            }
402    
403            return columnToProperty;
404        }
405    
406        /**
407         * Convert a <code>ResultSet</code> column into an object.  Simple 
408         * implementations could just call <code>rs.getObject(index)</code> while
409         * more complex implementations could perform type manipulation to match 
410         * the column's type to the bean property type.
411         * 
412         * <p>
413         * This implementation calls the appropriate <code>ResultSet</code> getter 
414         * method for the given property type to perform the type conversion.  If 
415         * the property type doesn't match one of the supported 
416         * <code>ResultSet</code> types, <code>getObject</code> is called.
417         * </p>
418         * 
419         * @param rs The <code>ResultSet</code> currently being processed.  It is
420         * positioned on a valid row before being passed into this method.
421         * 
422         * @param index The current column index being processed.
423         * 
424         * @param propType The bean property type that this column needs to be
425         * converted into.
426         * 
427         * @throws SQLException if a database access error occurs
428         * 
429         * @return The object from the <code>ResultSet</code> at the given column
430         * index after optional type processing or <code>null</code> if the column
431         * value was SQL NULL.
432         */
433        protected Object processColumn(ResultSet rs, int index, Class propType)
434            throws SQLException {
435    
436            if ( !propType.isPrimitive() && rs.getObject(index) == null ) {
437                return null;
438            }
439    
440            if (propType.equals(String.class)) {
441                return rs.getString(index);
442    
443            } else if (
444                propType.equals(Integer.TYPE) || propType.equals(Integer.class)) {
445                return new Integer(rs.getInt(index));
446    
447            } else if (
448                propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) {
449                return new Boolean(rs.getBoolean(index));
450    
451            } else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
452                return new Long(rs.getLong(index));
453    
454            } else if (
455                propType.equals(Double.TYPE) || propType.equals(Double.class)) {
456                return new Double(rs.getDouble(index));
457    
458            } else if (
459                propType.equals(Float.TYPE) || propType.equals(Float.class)) {
460                return new Float(rs.getFloat(index));
461    
462            } else if (
463                propType.equals(Short.TYPE) || propType.equals(Short.class)) {
464                return new Short(rs.getShort(index));
465    
466            } else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
467                return new Byte(rs.getByte(index));
468    
469            } else if (propType.equals(Timestamp.class)) {
470                return rs.getTimestamp(index);
471    
472            } else {
473                return rs.getObject(index);
474            }
475    
476        }
477    
478    }