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