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<Class<?>, Object> primitiveDefaults = new HashMap<Class<?>, Object>();
066
067 static {
068 primitiveDefaults.put(Integer.TYPE, 0);
069 primitiveDefaults.put(Short.TYPE, (Short)((short) 0));
070 primitiveDefaults.put(Byte.TYPE, (Byte)((byte) 0));
071 primitiveDefaults.put(Float.TYPE, (Float)(float)(0));
072 primitiveDefaults.put(Double.TYPE, (Double)(double)(0));
073 primitiveDefaults.put(Long.TYPE, (Long)(0L));
074 primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
075 primitiveDefaults.put(Character.TYPE, '\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 * @param <T> The type of bean to create
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 <T> T toBean(ResultSet rs, Class<T> 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 * @param <T> The type of bean to create
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 <T> List<T> toBeanList(ResultSet rs, Class<T> type) throws SQLException {
162 List<T> results = new ArrayList<T>();
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 * @param <T> The type of bean to create
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 <T> T createBean(ResultSet rs, Class<T> type,
190 PropertyDescriptor[] props, int[] columnToProperty)
191 throws SQLException {
192
193 T 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 * @param <T> The type of object to create
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 <T> T newInstance(Class<T> 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.getColumnLabel(col);
394 if (null == columnName || 0 == columnName.length()) {
395 columnName = rsmd.getColumnName(col);
396 }
397 for (int i = 0; i < props.length; i++) {
398
399 if (columnName.equalsIgnoreCase(props[i].getName())) {
400 columnToProperty[col] = i;
401 break;
402 }
403 }
404 }
405
406 return columnToProperty;
407 }
408
409 /**
410 * Convert a <code>ResultSet</code> column into an object. Simple
411 * implementations could just call <code>rs.getObject(index)</code> while
412 * more complex implementations could perform type manipulation to match
413 * the column's type to the bean property type.
414 *
415 * <p>
416 * This implementation calls the appropriate <code>ResultSet</code> getter
417 * method for the given property type to perform the type conversion. If
418 * the property type doesn't match one of the supported
419 * <code>ResultSet</code> types, <code>getObject</code> is called.
420 * </p>
421 *
422 * @param rs The <code>ResultSet</code> currently being processed. It is
423 * positioned on a valid row before being passed into this method.
424 *
425 * @param index The current column index being processed.
426 *
427 * @param propType The bean property type that this column needs to be
428 * converted into.
429 *
430 * @throws SQLException if a database access error occurs
431 *
432 * @return The object from the <code>ResultSet</code> at the given column
433 * index after optional type processing or <code>null</code> if the column
434 * value was SQL NULL.
435 */
436 protected Object processColumn(ResultSet rs, int index, Class<?> propType)
437 throws SQLException {
438
439 if ( !propType.isPrimitive() && rs.getObject(index) == null ) {
440 return null;
441 }
442
443 if ( !propType.isPrimitive() && rs.getObject(index) == null ) {
444 return null;
445 }
446
447 if (propType.equals(String.class)) {
448 return rs.getString(index);
449
450 } else if (
451 propType.equals(Integer.TYPE) || propType.equals(Integer.class)) {
452 return (rs.getInt(index));
453
454 } else if (
455 propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) {
456 return (rs.getBoolean(index));
457
458 } else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
459 return (rs.getLong(index));
460
461 } else if (
462 propType.equals(Double.TYPE) || propType.equals(Double.class)) {
463 return (rs.getDouble(index));
464
465 } else if (
466 propType.equals(Float.TYPE) || propType.equals(Float.class)) {
467 return (rs.getFloat(index));
468
469 } else if (
470 propType.equals(Short.TYPE) || propType.equals(Short.class)) {
471 return (rs.getShort(index));
472
473 } else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
474 return (rs.getByte(index));
475
476 } else if (propType.equals(Timestamp.class)) {
477 return rs.getTimestamp(index);
478
479 } else {
480 return rs.getObject(index);
481 }
482
483 }
484
485 }