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, Integer.valueOf(0)); 069 primitiveDefaults.put(Short.TYPE, Short.valueOf((short) 0)); 070 primitiveDefaults.put(Byte.TYPE, Byte.valueOf((byte) 0)); 071 primitiveDefaults.put(Float.TYPE, Float.valueOf(0f)); 072 primitiveDefaults.put(Double.TYPE, Double.valueOf(0d)); 073 primitiveDefaults.put(Long.TYPE, Long.valueOf(0L)); 074 primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE); 075 primitiveDefaults.put(Character.TYPE, Character.valueOf((char) 0)); 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 (type.equals(Integer.TYPE) && Integer.class.isInstance(value)) { 289 return true; 290 291 } else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) { 292 return true; 293 294 } else if (type.equals(Double.TYPE) && Double.class.isInstance(value)) { 295 return true; 296 297 } else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) { 298 return true; 299 300 } else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) { 301 return true; 302 303 } else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) { 304 return true; 305 306 } else if (type.equals(Character.TYPE) && Character.class.isInstance(value)) { 307 return true; 308 309 } else if (type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) { 310 return true; 311 312 } 313 return false; 314 315 } 316 317 /** 318 * Factory method that returns a new instance of the given Class. This 319 * is called at the start of the bean creation process and may be 320 * overridden to provide custom behavior like returning a cached bean 321 * instance. 322 * @param <T> The type of object to create 323 * @param c The Class to create an object from. 324 * @return A newly created object of the Class. 325 * @throws SQLException if creation failed. 326 */ 327 protected <T> T newInstance(Class<T> c) throws SQLException { 328 try { 329 return c.newInstance(); 330 331 } catch (InstantiationException e) { 332 throw new SQLException( 333 "Cannot create " + c.getName() + ": " + e.getMessage()); 334 335 } catch (IllegalAccessException e) { 336 throw new SQLException( 337 "Cannot create " + c.getName() + ": " + e.getMessage()); 338 } 339 } 340 341 /** 342 * Returns a PropertyDescriptor[] for the given Class. 343 * 344 * @param c The Class to retrieve PropertyDescriptors for. 345 * @return A PropertyDescriptor[] describing the Class. 346 * @throws SQLException if introspection failed. 347 */ 348 private PropertyDescriptor[] propertyDescriptors(Class<?> c) 349 throws SQLException { 350 // Introspector caches BeanInfo classes for better performance 351 BeanInfo beanInfo = null; 352 try { 353 beanInfo = Introspector.getBeanInfo(c); 354 355 } catch (IntrospectionException e) { 356 throw new SQLException( 357 "Bean introspection failed: " + e.getMessage()); 358 } 359 360 return beanInfo.getPropertyDescriptors(); 361 } 362 363 /** 364 * The positions in the returned array represent column numbers. The 365 * values stored at each position represent the index in the 366 * <code>PropertyDescriptor[]</code> for the bean property that matches 367 * the column name. If no bean property was found for a column, the 368 * position is set to <code>PROPERTY_NOT_FOUND</code>. 369 * 370 * @param rsmd The <code>ResultSetMetaData</code> containing column 371 * information. 372 * 373 * @param props The bean property descriptors. 374 * 375 * @throws SQLException if a database access error occurs 376 * 377 * @return An int[] with column index to property index mappings. The 0th 378 * element is meaningless because JDBC column indexing starts at 1. 379 */ 380 protected int[] mapColumnsToProperties(ResultSetMetaData rsmd, 381 PropertyDescriptor[] props) throws SQLException { 382 383 int cols = rsmd.getColumnCount(); 384 int[] columnToProperty = new int[cols + 1]; 385 Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND); 386 387 for (int col = 1; col <= cols; col++) { 388 String columnName = rsmd.getColumnLabel(col); 389 if (null == columnName || 0 == columnName.length()) { 390 columnName = rsmd.getColumnName(col); 391 } 392 for (int i = 0; i < props.length; i++) { 393 394 if (columnName.equalsIgnoreCase(props[i].getName())) { 395 columnToProperty[col] = i; 396 break; 397 } 398 } 399 } 400 401 return columnToProperty; 402 } 403 404 /** 405 * Convert a <code>ResultSet</code> column into an object. Simple 406 * implementations could just call <code>rs.getObject(index)</code> while 407 * more complex implementations could perform type manipulation to match 408 * the column's type to the bean property type. 409 * 410 * <p> 411 * This implementation calls the appropriate <code>ResultSet</code> getter 412 * method for the given property type to perform the type conversion. If 413 * the property type doesn't match one of the supported 414 * <code>ResultSet</code> types, <code>getObject</code> is called. 415 * </p> 416 * 417 * @param rs The <code>ResultSet</code> currently being processed. It is 418 * positioned on a valid row before being passed into this method. 419 * 420 * @param index The current column index being processed. 421 * 422 * @param propType The bean property type that this column needs to be 423 * converted into. 424 * 425 * @throws SQLException if a database access error occurs 426 * 427 * @return The object from the <code>ResultSet</code> at the given column 428 * index after optional type processing or <code>null</code> if the column 429 * value was SQL NULL. 430 */ 431 protected Object processColumn(ResultSet rs, int index, Class<?> propType) 432 throws SQLException { 433 434 if ( !propType.isPrimitive() && rs.getObject(index) == null ) { 435 return null; 436 } 437 438 if (propType.equals(String.class)) { 439 return rs.getString(index); 440 441 } else if ( 442 propType.equals(Integer.TYPE) || propType.equals(Integer.class)) { 443 return Integer.valueOf(rs.getInt(index)); 444 445 } else if ( 446 propType.equals(Boolean.TYPE) || propType.equals(Boolean.class)) { 447 return Boolean.valueOf(rs.getBoolean(index)); 448 449 } else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) { 450 return Long.valueOf(rs.getLong(index)); 451 452 } else if ( 453 propType.equals(Double.TYPE) || propType.equals(Double.class)) { 454 return Double.valueOf(rs.getDouble(index)); 455 456 } else if ( 457 propType.equals(Float.TYPE) || propType.equals(Float.class)) { 458 return Float.valueOf(rs.getFloat(index)); 459 460 } else if ( 461 propType.equals(Short.TYPE) || propType.equals(Short.class)) { 462 return Short.valueOf(rs.getShort(index)); 463 464 } else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) { 465 return Byte.valueOf(rs.getByte(index)); 466 467 } else if (propType.equals(Timestamp.class)) { 468 return rs.getTimestamp(index); 469 470 } else { 471 return rs.getObject(index); 472 } 473 474 } 475 476 }