1818
1919import java .beans .PropertyDescriptor ;
2020import java .util .HashMap ;
21- import java .util .HashSet ;
2221import java .util .List ;
2322import java .util .Locale ;
2423import java .util .Map ;
25- import java .util .Set ;
2624import java .util .function .Function ;
2725
2826import io .r2dbc .spi .OutParameters ;
3129import io .r2dbc .spi .ReadableMetadata ;
3230import io .r2dbc .spi .Row ;
3331import io .r2dbc .spi .RowMetadata ;
34- import org .apache .commons .logging .Log ;
35- import org .apache .commons .logging .LogFactory ;
3632
3733import org .springframework .beans .BeanUtils ;
38- import org .springframework .beans .BeanWrapper ;
3934import org .springframework .beans .BeanWrapperImpl ;
4035import org .springframework .beans .TypeConverter ;
41- import org .springframework .beans .TypeMismatchException ;
4236import org .springframework .core .convert .ConversionService ;
4337import org .springframework .core .convert .support .DefaultConversionService ;
44- import org .springframework .dao .InvalidDataAccessApiUsageException ;
4538import org .springframework .lang .Nullable ;
4639import org .springframework .util .Assert ;
47- import org .springframework .util .ClassUtils ;
4840import org .springframework .util .StringUtils ;
4941
5042/**
6860 * {@code "select fname as first_name from customer"}, where {@code first_name}
6961 * can be mapped to a {@code setFirstName(String)} method in the target class.
7062 *
71- * <p>For a {@code NULL} value read from the database, an attempt will be made to
72- * call the corresponding setter method with {@code null}, but in the case of
73- * Java primitives this will result in a {@link TypeMismatchException} by default.
74- * To ignore {@code NULL} database values for all primitive properties in the
75- * target class, set the {@code primitivesDefaultedForNullValue} flag to
76- * {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for
77- * details.
78- *
7963 * <p>If you need to map to a target class which has a <em>data class</em> constructor
8064 * — for example, a Java {@code record} or a Kotlin {@code data} class —
8165 * use {@link DataClassRowMapper} instead.
8569 * implementation.
8670 *
8771 * @author Simon Baslé
88- * @author Thomas Risberg
8972 * @author Juergen Hoeller
9073 * @author Sam Brannen
9174 * @since 6.1
9275 * @param <T> the result type
9376 * @see DataClassRowMapper
9477 */
95- // Note: this class is adapted from the BeanPropertyRowMapper in spring-jdbc
9678public class BeanPropertyRowMapper <T > implements Function <Readable , T > {
9779
98- /** Logger available to subclasses. */
99- protected final Log logger = LogFactory .getLog (getClass ());
100-
10180 /** The class we are mapping to. */
102- @ Nullable
103- private Class <T > mappedClass ;
81+ private final Class <T > mappedClass ;
10482
105- /** Whether we're strictly validating. */
106- private boolean checkFullyPopulated = false ;
107-
108- /**
109- * Whether {@code NULL} database values should be ignored for primitive
110- * properties in the target class.
111- * @see #setPrimitivesDefaultedForNullValue(boolean)
112- */
113- private boolean primitivesDefaultedForNullValue = false ;
114-
115- /** ConversionService for binding R2DBC values to bean properties. */
116- @ Nullable
117- private ConversionService conversionService = DefaultConversionService .getSharedInstance ();
83+ /** ConversionService for binding result values to bean properties. */
84+ private final ConversionService conversionService ;
11885
11986 /** Map of the properties we provide mapping for. */
120- @ Nullable
121- private Map <String , PropertyDescriptor > mappedProperties ;
87+ private final Map <String , PropertyDescriptor > mappedProperties ;
12288
123- /** Set of bean property names we provide mapping for. */
124- @ Nullable
125- private Set <String > mappedPropertyNames ;
12689
12790 /**
128- * Create a new {@code BeanPropertyRowMapper}, accepting unpopulated
129- * properties in the target bean.
130- * @param mappedClass the class that each row/outParameters should be mapped to
91+ * Create a new {@code BeanPropertyRowMapper}.
92+ * @param mappedClass the class that each row should be mapped to
13193 */
13294 public BeanPropertyRowMapper (Class <T > mappedClass ) {
133- initialize (mappedClass );
95+ this (mappedClass , DefaultConversionService . getSharedInstance () );
13496 }
13597
13698 /**
13799 * Create a new {@code BeanPropertyRowMapper}.
138100 * @param mappedClass the class that each row should be mapped to
139- * @param checkFullyPopulated whether we're strictly validating that
140- * all bean properties have been mapped from corresponding database columns or
141- * out-parameters
142- */
143- public BeanPropertyRowMapper (Class <T > mappedClass , boolean checkFullyPopulated ) {
144- initialize (mappedClass );
145- this .checkFullyPopulated = checkFullyPopulated ;
146- }
147-
148-
149- /**
150- * Get the class that we are mapping to.
151- */
152- @ Nullable
153- public final Class <T > getMappedClass () {
154- return this .mappedClass ;
155- }
156-
157- /**
158- * Set whether we're strictly validating that all bean properties have been mapped
159- * from corresponding database columns or out-parameters.
160- * <p>Default is {@code false}, accepting unpopulated properties in the target bean.
161- */
162- public void setCheckFullyPopulated (boolean checkFullyPopulated ) {
163- this .checkFullyPopulated = checkFullyPopulated ;
164- }
165-
166- /**
167- * Return whether we're strictly validating that all bean properties have been
168- * mapped from corresponding database columns or out-parameters.
101+ * @param conversionService a {@link ConversionService} for binding
102+ * result values to bean properties
169103 */
170- public boolean isCheckFullyPopulated () {
171- return this .checkFullyPopulated ;
172- }
173-
174- /**
175- * Set whether a {@code NULL} database column or out-parameter value should
176- * be ignored when mapping to a corresponding primitive property in the target class.
177- * <p>Default is {@code false}, throwing an exception when nulls are mapped
178- * to Java primitives.
179- * <p>If this flag is set to {@code true} and you use an <em>ignored</em>
180- * primitive property value from the mapped bean to update the database, the
181- * value in the database will be changed from {@code NULL} to the current value
182- * of that primitive property. That value may be the property's initial value
183- * (potentially Java's default value for the respective primitive type), or
184- * it may be some other value set for the property in the default constructor
185- * (or initialization block) or as a side effect of setting some other property
186- * in the mapped bean.
187- */
188- public void setPrimitivesDefaultedForNullValue (boolean primitivesDefaultedForNullValue ) {
189- this .primitivesDefaultedForNullValue = primitivesDefaultedForNullValue ;
190- }
191-
192- /**
193- * Get the value of the {@code primitivesDefaultedForNullValue} flag.
194- * @see #setPrimitivesDefaultedForNullValue(boolean)
195- */
196- public boolean isPrimitivesDefaultedForNullValue () {
197- return this .primitivesDefaultedForNullValue ;
198- }
199-
200- /**
201- * Set a {@link ConversionService} for binding R2DBC values to bean properties,
202- * or {@code null} for none.
203- * <p>Default is a {@link DefaultConversionService}. This provides support for
204- * {@code java.time} conversion and other special types.
205- * @see #initBeanWrapper(BeanWrapper)
206- */
207- public void setConversionService (@ Nullable ConversionService conversionService ) {
208- this .conversionService = conversionService ;
209- }
210-
211- /**
212- * Return a {@link ConversionService} for binding R2DBC values to bean properties,
213- * or {@code null} if none.
214- */
215- @ Nullable
216- public ConversionService getConversionService () {
217- return this .conversionService ;
218- }
219-
220-
221- /**
222- * Initialize the mapping meta-data for the given class.
223- * @param mappedClass the mapped class
224- */
225- protected void initialize (Class <T > mappedClass ) {
104+ public BeanPropertyRowMapper (Class <T > mappedClass , ConversionService conversionService ) {
105+ Assert .notNull (mappedClass , "Mapped Class must not be null" );
106+ Assert .notNull (conversionService , "ConversionService must not be null" );
226107 this .mappedClass = mappedClass ;
108+ this .conversionService = conversionService ;
227109 this .mappedProperties = new HashMap <>();
228- this .mappedPropertyNames = new HashSet <>();
229110
230111 for (PropertyDescriptor pd : BeanUtils .getPropertyDescriptors (mappedClass )) {
231112 if (pd .getWriteMethod () != null ) {
@@ -235,20 +116,18 @@ protected void initialize(Class<T> mappedClass) {
235116 if (!lowerCaseName .equals (underscoreName )) {
236117 this .mappedProperties .put (underscoreName , pd );
237118 }
238- this .mappedPropertyNames .add (pd .getName ());
239119 }
240120 }
241121 }
242122
123+
243124 /**
244125 * Remove the specified property from the mapped properties.
245126 * @param propertyName the property name (as used by property descriptors)
246127 */
247128 protected void suppressProperty (String propertyName ) {
248- if (this .mappedProperties != null ) {
249- this .mappedProperties .remove (lowerCaseName (propertyName ));
250- this .mappedProperties .remove (underscoreName (propertyName ));
251- }
129+ this .mappedProperties .remove (lowerCaseName (propertyName ));
130+ this .mappedProperties .remove (underscoreName (propertyName ));
252131 }
253132
254133 /**
@@ -309,52 +188,22 @@ public T apply(Readable readable) {
309188
310189 private <R extends Readable > T mapForReadable (R readable , List <? extends ReadableMetadata > readableMetadatas ) {
311190 BeanWrapperImpl bw = new BeanWrapperImpl ();
312- initBeanWrapper (bw );
313-
191+ bw .setConversionService (this .conversionService );
314192 T mappedObject = constructMappedInstance (readable , readableMetadatas , bw );
315193 bw .setBeanInstance (mappedObject );
316194
317- Set <String > populatedProperties = (isCheckFullyPopulated () ? new HashSet <>() : null );
318195 int readableItemCount = readableMetadatas .size ();
319- for (int itemIndex = 0 ; itemIndex < readableItemCount ; itemIndex ++) {
196+ for (int itemIndex = 0 ; itemIndex < readableItemCount ; itemIndex ++) {
320197 ReadableMetadata itemMetadata = readableMetadatas .get (itemIndex );
321198 String itemName = itemMetadata .getName ();
322199 String property = lowerCaseName (StringUtils .delete (itemName , " " ));
323- PropertyDescriptor pd = ( this .mappedProperties != null ? this . mappedProperties . get (property ) : null );
200+ PropertyDescriptor pd = this .mappedProperties . get (property );
324201 if (pd != null ) {
325- Object value = getItemValue (readable , itemIndex , pd );
326- // Implementation note: the JDBC mapper can log the column mapping details each time row 0 is encountered
327- // but unfortunately this is not possible in R2DBC as row number is not provided. The BiFunction#apply
328- // cannot be stateful as it could be applied to a different row set, e.g. when resubscribing.
329- try {
330- bw .setPropertyValue (pd .getName (), value );
331- }
332- catch (TypeMismatchException ex ) {
333- if (value == null && isPrimitivesDefaultedForNullValue ()) {
334- if (logger .isDebugEnabled ()) {
335- String propertyType = ClassUtils .getQualifiedName (pd .getPropertyType ());
336- //here too, we miss the rowNumber information
337- logger .debug ("""
338- Ignoring intercepted TypeMismatchException for item '%s' \
339- with null value when setting property '%s' of type '%s' on object: %s"
340- """ .formatted (itemName , pd .getName (), propertyType , mappedObject ), ex );
341- }
342- }
343- else {
344- throw ex ;
345- }
346- }
347- if (populatedProperties != null ) {
348- populatedProperties .add (pd .getName ());
349- }
202+ Object value = getItemValue (readable , itemIndex , pd .getPropertyType ());
203+ bw .setPropertyValue (pd .getName (), value );
350204 }
351205 }
352206
353- if (populatedProperties != null && !populatedProperties .equals (this .mappedPropertyNames )) {
354- throw new InvalidDataAccessApiUsageException ("Given readable does not contain all items " +
355- "necessary to populate object of " + this .mappedClass + ": " + this .mappedPropertyNames );
356- }
357-
358207 return mappedObject ;
359208 }
360209
@@ -369,43 +218,9 @@ private <R extends Readable> T mapForReadable(R readable, List<? extends Readabl
369218 * @return a corresponding instance of the mapped class
370219 */
371220 protected T constructMappedInstance (Readable readable , List <? extends ReadableMetadata > itemMetadatas , TypeConverter tc ) {
372- Assert .state (this .mappedClass != null , "Mapped class was not specified" );
373221 return BeanUtils .instantiateClass (this .mappedClass );
374222 }
375223
376- /**
377- * Initialize the given BeanWrapper to be used for row mapping or outParameters
378- * mapping.
379- * <p>To be called for each Readable.
380- * <p>The default implementation applies the configured {@link ConversionService},
381- * if any. Can be overridden in subclasses.
382- * @param bw the BeanWrapper to initialize
383- * @see #getConversionService()
384- * @see BeanWrapper#setConversionService
385- */
386- protected void initBeanWrapper (BeanWrapper bw ) {
387- ConversionService cs = getConversionService ();
388- if (cs != null ) {
389- bw .setConversionService (cs );
390- }
391- }
392-
393- /**
394- * Retrieve an R2DBC object value for the specified item index (a column or an
395- * out-parameter).
396- * <p>The default implementation delegates to
397- * {@link #getItemValue(Readable, int, Class)}.
398- * @param readable is the {@code Row} or {@code OutParameters} holding the data
399- * @param itemIndex is the column index or out-parameter index
400- * @param pd the bean property that each result object is expected to match
401- * @return the Object value
402- * @see #getItemValue(Readable, int, Class)
403- */
404- @ Nullable
405- protected Object getItemValue (Readable readable , int itemIndex , PropertyDescriptor pd ) {
406- return getItemValue (readable , itemIndex , pd .getPropertyType ());
407- }
408-
409224 /**
410225 * Retrieve an R2DBC object value for the specified item index (a column or
411226 * an out-parameter).
@@ -430,30 +245,4 @@ protected Object getItemValue(Readable readable, int itemIndex, Class<?> paramTy
430245 }
431246 }
432247
433-
434- /**
435- * Static factory method to create a new {@code BeanPropertyRowMapper}.
436- * @param mappedClass the class that each row should be mapped to
437- * @see #newInstance(Class, ConversionService)
438- */
439- public static <T > BeanPropertyRowMapper <T > newInstance (Class <T > mappedClass ) {
440- return new BeanPropertyRowMapper <>(mappedClass );
441- }
442-
443- /**
444- * Static factory method to create a new {@code BeanPropertyRowMapper}.
445- * @param mappedClass the class that each row should be mapped to
446- * @param conversionService the {@link ConversionService} for binding
447- * R2DBC values to bean properties, or {@code null} for none
448- * @see #newInstance(Class)
449- * @see #setConversionService
450- */
451- public static <T > BeanPropertyRowMapper <T > newInstance (
452- Class <T > mappedClass , @ Nullable ConversionService conversionService ) {
453-
454- BeanPropertyRowMapper <T > rowMapper = newInstance (mappedClass );
455- rowMapper .setConversionService (conversionService );
456- return rowMapper ;
457- }
458-
459248}
0 commit comments