View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.dbutils;
18  
19  import java.beans.BeanInfo;
20  import java.beans.IntrospectionException;
21  import java.beans.Introspector;
22  import java.beans.PropertyDescriptor;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.sql.ResultSet;
26  import java.sql.ResultSetMetaData;
27  import java.sql.SQLException;
28  import java.sql.Timestamp;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  
35  /**
36   * <p>
37   * <code>BeanProcessor</code> matches column names to bean property names 
38   * and converts <code>ResultSet</code> columns into objects for those bean 
39   * properties.  Subclasses should override the methods in the processing chain
40   * to customize behavior.
41   * </p>
42   * 
43   * <p>
44   * This class is thread-safe.
45   * </p>
46   * 
47   * @see BasicRowProcessor
48   * 
49   * @since DbUtils 1.1
50   */
51  public class BeanProcessor {
52      
53      /**
54       * Special array value used by <code>mapColumnsToProperties</code> that 
55       * indicates there is no bean property that matches a column from a 
56       * <code>ResultSet</code>.
57       */
58      protected static final int PROPERTY_NOT_FOUND = -1;
59      
60      /**
61       * Set a bean's primitive properties to these defaults when SQL NULL 
62       * is returned.  These are the same as the defaults that ResultSet get* 
63       * methods return in the event of a NULL column.
64       */
65      private static final Map primitiveDefaults = new HashMap();
66  
67      static {
68          primitiveDefaults.put(Integer.TYPE, new Integer(0));
69          primitiveDefaults.put(Short.TYPE, new Short((short) 0));
70          primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
71          primitiveDefaults.put(Float.TYPE, new Float(0));
72          primitiveDefaults.put(Double.TYPE, new Double(0));
73          primitiveDefaults.put(Long.TYPE, new Long(0));
74          primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
75          primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
76      }
77  
78      /**
79       * Constructor for BeanProcessor.
80       */
81      public BeanProcessor() {
82          super();
83      }
84      
85      /**
86       * Convert a <code>ResultSet</code> row into a JavaBean.  This 
87       * implementation uses reflection and <code>BeanInfo</code> classes to 
88       * match column names to bean property names.  Properties are matched to 
89       * columns based on several factors:
90       * <br/>
91       * <ol>
92       *     <li>
93       *     The class has a writable property with the same name as a column.
94       *     The name comparison is case insensitive.
95       *     </li>
96       * 
97       *     <li>
98       *     The column type can be converted to the property's set method 
99       *     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.equals(String.class)) {
437             return rs.getString(index);
438             
439         } else if (
440             propType.equals(Integer.TYPE) || propType.equals(Integer.class)) {
441             return new Integer(rs.getInt(index));
442 
443         } else if (
444             propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) {
445             return new Boolean(rs.getBoolean(index));
446 
447         } else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
448             return new Long(rs.getLong(index));
449 
450         } else if (
451             propType.equals(Double.TYPE) || propType.equals(Double.class)) {
452             return new Double(rs.getDouble(index));
453 
454         } else if (
455             propType.equals(Float.TYPE) || propType.equals(Float.class)) {
456             return new Float(rs.getFloat(index));
457 
458         } else if (
459             propType.equals(Short.TYPE) || propType.equals(Short.class)) {
460             return new Short(rs.getShort(index));
461 
462         } else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
463             return new Byte(rs.getByte(index));
464             
465         } else if (propType.equals(Timestamp.class)) {
466             return rs.getTimestamp(index);
467 
468         } else {
469             return rs.getObject(index);
470         }
471 
472     }
473 
474 }