001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.jdbc.core; 018 019import java.beans.PropertyDescriptor; 020import java.sql.ResultSet; 021import java.sql.ResultSetMetaData; 022import java.sql.SQLException; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Locale; 026import java.util.Map; 027import java.util.Set; 028 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031 032import org.springframework.beans.BeanUtils; 033import org.springframework.beans.BeanWrapper; 034import org.springframework.beans.NotWritablePropertyException; 035import org.springframework.beans.PropertyAccessorFactory; 036import org.springframework.beans.TypeMismatchException; 037import org.springframework.core.convert.ConversionService; 038import org.springframework.core.convert.support.DefaultConversionService; 039import org.springframework.dao.DataRetrievalFailureException; 040import org.springframework.dao.InvalidDataAccessApiUsageException; 041import org.springframework.jdbc.support.JdbcUtils; 042import org.springframework.lang.Nullable; 043import org.springframework.util.Assert; 044import org.springframework.util.ClassUtils; 045import org.springframework.util.StringUtils; 046 047/** 048 * {@link RowMapper} implementation that converts a row into a new instance 049 * of the specified mapped target class. The mapped target class must be a 050 * top-level class and it must have a default or no-arg constructor. 051 * 052 * <p>Column values are mapped based on matching the column name as obtained from result set 053 * meta-data to public setters for the corresponding properties. The names are matched either 054 * directly or by transforming a name separating the parts with underscores to the same name 055 * using "camel" case. 056 * 057 * <p>Mapping is provided for fields in the target class for many common types, e.g.: 058 * String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long, 059 * float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc. 060 * 061 * <p>To facilitate mapping between columns and fields that don't have matching names, 062 * try using column aliases in the SQL statement like "select fname as first_name from customer". 063 * 064 * <p>For 'null' values read from the database, we will attempt to call the setter, but in the case of 065 * Java primitives, this causes a TypeMismatchException. This class can be configured (using the 066 * primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value. 067 * Be aware that if you use the values from the generated bean to update the database the primitive value 068 * will have been set to the primitive's default value instead of null. 069 * 070 * <p>Please note that this class is designed to provide convenience rather than high performance. 071 * For best performance, consider using a custom {@link RowMapper} implementation. 072 * 073 * @author Thomas Risberg 074 * @author Juergen Hoeller 075 * @since 2.5 076 * @param <T> the result type 077 */ 078public class BeanPropertyRowMapper<T> implements RowMapper<T> { 079 080 /** Logger available to subclasses. */ 081 protected final Log logger = LogFactory.getLog(getClass()); 082 083 /** The class we are mapping to. */ 084 @Nullable 085 private Class<T> mappedClass; 086 087 /** Whether we're strictly validating. */ 088 private boolean checkFullyPopulated = false; 089 090 /** Whether we're defaulting primitives when mapping a null value. */ 091 private boolean primitivesDefaultedForNullValue = false; 092 093 /** ConversionService for binding JDBC values to bean properties. */ 094 @Nullable 095 private ConversionService conversionService = DefaultConversionService.getSharedInstance(); 096 097 /** Map of the fields we provide mapping for. */ 098 @Nullable 099 private Map<String, PropertyDescriptor> mappedFields; 100 101 /** Set of bean properties we provide mapping for. */ 102 @Nullable 103 private Set<String> mappedProperties; 104 105 106 /** 107 * Create a new {@code BeanPropertyRowMapper} for bean-style configuration. 108 * @see #setMappedClass 109 * @see #setCheckFullyPopulated 110 */ 111 public BeanPropertyRowMapper() { 112 } 113 114 /** 115 * Create a new {@code BeanPropertyRowMapper}, accepting unpopulated 116 * properties in the target bean. 117 * @param mappedClass the class that each row should be mapped to 118 */ 119 public BeanPropertyRowMapper(Class<T> mappedClass) { 120 initialize(mappedClass); 121 } 122 123 /** 124 * Create a new {@code BeanPropertyRowMapper}. 125 * @param mappedClass the class that each row should be mapped to 126 * @param checkFullyPopulated whether we're strictly validating that 127 * all bean properties have been mapped from corresponding database fields 128 */ 129 public BeanPropertyRowMapper(Class<T> mappedClass, boolean checkFullyPopulated) { 130 initialize(mappedClass); 131 this.checkFullyPopulated = checkFullyPopulated; 132 } 133 134 135 /** 136 * Set the class that each row should be mapped to. 137 */ 138 public void setMappedClass(Class<T> mappedClass) { 139 if (this.mappedClass == null) { 140 initialize(mappedClass); 141 } 142 else { 143 if (this.mappedClass != mappedClass) { 144 throw new InvalidDataAccessApiUsageException("The mapped class can not be reassigned to map to " + 145 mappedClass + " since it is already providing mapping for " + this.mappedClass); 146 } 147 } 148 } 149 150 /** 151 * Get the class that we are mapping to. 152 */ 153 @Nullable 154 public final Class<T> getMappedClass() { 155 return this.mappedClass; 156 } 157 158 /** 159 * Set whether we're strictly validating that all bean properties have been mapped 160 * from corresponding database fields. 161 * <p>Default is {@code false}, accepting unpopulated properties in the target bean. 162 */ 163 public void setCheckFullyPopulated(boolean checkFullyPopulated) { 164 this.checkFullyPopulated = checkFullyPopulated; 165 } 166 167 /** 168 * Return whether we're strictly validating that all bean properties have been 169 * mapped from corresponding database fields. 170 */ 171 public boolean isCheckFullyPopulated() { 172 return this.checkFullyPopulated; 173 } 174 175 /** 176 * Set whether we're defaulting Java primitives in the case of mapping a null value 177 * from corresponding database fields. 178 * <p>Default is {@code false}, throwing an exception when nulls are mapped to Java primitives. 179 */ 180 public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) { 181 this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue; 182 } 183 184 /** 185 * Return whether we're defaulting Java primitives in the case of mapping a null value 186 * from corresponding database fields. 187 */ 188 public boolean isPrimitivesDefaultedForNullValue() { 189 return this.primitivesDefaultedForNullValue; 190 } 191 192 /** 193 * Set a {@link ConversionService} for binding JDBC values to bean properties, 194 * or {@code null} for none. 195 * <p>Default is a {@link DefaultConversionService}, as of Spring 4.3. This 196 * provides support for {@code java.time} conversion and other special types. 197 * @since 4.3 198 * @see #initBeanWrapper(BeanWrapper) 199 */ 200 public void setConversionService(@Nullable ConversionService conversionService) { 201 this.conversionService = conversionService; 202 } 203 204 /** 205 * Return a {@link ConversionService} for binding JDBC values to bean properties, 206 * or {@code null} if none. 207 * @since 4.3 208 */ 209 @Nullable 210 public ConversionService getConversionService() { 211 return this.conversionService; 212 } 213 214 215 /** 216 * Initialize the mapping meta-data for the given class. 217 * @param mappedClass the mapped class 218 */ 219 protected void initialize(Class<T> mappedClass) { 220 this.mappedClass = mappedClass; 221 this.mappedFields = new HashMap<>(); 222 this.mappedProperties = new HashSet<>(); 223 224 for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) { 225 if (pd.getWriteMethod() != null) { 226 this.mappedFields.put(lowerCaseName(pd.getName()), pd); 227 String underscoredName = underscoreName(pd.getName()); 228 if (!lowerCaseName(pd.getName()).equals(underscoredName)) { 229 this.mappedFields.put(underscoredName, pd); 230 } 231 this.mappedProperties.add(pd.getName()); 232 } 233 } 234 } 235 236 /** 237 * Convert a name in camelCase to an underscored name in lower case. 238 * Any upper case letters are converted to lower case with a preceding underscore. 239 * @param name the original name 240 * @return the converted name 241 * @since 4.2 242 * @see #lowerCaseName 243 */ 244 protected String underscoreName(String name) { 245 if (!StringUtils.hasLength(name)) { 246 return ""; 247 } 248 249 StringBuilder result = new StringBuilder(); 250 result.append(lowerCaseName(name.substring(0, 1))); 251 for (int i = 1; i < name.length(); i++) { 252 String s = name.substring(i, i + 1); 253 String slc = lowerCaseName(s); 254 if (!s.equals(slc)) { 255 result.append("_").append(slc); 256 } 257 else { 258 result.append(s); 259 } 260 } 261 return result.toString(); 262 } 263 264 /** 265 * Convert the given name to lower case. 266 * By default, conversions will happen within the US locale. 267 * @param name the original name 268 * @return the converted name 269 * @since 4.2 270 */ 271 protected String lowerCaseName(String name) { 272 return name.toLowerCase(Locale.US); 273 } 274 275 276 /** 277 * Extract the values for all columns in the current row. 278 * <p>Utilizes public setters and result set meta-data. 279 * @see java.sql.ResultSetMetaData 280 */ 281 @Override 282 public T mapRow(ResultSet rs, int rowNumber) throws SQLException { 283 Assert.state(this.mappedClass != null, "Mapped class was not specified"); 284 T mappedObject = BeanUtils.instantiateClass(this.mappedClass); 285 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject); 286 initBeanWrapper(bw); 287 288 ResultSetMetaData rsmd = rs.getMetaData(); 289 int columnCount = rsmd.getColumnCount(); 290 Set<String> populatedProperties = (isCheckFullyPopulated() ? new HashSet<>() : null); 291 292 for (int index = 1; index <= columnCount; index++) { 293 String column = JdbcUtils.lookupColumnName(rsmd, index); 294 String field = lowerCaseName(StringUtils.delete(column, " ")); 295 PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null); 296 if (pd != null) { 297 try { 298 Object value = getColumnValue(rs, index, pd); 299 if (rowNumber == 0 && logger.isDebugEnabled()) { 300 logger.debug("Mapping column '" + column + "' to property '" + pd.getName() + 301 "' of type '" + ClassUtils.getQualifiedName(pd.getPropertyType()) + "'"); 302 } 303 try { 304 bw.setPropertyValue(pd.getName(), value); 305 } 306 catch (TypeMismatchException ex) { 307 if (value == null && this.primitivesDefaultedForNullValue) { 308 if (logger.isDebugEnabled()) { 309 logger.debug("Intercepted TypeMismatchException for row " + rowNumber + 310 " and column '" + column + "' with null value when setting property '" + 311 pd.getName() + "' of type '" + 312 ClassUtils.getQualifiedName(pd.getPropertyType()) + 313 "' on object: " + mappedObject, ex); 314 } 315 } 316 else { 317 throw ex; 318 } 319 } 320 if (populatedProperties != null) { 321 populatedProperties.add(pd.getName()); 322 } 323 } 324 catch (NotWritablePropertyException ex) { 325 throw new DataRetrievalFailureException( 326 "Unable to map column '" + column + "' to property '" + pd.getName() + "'", ex); 327 } 328 } 329 else { 330 // No PropertyDescriptor found 331 if (rowNumber == 0 && logger.isDebugEnabled()) { 332 logger.debug("No property found for column '" + column + "' mapped to field '" + field + "'"); 333 } 334 } 335 } 336 337 if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) { 338 throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " + 339 "necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties); 340 } 341 342 return mappedObject; 343 } 344 345 /** 346 * Initialize the given BeanWrapper to be used for row mapping. 347 * To be called for each row. 348 * <p>The default implementation applies the configured {@link ConversionService}, 349 * if any. Can be overridden in subclasses. 350 * @param bw the BeanWrapper to initialize 351 * @see #getConversionService() 352 * @see BeanWrapper#setConversionService 353 */ 354 protected void initBeanWrapper(BeanWrapper bw) { 355 ConversionService cs = getConversionService(); 356 if (cs != null) { 357 bw.setConversionService(cs); 358 } 359 } 360 361 /** 362 * Retrieve a JDBC object value for the specified column. 363 * <p>The default implementation calls 364 * {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}. 365 * Subclasses may override this to check specific value types upfront, 366 * or to post-process values return from {@code getResultSetValue}. 367 * @param rs is the ResultSet holding the data 368 * @param index is the column index 369 * @param pd the bean property that each result object is expected to match 370 * @return the Object value 371 * @throws SQLException in case of extraction failure 372 * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) 373 */ 374 @Nullable 375 protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { 376 return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType()); 377 } 378 379 380 /** 381 * Static factory method to create a new {@code BeanPropertyRowMapper}. 382 * @param mappedClass the class that each row should be mapped to 383 * @see #newInstance(Class, ConversionService) 384 */ 385 public static <T> BeanPropertyRowMapper<T> newInstance(Class<T> mappedClass) { 386 return new BeanPropertyRowMapper<>(mappedClass); 387 } 388 389 /** 390 * Static factory method to create a new {@code BeanPropertyRowMapper}. 391 * @param mappedClass the class that each row should be mapped to 392 * @param conversionService the {@link ConversionService} for binding 393 * JDBC values to bean properties, or {@code null} for none 394 * @since 5.2.3 395 * @see #newInstance(Class) 396 * @see #setConversionService 397 */ 398 public static <T> BeanPropertyRowMapper<T> newInstance( 399 Class<T> mappedClass, @Nullable ConversionService conversionService) { 400 401 BeanPropertyRowMapper<T> rowMapper = newInstance(mappedClass); 402 rowMapper.setConversionService(conversionService); 403 return rowMapper; 404 } 405 406}