1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.commons.dbutils; 18 19 import java.beans.BeanInfo; 20 import java.beans.IntrospectionException; 21 import java.beans.Introspector; 22 import java.beans.PropertyDescriptor; 23 import java.lang.reflect.Field; 24 import java.lang.reflect.InvocationTargetException; 25 import java.lang.reflect.Method; 26 import java.sql.ResultSet; 27 import java.sql.ResultSetMetaData; 28 import java.sql.SQLException; 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.ServiceLoader; 35 36 /** 37 * <p> 38 * {@code BeanProcessor} matches column names to bean property names 39 * and converts {@code ResultSet} columns into objects for those bean 40 * properties. Subclasses should override the methods in the processing chain 41 * to customize behavior. 42 * </p> 43 * 44 * <p> 45 * This class is thread-safe. 46 * </p> 47 * 48 * @see BasicRowProcessor 49 * 50 * @since 1.1 51 */ 52 public class BeanProcessor { 53 54 /** 55 * Special array value used by {@code mapColumnsToProperties} that 56 * indicates there is no bean property that matches a column from a 57 * {@code ResultSet}. 58 */ 59 protected static final int PROPERTY_NOT_FOUND = -1; 60 61 /** 62 * Set a bean's primitive properties to these defaults when SQL NULL 63 * is returned. These are the same as the defaults that ResultSet get* 64 * methods return in the event of a NULL column. 65 */ 66 private static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = new HashMap<>(); 67 68 private static final List<ColumnHandler<?>> COLUMN_HANDLERS = new ArrayList<>(); 69 70 private static final List<PropertyHandler> PROPERTY_HANDLERS = new ArrayList<>(); 71 72 static { 73 PRIMITIVE_DEFAULTS.put(Integer.TYPE, Integer.valueOf(0)); 74 PRIMITIVE_DEFAULTS.put(Short.TYPE, Short.valueOf((short) 0)); 75 PRIMITIVE_DEFAULTS.put(Byte.TYPE, Byte.valueOf((byte) 0)); 76 PRIMITIVE_DEFAULTS.put(Float.TYPE, Float.valueOf(0f)); 77 PRIMITIVE_DEFAULTS.put(Double.TYPE, Double.valueOf(0d)); 78 PRIMITIVE_DEFAULTS.put(Long.TYPE, Long.valueOf(0L)); 79 PRIMITIVE_DEFAULTS.put(Boolean.TYPE, Boolean.FALSE); 80 PRIMITIVE_DEFAULTS.put(Character.TYPE, Character.valueOf((char) 0)); 81 82 // Use a ServiceLoader to find implementations 83 ServiceLoader.load(ColumnHandler.class).forEach(COLUMN_HANDLERS::add); 84 85 // Use a ServiceLoader to find implementations 86 ServiceLoader.load(PropertyHandler.class).forEach(PROPERTY_HANDLERS::add); 87 } 88 89 /** 90 * ResultSet column to bean property name overrides. 91 */ 92 private final Map<String, String> columnToPropertyOverrides; 93 94 /** 95 * Constructor for BeanProcessor. 96 */ 97 public BeanProcessor() { 98 this(new HashMap<>()); 99 } 100 101 /** 102 * Constructor for BeanProcessor configured with column to property name overrides. 103 * 104 * @param columnToPropertyOverrides ResultSet column to bean property name overrides 105 * @since 1.5 106 */ 107 public BeanProcessor(final Map<String, String> columnToPropertyOverrides) { 108 if (columnToPropertyOverrides == null) { 109 throw new IllegalArgumentException("columnToPropertyOverrides map cannot be null"); 110 } 111 this.columnToPropertyOverrides = columnToPropertyOverrides; 112 } 113 114 /** 115 * Calls the setter method on the target object for the given property. 116 * If no setter method exists for the property, this method does nothing. 117 * @param target The object to set the property on. 118 * @param prop The property to set. 119 * @param value The value to pass into the setter. 120 * @throws SQLException if an error occurs setting the property. 121 */ 122 private void callSetter(final Object target, final PropertyDescriptor prop, Object value) 123 throws SQLException { 124 125 final Method setter = getWriteMethod(target, prop, value); 126 127 if (setter == null || setter.getParameterTypes().length != 1) { 128 return; 129 } 130 131 try { 132 final Class<?> firstParam = setter.getParameterTypes()[0]; 133 for (final PropertyHandler handler : PROPERTY_HANDLERS) { 134 if (handler.match(firstParam, value)) { 135 value = handler.apply(firstParam, value); 136 break; 137 } 138 } 139 140 // Don't call setter if the value object isn't the right type 141 if (!this.isCompatibleType(value, firstParam)) { 142 throw new SQLException( 143 "Cannot set " + prop.getName() + ": incompatible types, cannot convert " + value.getClass().getName() + " to " + firstParam.getName()); 144 // value cannot be null here because isCompatibleType allows null 145 } 146 setter.invoke(target, value); 147 148 } catch (final IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { 149 throw new SQLException("Cannot set " + prop.getName() + ": " + e.getMessage()); 150 } 151 } 152 153 /** 154 * Creates a new object and initializes its fields from the ResultSet. 155 * @param <T> The type of bean to create 156 * @param resultSet The result set. 157 * @param type The bean type (the return type of the object). 158 * @param props The property descriptors. 159 * @param columnToProperty The column indices in the result set. 160 * @return An initialized object. 161 * @throws SQLException if a database error occurs. 162 */ 163 private <T> T createBean(final ResultSet resultSet, final Class<T> type, final PropertyDescriptor[] props, final int[] columnToProperty) 164 throws SQLException { 165 return populateBean(resultSet, this.newInstance(type), props, columnToProperty); 166 } 167 168 /** 169 * Get the write method to use when setting {@code value} to the {@code target}. 170 * 171 * @param target Object where the write method will be called. 172 * @param prop BeanUtils information. 173 * @param value The value that will be passed to the write method. 174 * @return The {@link java.lang.reflect.Method} to call on {@code target} to write {@code value} or {@code null} if 175 * there is no suitable write method. 176 */ 177 protected Method getWriteMethod(final Object target, final PropertyDescriptor prop, final Object value) { 178 return prop.getWriteMethod(); 179 } 180 181 /** 182 * ResultSet.getObject() returns an Integer object for an INT column. The 183 * setter method for the property might take an Integer or a primitive int. 184 * This method returns true if the value can be successfully passed into 185 * the setter method. Remember, Method.invoke() handles the unwrapping 186 * of Integer into an int. 187 * 188 * @param value The value to be passed into the setter method. 189 * @param type The setter's parameter type (non-null) 190 * @return boolean True if the value is compatible (null => true) 191 */ 192 private boolean isCompatibleType(final Object value, final Class<?> type) { 193 // Do object check first, then primitives 194 return value == null || type.isInstance(value) || matchesPrimitive(type, value.getClass()); 195 } 196 197 /** 198 * The positions in the returned array represent column numbers. The 199 * values stored at each position represent the index in the 200 * {@code PropertyDescriptor[]} for the bean property that matches 201 * the column name. If no bean property was found for a column, the 202 * position is set to {@code PROPERTY_NOT_FOUND}. 203 * 204 * @param rsmd The {@code ResultSetMetaData} containing column 205 * information. 206 * 207 * @param props The bean property descriptors. 208 * 209 * @throws SQLException if a database access error occurs 210 * 211 * @return An int[] with column index to property index mappings. The 0th 212 * element is meaningless because JDBC column indexing starts at 1. 213 */ 214 protected int[] mapColumnsToProperties(final ResultSetMetaData rsmd, 215 final PropertyDescriptor[] props) throws SQLException { 216 217 final int cols = rsmd.getColumnCount(); 218 final int[] columnToProperty = new int[cols + 1]; 219 Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND); 220 221 for (int col = 1; col <= cols; col++) { 222 String columnName = rsmd.getColumnLabel(col); 223 if (null == columnName || 0 == columnName.length()) { 224 columnName = rsmd.getColumnName(col); 225 } 226 String propertyName = columnToPropertyOverrides.get(columnName); 227 if (propertyName == null) { 228 propertyName = columnName; 229 } 230 if (propertyName == null) { 231 propertyName = Integer.toString(col); 232 } 233 234 for (int i = 0; i < props.length; i++) { 235 final PropertyDescriptor prop = props[i]; 236 final Method reader = prop.getReadMethod(); 237 238 // Check for @Column annotations as explicit marks 239 final Column column; 240 if (reader != null) { 241 column = reader.getAnnotation(Column.class); 242 } else { 243 column = null; 244 } 245 246 final String propertyColumnName; 247 if (column != null) { 248 propertyColumnName = column.name(); 249 } else { 250 propertyColumnName = prop.getName(); 251 } 252 if (propertyName.equalsIgnoreCase(propertyColumnName)) { 253 columnToProperty[col] = i; 254 break; 255 } 256 } 257 } 258 259 return columnToProperty; 260 } 261 262 /** 263 * Check whether a value is of the same primitive type as {@code targetType}. 264 * 265 * @param targetType The primitive type to target. 266 * @param valueType The value to match to the primitive type. 267 * @return Whether {@code valueType} can be coerced (e.g. autoboxed) into {@code targetType}. 268 */ 269 private boolean matchesPrimitive(final Class<?> targetType, final Class<?> valueType) { 270 if (!targetType.isPrimitive()) { 271 return false; 272 } 273 274 try { 275 // see if there is a "TYPE" field. This is present for primitive wrappers. 276 final Field typeField = valueType.getField("TYPE"); 277 final Object primitiveValueType = typeField.get(valueType); 278 279 if (targetType == primitiveValueType) { 280 return true; 281 } 282 } catch (final NoSuchFieldException | IllegalAccessException ignored) { 283 // an inaccessible TYPE field is a good sign that we're not working with a primitive wrapper. 284 // nothing to do. we can't match for compatibility 285 } 286 return false; 287 } 288 289 /** 290 * Factory method that returns a new instance of the given Class. This 291 * is called at the start of the bean creation process and may be 292 * overridden to provide custom behavior like returning a cached bean 293 * instance. 294 * @param <T> The type of object to create 295 * @param c The Class to create an object from. 296 * @return A newly created object of the Class. 297 * @throws SQLException if creation failed. 298 */ 299 protected <T> T newInstance(final Class<T> c) throws SQLException { 300 try { 301 return c.getDeclaredConstructor().newInstance(); 302 303 } catch (final IllegalAccessException | InstantiationException | InvocationTargetException | 304 NoSuchMethodException e) { 305 throw new SQLException("Cannot create " + c.getName() + ": " + e.getMessage()); 306 } 307 } 308 309 /** 310 * Initializes the fields of the provided bean from the ResultSet. 311 * @param <T> The type of bean 312 * @param resultSet The result set. 313 * @param bean The bean to be populated. 314 * @return An initialized object. 315 * @throws SQLException if a database error occurs. 316 */ 317 public <T> T populateBean(final ResultSet resultSet, final T bean) throws SQLException { 318 final PropertyDescriptor[] props = this.propertyDescriptors(bean.getClass()); 319 final ResultSetMetaData rsmd = resultSet.getMetaData(); 320 final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); 321 322 return populateBean(resultSet, bean, props, columnToProperty); 323 } 324 325 /** 326 * This method populates a bean from the ResultSet based upon the underlying meta-data. 327 * 328 * @param <T> The type of bean 329 * @param resultSet The result set. 330 * @param bean The bean to be populated. 331 * @param props The property descriptors. 332 * @param columnToProperty The column indices in the result set. 333 * @return An initialized object. 334 * @throws SQLException if a database error occurs. 335 */ 336 private <T> T populateBean(final ResultSet resultSet, final T bean, 337 final PropertyDescriptor[] props, final int[] columnToProperty) 338 throws SQLException { 339 340 for (int i = 1; i < columnToProperty.length; i++) { 341 342 if (columnToProperty[i] == PROPERTY_NOT_FOUND) { 343 continue; 344 } 345 346 final PropertyDescriptor prop = props[columnToProperty[i]]; 347 final Class<?> propType = prop.getPropertyType(); 348 349 Object value = null; 350 if (propType != null) { 351 value = this.processColumn(resultSet, i, propType); 352 353 if (value == null && propType.isPrimitive()) { 354 value = PRIMITIVE_DEFAULTS.get(propType); 355 } 356 } 357 358 this.callSetter(bean, prop, value); 359 } 360 361 return bean; 362 } 363 364 /** 365 * Convert a {@code ResultSet} column into an object. Simple 366 * implementations could just call {@code rs.getObject(index)} while 367 * more complex implementations could perform type manipulation to match 368 * the column's type to the bean property type. 369 * 370 * <p> 371 * This implementation calls the appropriate {@code ResultSet} getter 372 * method for the given property type to perform the type conversion. If 373 * the property type doesn't match one of the supported 374 * {@code ResultSet} types, {@code getObject} is called. 375 * </p> 376 * 377 * @param resultSet The {@code ResultSet} currently being processed. It is 378 * positioned on a valid row before being passed into this method. 379 * 380 * @param index The current column index being processed. 381 * 382 * @param propType The bean property type that this column needs to be 383 * converted into. 384 * 385 * @throws SQLException if a database access error occurs 386 * 387 * @return The object from the {@code ResultSet} at the given column 388 * index after optional type processing or {@code null} if the column 389 * value was SQL NULL. 390 */ 391 protected Object processColumn(final ResultSet resultSet, final int index, final Class<?> propType) 392 throws SQLException { 393 394 Object retval = resultSet.getObject(index); 395 396 if ( !propType.isPrimitive() && retval == null ) { 397 return null; 398 } 399 400 for (final ColumnHandler<?> handler : COLUMN_HANDLERS) { 401 if (handler.match(propType)) { 402 retval = handler.apply(resultSet, index); 403 break; 404 } 405 } 406 407 return retval; 408 409 } 410 411 /** 412 * Returns a PropertyDescriptor[] for the given Class. 413 * 414 * @param c The Class to retrieve PropertyDescriptors for. 415 * @return A PropertyDescriptor[] describing the Class. 416 * @throws SQLException if introspection failed. 417 */ 418 private PropertyDescriptor[] propertyDescriptors(final Class<?> c) 419 throws SQLException { 420 // Introspector caches BeanInfo classes for better performance 421 BeanInfo beanInfo = null; 422 try { 423 beanInfo = Introspector.getBeanInfo(c); 424 425 } catch (final IntrospectionException e) { 426 throw new SQLException( 427 "Bean introspection failed: " + e.getMessage()); 428 } 429 430 return beanInfo.getPropertyDescriptors(); 431 } 432 433 /** 434 * Convert a {@code ResultSet} row into a JavaBean. This 435 * implementation uses reflection and {@code BeanInfo} classes to 436 * match column names to bean property names. Properties are matched to 437 * columns based on several factors: 438 * <br/> 439 * <ol> 440 * <li> 441 * The class has a writable property with the same name as a column. 442 * The name comparison is case insensitive. 443 * </li> 444 * 445 * <li> 446 * The column type can be converted to the property's set method 447 * parameter type with a ResultSet.get* method. If the conversion fails 448 * (ie. the property was an int and the column was a Timestamp) an 449 * SQLException is thrown. 450 * </li> 451 * </ol> 452 * 453 * <p> 454 * Primitive bean properties are set to their defaults when SQL NULL is 455 * returned from the {@code ResultSet}. Numeric fields are set to 0 456 * and booleans are set to false. Object bean properties are set to 457 * {@code null} when SQL NULL is returned. This is the same behavior 458 * as the {@code ResultSet} get* methods. 459 * </p> 460 * @param <T> The type of bean to create 461 * @param rs ResultSet that supplies the bean data 462 * @param type Class from which to create the bean instance 463 * @throws SQLException if a database access error occurs 464 * @return the newly created bean 465 */ 466 public <T> T toBean(final ResultSet rs, final Class<? extends T> type) throws SQLException { 467 final T bean = this.newInstance(type); 468 return this.populateBean(rs, bean); 469 } 470 471 /** 472 * Convert a {@code ResultSet} into a {@code List} of JavaBeans. 473 * This implementation uses reflection and {@code BeanInfo} classes to 474 * match column names to bean property names. Properties are matched to 475 * columns based on several factors: 476 * <br/> 477 * <ol> 478 * <li> 479 * The class has a writable property with the same name as a column. 480 * The name comparison is case insensitive. 481 * </li> 482 * 483 * <li> 484 * The column type can be converted to the property's set method 485 * parameter type with a ResultSet.get* method. If the conversion fails 486 * (ie. the property was an int and the column was a Timestamp) an 487 * SQLException is thrown. 488 * </li> 489 * </ol> 490 * 491 * <p> 492 * Primitive bean properties are set to their defaults when SQL NULL is 493 * returned from the {@code ResultSet}. Numeric fields are set to 0 494 * and booleans are set to false. Object bean properties are set to 495 * {@code null} when SQL NULL is returned. This is the same behavior 496 * as the {@code ResultSet} get* methods. 497 * </p> 498 * @param <T> The type of bean to create 499 * @param resultSet ResultSet that supplies the bean data 500 * @param type Class from which to create the bean instance 501 * @throws SQLException if a database access error occurs 502 * @return the newly created List of beans 503 */ 504 public <T> List<T> toBeanList(final ResultSet resultSet, final Class<? extends T> type) throws SQLException { 505 final List<T> results = new ArrayList<>(); 506 507 if (!resultSet.next()) { 508 return results; 509 } 510 511 final PropertyDescriptor[] props = this.propertyDescriptors(type); 512 final ResultSetMetaData rsmd = resultSet.getMetaData(); 513 final int[] columnToProperty = this.mapColumnsToProperties(rsmd, props); 514 515 do { 516 results.add(this.createBean(resultSet, type, props, columnToProperty)); 517 } while (resultSet.next()); 518 519 return results; 520 } 521 522 }