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}