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,
124: props);
125:
126: return this .createBean(rs, type, props, columnToProperty);
127: }
128:
129: /**
130: * Convert a <code>ResultSet</code> into a <code>List</code> of JavaBeans.
131: * This implementation uses reflection and <code>BeanInfo</code> classes to
132: * match column names to bean property names. Properties are matched to
133: * columns based on several factors:
134: * <br/>
135: * <ol>
136: * <li>
137: * The class has a writable property with the same name as a column.
138: * The name comparison is case insensitive.
139: * </li>
140: *
141: * <li>
142: * The column type can be converted to the property's set method
143: * parameter type with a ResultSet.get* method. If the conversion fails
144: * (ie. the property was an int and the column was a Timestamp) an
145: * SQLException is thrown.
146: * </li>
147: * </ol>
148: *
149: * <p>
150: * Primitive bean properties are set to their defaults when SQL NULL is
151: * returned from the <code>ResultSet</code>. Numeric fields are set to 0
152: * and booleans are set to false. Object bean properties are set to
153: * <code>null</code> when SQL NULL is returned. This is the same behavior
154: * as the <code>ResultSet</code> get* methods.
155: * </p>
156: *
157: * @param rs ResultSet that supplies the bean data
158: * @param type Class from which to create the bean instance
159: * @throws SQLException if a database access error occurs
160: * @return the newly created List of beans
161: */
162: public List toBeanList(ResultSet rs, Class type)
163: throws SQLException {
164: List results = new ArrayList();
165:
166: if (!rs.next()) {
167: return results;
168: }
169:
170: PropertyDescriptor[] props = this .propertyDescriptors(type);
171: ResultSetMetaData rsmd = rs.getMetaData();
172: int[] columnToProperty = this .mapColumnsToProperties(rsmd,
173: props);
174:
175: do {
176: results.add(this .createBean(rs, type, props,
177: columnToProperty));
178: } while (rs.next());
179:
180: return results;
181: }
182:
183: /**
184: * Creates a new object and initializes its fields from the ResultSet.
185: *
186: * @param rs The result set.
187: * @param type The bean type (the return type of the object).
188: * @param props The property descriptors.
189: * @param columnToProperty The column indices in the result set.
190: * @return An initialized object.
191: * @throws SQLException if a database error occurs.
192: */
193: private Object createBean(ResultSet rs, Class type,
194: PropertyDescriptor[] props, int[] columnToProperty)
195: throws SQLException {
196:
197: Object bean = this .newInstance(type);
198:
199: for (int i = 1; i < columnToProperty.length; i++) {
200:
201: if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
202: continue;
203: }
204:
205: PropertyDescriptor prop = props[columnToProperty[i]];
206: Class propType = prop.getPropertyType();
207:
208: Object value = this .processColumn(rs, i, propType);
209:
210: if (propType != null && value == null
211: && propType.isPrimitive()) {
212: value = primitiveDefaults.get(propType);
213: }
214:
215: this .callSetter(bean, prop, value);
216: }
217:
218: return bean;
219: }
220:
221: /**
222: * Calls the setter method on the target object for the given property.
223: * If no setter method exists for the property, this method does nothing.
224: * @param target The object to set the property on.
225: * @param prop The property to set.
226: * @param value The value to pass into the setter.
227: * @throws SQLException if an error occurs setting the property.
228: */
229: private void callSetter(Object target, PropertyDescriptor prop,
230: Object value) throws SQLException {
231:
232: Method setter = prop.getWriteMethod();
233:
234: if (setter == null) {
235: return;
236: }
237:
238: Class[] params = setter.getParameterTypes();
239: try {
240: // convert types for some popular ones
241: if (value != null) {
242: if (value instanceof java.util.Date) {
243: if (params[0].getName().equals("java.sql.Date")) {
244: value = new java.sql.Date(
245: ((java.util.Date) value).getTime());
246: } else if (params[0].getName().equals(
247: "java.sql.Time")) {
248: value = new java.sql.Time(
249: ((java.util.Date) value).getTime());
250: } else if (params[0].getName().equals(
251: "java.sql.Timestamp")) {
252: value = new java.sql.Timestamp(
253: ((java.util.Date) value).getTime());
254: }
255: }
256: }
257:
258: // Don't call setter if the value object isn't the right type
259: if (this .isCompatibleType(value, params[0])) {
260: setter.invoke(target, new Object[] { value });
261: } else {
262: throw new SQLException("Cannot set " + prop.getName()
263: + ": incompatible types.");
264: }
265:
266: } catch (IllegalArgumentException e) {
267: throw new SQLException("Cannot set " + prop.getName()
268: + ": " + e.getMessage());
269:
270: } catch (IllegalAccessException e) {
271: throw new SQLException("Cannot set " + prop.getName()
272: + ": " + e.getMessage());
273:
274: } catch (InvocationTargetException e) {
275: throw new SQLException("Cannot set " + prop.getName()
276: + ": " + e.getMessage());
277: }
278: }
279:
280: /**
281: * ResultSet.getObject() returns an Integer object for an INT column. The
282: * setter method for the property might take an Integer or a primitive int.
283: * This method returns true if the value can be successfully passed into
284: * the setter method. Remember, Method.invoke() handles the unwrapping
285: * of Integer into an int.
286: *
287: * @param value The value to be passed into the setter method.
288: * @param type The setter's parameter type.
289: * @return boolean True if the value is compatible.
290: */
291: private boolean isCompatibleType(Object value, Class type) {
292: // Do object check first, then primitives
293: if (value == null || type.isInstance(value)) {
294: return true;
295:
296: } else if (type.equals(Integer.TYPE)
297: && Integer.class.isInstance(value)) {
298: return true;
299:
300: } else if (type.equals(Long.TYPE)
301: && Long.class.isInstance(value)) {
302: return true;
303:
304: } else if (type.equals(Double.TYPE)
305: && Double.class.isInstance(value)) {
306: return true;
307:
308: } else if (type.equals(Float.TYPE)
309: && Float.class.isInstance(value)) {
310: return true;
311:
312: } else if (type.equals(Short.TYPE)
313: && Short.class.isInstance(value)) {
314: return true;
315:
316: } else if (type.equals(Byte.TYPE)
317: && Byte.class.isInstance(value)) {
318: return true;
319:
320: } else if (type.equals(Character.TYPE)
321: && Character.class.isInstance(value)) {
322: return true;
323:
324: } else if (type.equals(Boolean.TYPE)
325: && Boolean.class.isInstance(value)) {
326: return true;
327:
328: } else {
329: return false;
330: }
331:
332: }
333:
334: /**
335: * Factory method that returns a new instance of the given Class. This
336: * is called at the start of the bean creation process and may be
337: * overridden to provide custom behavior like returning a cached bean
338: * instance.
339: *
340: * @param c The Class to create an object from.
341: * @return A newly created object of the Class.
342: * @throws SQLException if creation failed.
343: */
344: protected Object newInstance(Class c) throws SQLException {
345: try {
346: return c.newInstance();
347:
348: } catch (InstantiationException e) {
349: throw new SQLException("Cannot create " + c.getName()
350: + ": " + e.getMessage());
351:
352: } catch (IllegalAccessException e) {
353: throw new SQLException("Cannot create " + c.getName()
354: + ": " + e.getMessage());
355: }
356: }
357:
358: /**
359: * Returns a PropertyDescriptor[] for the given Class.
360: *
361: * @param c The Class to retrieve PropertyDescriptors for.
362: * @return A PropertyDescriptor[] describing the Class.
363: * @throws SQLException if introspection failed.
364: */
365: private PropertyDescriptor[] propertyDescriptors(Class c)
366: throws SQLException {
367: // Introspector caches BeanInfo classes for better performance
368: BeanInfo beanInfo = null;
369: try {
370: beanInfo = Introspector.getBeanInfo(c);
371:
372: } catch (IntrospectionException e) {
373: throw new SQLException("Bean introspection failed: "
374: + e.getMessage());
375: }
376:
377: return beanInfo.getPropertyDescriptors();
378: }
379:
380: /**
381: * The positions in the returned array represent column numbers. The
382: * values stored at each position represent the index in the
383: * <code>PropertyDescriptor[]</code> for the bean property that matches
384: * the column name. If no bean property was found for a column, the
385: * position is set to <code>PROPERTY_NOT_FOUND</code>.
386: *
387: * @param rsmd The <code>ResultSetMetaData</code> containing column
388: * information.
389: *
390: * @param props The bean property descriptors.
391: *
392: * @throws SQLException if a database access error occurs
393: *
394: * @return An int[] with column index to property index mappings. The 0th
395: * element is meaningless because JDBC column indexing starts at 1.
396: */
397: protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
398: PropertyDescriptor[] props) throws SQLException {
399:
400: int cols = rsmd.getColumnCount();
401: int columnToProperty[] = new int[cols + 1];
402: Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);
403:
404: for (int col = 1; col <= cols; col++) {
405: String columnName = rsmd.getColumnName(col);
406: for (int i = 0; i < props.length; i++) {
407:
408: if (columnName.equalsIgnoreCase(props[i].getName())) {
409: columnToProperty[col] = i;
410: break;
411: }
412: }
413: }
414:
415: return columnToProperty;
416: }
417:
418: /**
419: * Convert a <code>ResultSet</code> column into an object. Simple
420: * implementations could just call <code>rs.getObject(index)</code> while
421: * more complex implementations could perform type manipulation to match
422: * the column's type to the bean property type.
423: *
424: * <p>
425: * This implementation calls the appropriate <code>ResultSet</code> getter
426: * method for the given property type to perform the type conversion. If
427: * the property type doesn't match one of the supported
428: * <code>ResultSet</code> types, <code>getObject</code> is called.
429: * </p>
430: *
431: * @param rs The <code>ResultSet</code> currently being processed. It is
432: * positioned on a valid row before being passed into this method.
433: *
434: * @param index The current column index being processed.
435: *
436: * @param propType The bean property type that this column needs to be
437: * converted into.
438: *
439: * @throws SQLException if a database access error occurs
440: *
441: * @return The object from the <code>ResultSet</code> at the given column
442: * index after optional type processing or <code>null</code> if the column
443: * value was SQL NULL.
444: */
445: protected Object processColumn(ResultSet rs, int index,
446: Class propType) throws SQLException {
447:
448: if (propType.equals(String.class)) {
449: return rs.getString(index);
450:
451: } else if (propType.equals(Integer.TYPE)
452: || propType.equals(Integer.class)) {
453: return new Integer(rs.getInt(index));
454:
455: } else if (propType.equals(Boolean.TYPE)
456: || propType.equals(Boolean.class)) {
457: return new Boolean(rs.getBoolean(index));
458:
459: } else if (propType.equals(Long.TYPE)
460: || propType.equals(Long.class)) {
461: return new Long(rs.getLong(index));
462:
463: } else if (propType.equals(Double.TYPE)
464: || propType.equals(Double.class)) {
465: return new Double(rs.getDouble(index));
466:
467: } else if (propType.equals(Float.TYPE)
468: || propType.equals(Float.class)) {
469: return new Float(rs.getFloat(index));
470:
471: } else if (propType.equals(Short.TYPE)
472: || propType.equals(Short.class)) {
473: return new Short(rs.getShort(index));
474:
475: } else if (propType.equals(Byte.TYPE)
476: || propType.equals(Byte.class)) {
477: return new Byte(rs.getByte(index));
478:
479: } else if (propType.equals(Timestamp.class)) {
480: return rs.getTimestamp(index);
481:
482: } else {
483: return rs.getObject(index);
484: }
485:
486: }
487:
488: }
|