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}