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 }