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 */ 017package org.apache.commons.dbutils; 018 019import org.apache.commons.dbutils.annotations.Column; 020 021import java.beans.BeanInfo; 022import java.beans.IntrospectionException; 023import java.beans.Introspector; 024import java.beans.PropertyDescriptor; 025import java.lang.reflect.Field; 026import java.lang.reflect.InvocationTargetException; 027import java.lang.reflect.Method; 028import java.sql.ResultSet; 029import java.sql.ResultSetMetaData; 030import java.sql.SQLException; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.ServiceLoader; 037 038/** 039 * <p> 040 * {@code BeanProcessor} matches column names to bean property names 041 * and converts {@code ResultSet} columns into objects for those bean 042 * properties. Subclasses should override the methods in the processing chain 043 * to customize behavior. 044 * </p> 045 * 046 * <p> 047 * This class is thread-safe. 048 * </p> 049 * 050 * @see BasicRowProcessor 051 * 052 * @since DbUtils 1.1 053 */ 054public class BeanProcessor { 055 056 /** 057 * Special array value used by {@code mapColumnsToProperties} that 058 * indicates there is no bean property that matches a column from a 059 * {@code ResultSet}. 060 */ 061 protected static final int PROPERTY_NOT_FOUND = -1; 062 063 /** 064 * Set a bean's primitive properties to these defaults when SQL NULL 065 * is returned. These are the same as the defaults that ResultSet get* 066 * methods return in the event of a NULL column. 067 */ 068 private static final Map<Class<?>, Object> primitiveDefaults = new HashMap<>(); 069 070 private static final List<ColumnHandler> columnHandlers = new ArrayList<>(); 071 072 private static final List<PropertyHandler> propertyHandlers = new ArrayList<>(); 073 074 /** 075 * ResultSet column to bean property name overrides. 076 */ 077 private final Map<String, String> columnToPropertyOverrides; 078 079 static { 080 primitiveDefaults.put(Integer.TYPE, Integer.valueOf(0)); 081 primitiveDefaults.put(Short.TYPE, Short.valueOf((short) 0)); 082 primitiveDefaults.put(Byte.TYPE, Byte.valueOf((byte) 0)); 083 primitiveDefaults.put(Float.TYPE, Float.valueOf(0f)); 084 primitiveDefaults.put(Double.TYPE, Double.valueOf(0d)); 085 primitiveDefaults.put(Long.TYPE, Long.valueOf(0L)); 086 primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE); 087 primitiveDefaults.put(Character.TYPE, Character.valueOf((char) 0)); 088 089 // Use a ServiceLoader to find implementations 090 for (final ColumnHandler handler : ServiceLoader.load(ColumnHandler.class)) { 091 columnHandlers.add(handler); 092 } 093 094 // Use a ServiceLoader to find implementations 095 for (final PropertyHandler handler : ServiceLoader.load(PropertyHandler.class)) { 096 propertyHandlers.add(handler); 097 } 098 } 099 100 /** 101 * Constructor for BeanProcessor. 102 */ 103 public BeanProcessor() { 104 this(new HashMap<String, String>()); 105 } 106 107 /** 108 * Constructor for BeanProcessor configured with column to property name overrides. 109 * 110 * @param columnToPropertyOverrides ResultSet column to bean property name overrides 111 * @since 1.5 112 */ 113 public BeanProcessor(final Map<String, String> columnToPropertyOverrides) { 114 super(); 115 if (columnToPropertyOverrides == null) { 116 throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null"); 117 } 118 this.columnToPropertyOverrides = columnToPropertyOverrides; 119 } 120 121 /** 122 * Convert a {@code ResultSet} row into a JavaBean. This 123 * implementation uses reflection and {@code BeanInfo} classes to 124 * match column names to bean property names. Properties are matched to 125 * columns based on several factors: 126 * <br/> 127 * <ol> 128 * <li> 129 * The class has a writable property with the same name as a column. 130 * The name comparison is case insensitive. 131 * </li> 132 * 133 * <li> 134 * The column type can be converted to the property's set method 135 * parameter type with a ResultSet.get* method. If the conversion fails 136 * (ie. the property was an int and the column was a Timestamp) an 137 * SQLException is thrown. 138 * </li> 139 * </ol> 140 * 141 * <p> 142 * Primitive bean properties are set to their defaults when SQL NULL is 143 * returned from the {@code ResultSet}. Numeric fields are set to 0 144 * and booleans are set to false. Object bean properties are set to 145 * {@code null} when SQL NULL is returned. This is the same behavior 146 * as the {@code ResultSet} get* methods. 147 * </p> 148 * @param <T> The type of bean to create 149 * @param rs ResultSet that supplies the bean data 150 * @param type Class from which to create the bean instance 151 * @throws SQLException if a database access error occurs 152 * @return the newly created bean 153 */ 154 public <T> T toBean(final ResultSet rs, final Class<? extends T> type) throws SQLException { 155 final T bean = this.newInstance(type); 156 return this.populateBean(rs, bean); 157 } 158 159 /** 160 * Convert a {@code ResultSet} into a {@code List} of JavaBeans. 161 * This implementation uses reflection and {@code BeanInfo} classes to 162 * match column names to bean property names. Properties are matched to 163 * columns based on several factors: 164 * <br/> 165 * <ol> 166 * <li> 167 * The class has a writable property with the same name as a column. 168 * The name comparison is case insensitive. 169 * </li> 170 * 171 * <li> 172 * The column type can be converted to the property's set method 173 * parameter type with a ResultSet.get* method. If the conversion fails 174 * (ie. the property was an int and the column was a Timestamp) an 175 * SQLException is thrown. 176 * </li> 177 * </ol> 178 * 179 * <p> 180 * Primitive bean properties are set to their defaults when SQL NULL is 181 * returned from the {@code ResultSet}. Numeric fields are set to 0 182 * and booleans are set to false. Object bean properties are set to 183 * {@code null} when SQL NULL is returned. This is the same behavior 184 * as the {@code ResultSet} get* methods. 185 * </p> 186 * @param <T> The type of bean to create 187 * @param rs ResultSet that supplies the bean data 188 * @param type Class from which to create the bean instance 189 * @throws SQLException if a database access error occurs 190 * @return the newly created List of beans 191 */ 192 public <T> List<T> toBeanList(final ResultSet rs, final Class<? extends T> type) throws SQLException { 193 final List<T> results = new ArrayList<>(); 194 195 if (!rs.next()) { 196 return results; 197 } 198 199 final PropertyDescriptor[] props = this.propertyDescriptors(type); 200 final ResultSetMetaData rsmd = rs.getMetaData(); 201 final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); 202 203 do { 204 results.add(this.createBean(rs, type, props, columnToProperty)); 205 } while (rs.next()); 206 207 return results; 208 } 209 210 /** 211 * Creates a new object and initializes its fields from the ResultSet. 212 * @param <T> The type of bean to create 213 * @param rs The result set. 214 * @param type The bean type (the return type of the object). 215 * @param props The property descriptors. 216 * @param columnToProperty The column indices in the result set. 217 * @return An initialized object. 218 * @throws SQLException if a database error occurs. 219 */ 220 private <T> T createBean(final ResultSet rs, final Class<T> type, 221 final PropertyDescriptor[] props, final int[] columnToProperty) 222 throws SQLException { 223 224 final T bean = this.newInstance(type); 225 return populateBean(rs, bean, props, columnToProperty); 226 } 227 228 /** 229 * Initializes the fields of the provided bean from the ResultSet. 230 * @param <T> The type of bean 231 * @param rs The result set. 232 * @param bean The bean to be populated. 233 * @return An initialized object. 234 * @throws SQLException if a database error occurs. 235 */ 236 public <T> T populateBean(final ResultSet rs, final T bean) throws SQLException { 237 final PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass()); 238 final ResultSetMetaData rsmd = rs.getMetaData(); 239 final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); 240 241 return populateBean(rs, bean, props, columnToProperty); 242 } 243 244 /** 245 * This method populates a bean from the ResultSet based upon the underlying meta-data. 246 * 247 * @param <T> The type of bean 248 * @param rs The result set. 249 * @param bean The bean to be populated. 250 * @param props The property descriptors. 251 * @param columnToProperty The column indices in the result set. 252 * @return An initialized object. 253 * @throws SQLException if a database error occurs. 254 */ 255 private <T> T populateBean(final ResultSet rs, final T bean, 256 final PropertyDescriptor[] props, final int[] columnToProperty) 257 throws SQLException { 258 259 for (int i = 1; i < columnToProperty.length; i++) { 260 261 if (columnToProperty[i] == PROPERTY_NOT_FOUND) { 262 continue; 263 } 264 265 final PropertyDescriptor prop = props[columnToProperty[i]]; 266 final Class<?> propType = prop.getPropertyType(); 267 268 Object value = null; 269 if (propType != null) { 270 value = this.processColumn(rs, i, propType); 271 272 if (value == null && propType.isPrimitive()) { 273 value = primitiveDefaults.get(propType); 274 } 275 } 276 277 this.callSetter(bean, prop, value); 278 } 279 280 return bean; 281 } 282 283 /** 284 * Calls the setter method on the target object for the given property. 285 * If no setter method exists for the property, this method does nothing. 286 * @param target The object to set the property on. 287 * @param prop The property to set. 288 * @param value The value to pass into the setter. 289 * @throws SQLException if an error occurs setting the property. 290 */ 291 private void callSetter(final Object target, final PropertyDescriptor prop, Object value) 292 throws SQLException { 293 294 final Method setter = getWriteMethod(target, prop, value); 295 296 if (setter == null || setter.getParameterTypes().length != 1) { 297 return; 298 } 299 300 try { 301 final Class<?> firstParam = setter.getParameterTypes()[0]; 302 for (final PropertyHandler handler : propertyHandlers) { 303 if (handler.match(firstParam, value)) { 304 value = handler.apply(firstParam, value); 305 break; 306 } 307 } 308 309 // Don't call setter if the value object isn't the right type 310 if (this.isCompatibleType(value, firstParam)) { 311 setter.invoke(target, value); 312 } else { 313 throw new SQLException( 314 "Cannot set " + prop.getName() + ": incompatible types, cannot convert " 315 + value.getClass().getName() + " to " + firstParam.getName()); 316 // value cannot be null here because isCompatibleType allows null 317 } 318 319 } catch (final IllegalArgumentException e) { 320 throw new SQLException( 321 "Cannot set " + prop.getName() + ": " + e.getMessage()); 322 323 } catch (final IllegalAccessException e) { 324 throw new SQLException( 325 "Cannot set " + prop.getName() + ": " + e.getMessage()); 326 327 } catch (final InvocationTargetException e) { 328 throw new SQLException( 329 "Cannot set " + prop.getName() + ": " + e.getMessage()); 330 } 331 } 332 333 /** 334 * ResultSet.getObject() returns an Integer object for an INT column. The 335 * setter method for the property might take an Integer or a primitive int. 336 * This method returns true if the value can be successfully passed into 337 * the setter method. Remember, Method.invoke() handles the unwrapping 338 * of Integer into an int. 339 * 340 * @param value The value to be passed into the setter method. 341 * @param type The setter's parameter type (non-null) 342 * @return boolean True if the value is compatible (null => true) 343 */ 344 private boolean isCompatibleType(final Object value, final Class<?> type) { 345 // Do object check first, then primitives 346 if (value == null || type.isInstance(value) || matchesPrimitive(type, value.getClass())) { 347 return true; 348 349 } 350 return false; 351 352 } 353 354 /** 355 * Check whether a value is of the same primitive type as {@code targetType}. 356 * 357 * @param targetType The primitive type to target. 358 * @param valueType The value to match to the primitive type. 359 * @return Whether {@code valueType} can be coerced (e.g. autoboxed) into {@code targetType}. 360 */ 361 private boolean matchesPrimitive(final Class<?> targetType, final Class<?> valueType) { 362 if (!targetType.isPrimitive()) { 363 return false; 364 } 365 366 try { 367 // see if there is a "TYPE" field. This is present for primitive wrappers. 368 final Field typeField = valueType.getField("TYPE"); 369 final Object primitiveValueType = typeField.get(valueType); 370 371 if (targetType == primitiveValueType) { 372 return true; 373 } 374 } catch (final NoSuchFieldException e) { 375 // lacking the TYPE field is a good sign that we're not working with a primitive wrapper. 376 // we can't match for compatibility 377 } catch (final IllegalAccessException e) { 378 // an inaccessible TYPE field is a good sign that we're not working with a primitive wrapper. 379 // nothing to do. we can't match for compatibility 380 } 381 return false; 382 } 383 384 /** 385 * Get the write method to use when setting {@code value} to the {@code target}. 386 * 387 * @param target Object where the write method will be called. 388 * @param prop BeanUtils information. 389 * @param value The value that will be passed to the write method. 390 * @return The {@link java.lang.reflect.Method} to call on {@code target} to write {@code value} or {@code null} if 391 * there is no suitable write method. 392 */ 393 protected Method getWriteMethod(final Object target, final PropertyDescriptor prop, final Object value) { 394 final Method method = prop.getWriteMethod(); 395 return method; 396 } 397 398 /** 399 * Factory method that returns a new instance of the given Class. This 400 * is called at the start of the bean creation process and may be 401 * overridden to provide custom behavior like returning a cached bean 402 * instance. 403 * @param <T> The type of object to create 404 * @param c The Class to create an object from. 405 * @return A newly created object of the Class. 406 * @throws SQLException if creation failed. 407 */ 408 protected <T> T newInstance(final Class<T> c) throws SQLException { 409 try { 410 return c.getDeclaredConstructor().newInstance(); 411 412 } catch (final IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { 413 throw new SQLException( 414 "Cannot create " + c.getName() + ": " + e.getMessage()); 415 } 416 } 417 418 /** 419 * Returns a PropertyDescriptor[] for the given Class. 420 * 421 * @param c The Class to retrieve PropertyDescriptors for. 422 * @return A PropertyDescriptor[] describing the Class. 423 * @throws SQLException if introspection failed. 424 */ 425 private PropertyDescriptor[] propertyDescriptors(final Class<?> c) 426 throws SQLException { 427 // Introspector caches BeanInfo classes for better performance 428 BeanInfo beanInfo = null; 429 try { 430 beanInfo = Introspector.getBeanInfo(c); 431 432 } catch (final IntrospectionException e) { 433 throw new SQLException( 434 "Bean introspection failed: " + e.getMessage()); 435 } 436 437 return beanInfo.getPropertyDescriptors(); 438 } 439 440 /** 441 * The positions in the returned array represent column numbers. The 442 * values stored at each position represent the index in the 443 * {@code PropertyDescriptor[]} for the bean property that matches 444 * the column name. If no bean property was found for a column, the 445 * position is set to {@code PROPERTY_NOT_FOUND}. 446 * 447 * @param rsmd The {@code ResultSetMetaData} containing column 448 * information. 449 * 450 * @param props The bean property descriptors. 451 * 452 * @throws SQLException if a database access error occurs 453 * 454 * @return An int[] with column index to property index mappings. The 0th 455 * element is meaningless because JDBC column indexing starts at 1. 456 */ 457 protected int[] mapColumnsToProperties(final ResultSetMetaData rsmd, 458 final PropertyDescriptor[] props) throws SQLException { 459 460 final int cols = rsmd.getColumnCount(); 461 final int[] columnToProperty = new int[cols + 1]; 462 Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND); 463 464 for (int col = 1; col <= cols; col++) { 465 String columnName = rsmd.getColumnLabel(col); 466 if (null == columnName || 0 == columnName.length()) { 467 columnName = rsmd.getColumnName(col); 468 } 469 String propertyName = columnToPropertyOverrides.get(columnName); 470 if (propertyName == null) { 471 propertyName = columnName; 472 } 473 for (int i = 0; i < props.length; i++) { 474 475 PropertyDescriptor prop = props[i]; 476 Column column = prop.getReadMethod().getAnnotation(Column.class); 477 String propertyColumnName = null; 478 if (column != null) { 479 propertyColumnName = column.name(); 480 } else { 481 propertyColumnName = prop.getName(); 482 } 483 if (propertyName.equalsIgnoreCase(propertyColumnName)) { 484 columnToProperty[col] = i; 485 break; 486 } 487 } 488 } 489 490 return columnToProperty; 491 } 492 493 /** 494 * Convert a {@code ResultSet} column into an object. Simple 495 * implementations could just call {@code rs.getObject(index)} while 496 * more complex implementations could perform type manipulation to match 497 * the column's type to the bean property type. 498 * 499 * <p> 500 * This implementation calls the appropriate {@code ResultSet} getter 501 * method for the given property type to perform the type conversion. If 502 * the property type doesn't match one of the supported 503 * {@code ResultSet} types, {@code getObject} is called. 504 * </p> 505 * 506 * @param rs The {@code ResultSet} currently being processed. It is 507 * positioned on a valid row before being passed into this method. 508 * 509 * @param index The current column index being processed. 510 * 511 * @param propType The bean property type that this column needs to be 512 * converted into. 513 * 514 * @throws SQLException if a database access error occurs 515 * 516 * @return The object from the {@code ResultSet} at the given column 517 * index after optional type processing or {@code null} if the column 518 * value was SQL NULL. 519 */ 520 protected Object processColumn(final ResultSet rs, final int index, final Class<?> propType) 521 throws SQLException { 522 523 Object retval = rs.getObject(index); 524 525 if ( !propType.isPrimitive() && retval == null ) { 526 return null; 527 } 528 529 for (final ColumnHandler handler : columnHandlers) { 530 if (handler.match(propType)) { 531 retval = handler.apply(rs, index); 532 break; 533 } 534 } 535 536 return retval; 537 538 } 539 540}