Skip to content

Commit aea4f4e

Browse files
committed
#252 - Fix EnumSet conversion.
We now convert properly collections of enum values for read and write.
1 parent 507340e commit aea4f4e

File tree

4 files changed

+276
-13
lines changed

4 files changed

+276
-13
lines changed

src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java

Lines changed: 191 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919
import io.r2dbc.spi.Row;
2020
import io.r2dbc.spi.RowMetadata;
2121

22+
import java.util.ArrayList;
2223
import java.util.Collection;
2324
import java.util.Collections;
2425
import java.util.LinkedHashMap;
26+
import java.util.List;
2527
import java.util.Map;
2628
import java.util.Optional;
2729
import java.util.function.BiFunction;
2830

31+
import org.springframework.core.CollectionFactory;
2932
import org.springframework.core.convert.ConversionService;
3033
import org.springframework.dao.InvalidDataAccessApiUsageException;
3134
import org.springframework.data.convert.CustomConversions;
@@ -49,6 +52,7 @@
4952
import org.springframework.lang.Nullable;
5053
import org.springframework.util.Assert;
5154
import org.springframework.util.ClassUtils;
55+
import org.springframework.util.CollectionUtils;
5256

5357
/**
5458
* Converter for R2DBC.
@@ -164,13 +168,76 @@ private Object readFrom(Row row, @Nullable RowMetadata metadata, RelationalPersi
164168
}
165169

166170
Object value = row.get(identifier);
167-
return getPotentiallyConvertedSimpleRead(value, property.getTypeInformation().getType());
171+
return readValue(value, property.getTypeInformation());
168172

169173
} catch (Exception o_O) {
170174
throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
171175
}
172176
}
173177

178+
public Object readValue(@Nullable Object value, TypeInformation<?> type) {
179+
180+
if (null == value) {
181+
return null;
182+
}
183+
184+
if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) {
185+
return getConversionService().convert(value, type.getType());
186+
} else if (value instanceof Collection || value.getClass().isArray()) {
187+
return readCollectionOrArray(asCollection(value), type);
188+
} else {
189+
return getPotentiallyConvertedSimpleRead(value, type.getType());
190+
}
191+
}
192+
193+
/**
194+
* Reads the given value into a collection of the given {@link TypeInformation}.
195+
*
196+
* @param source must not be {@literal null}.
197+
* @param targetType must not be {@literal null}.
198+
* @return the converted {@link Collection} or array, will never be {@literal null}.
199+
*/
200+
@SuppressWarnings("unchecked")
201+
private Object readCollectionOrArray(Collection<?> source, TypeInformation<?> targetType) {
202+
203+
Assert.notNull(targetType, "Target type must not be null!");
204+
205+
Class<?> collectionType = targetType.isSubTypeOf(Collection.class) //
206+
? targetType.getType() //
207+
: List.class;
208+
209+
TypeInformation<?> componentType = targetType.getComponentType() != null //
210+
? targetType.getComponentType() //
211+
: ClassTypeInformation.OBJECT;
212+
Class<?> rawComponentType = componentType.getType();
213+
214+
Collection<Object> items = targetType.getType().isArray() //
215+
? new ArrayList<>(source.size()) //
216+
: CollectionFactory.createCollection(collectionType, rawComponentType, source.size());
217+
218+
if (source.isEmpty()) {
219+
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
220+
}
221+
222+
for (Object element : source) {
223+
224+
if (!Object.class.equals(rawComponentType) && element instanceof Collection) {
225+
if (!rawComponentType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawComponentType)) {
226+
throw new MappingException(String.format(
227+
"Cannot convert %1$s of type %2$s into an instance of %3$s! Implement a custom Converter<%2$s, %3$s> and register it with the CustomConversions",
228+
element, element.getClass(), rawComponentType));
229+
}
230+
}
231+
if (element instanceof List) {
232+
items.add(readCollectionOrArray((Collection<Object>) element, componentType));
233+
} else {
234+
items.add(getPotentiallyConvertedSimpleRead(element, rawComponentType));
235+
}
236+
}
237+
238+
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
239+
}
240+
174241
/**
175242
* Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies
176243
* {@link Enum} handling or returns the value as is.
@@ -283,22 +350,87 @@ private void writeProperties(OutboundRow sink, RelationalPersistentEntity<?> ent
283350
continue;
284351
}
285352

286-
if (!getConversions().isSimpleType(value.getClass())) {
287-
288-
RelationalPersistentEntity<?> nestedEntity = getMappingContext().getPersistentEntity(property.getActualType());
289-
if (nestedEntity != null) {
290-
throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
291-
}
353+
if (getConversions().isSimpleType(value.getClass())) {
354+
writeSimpleInternal(sink, value, property);
355+
} else {
356+
writePropertyInternal(sink, value, property);
292357
}
293-
294-
writeSimpleInternal(sink, value, property);
295358
}
296359
}
297360

298361
private void writeSimpleInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) {
299362
sink.put(property.getColumnName(), SettableValue.from(getPotentiallyConvertedSimpleWrite(value)));
300363
}
301364

365+
private void writePropertyInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) {
366+
367+
TypeInformation<?> valueType = ClassTypeInformation.from(value.getClass());
368+
369+
if (valueType.isCollectionLike()) {
370+
371+
if (valueType.getActualType() != null && valueType.getRequiredActualType().isCollectionLike()) {
372+
373+
// pass-thru nested collections
374+
writeSimpleInternal(sink, value, property);
375+
return;
376+
}
377+
378+
List<Object> collectionInternal = createCollection(asCollection(value), property);
379+
sink.put(property.getColumnName(), SettableValue.from(collectionInternal));
380+
return;
381+
}
382+
383+
throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
384+
}
385+
386+
/**
387+
* Writes the given {@link Collection} using the given {@link RelationalPersistentProperty} information.
388+
*
389+
* @param collection must not be {@literal null}.
390+
* @param property must not be {@literal null}.
391+
* @return
392+
*/
393+
protected List<Object> createCollection(Collection<?> collection, RelationalPersistentProperty property) {
394+
return writeCollectionInternal(collection, property.getTypeInformation(), new ArrayList<>());
395+
}
396+
397+
/**
398+
* Populates the given {@link Collection sink} with converted values from the given {@link Collection source}.
399+
*
400+
* @param source the collection to create a {@link Collection} for, must not be {@literal null}.
401+
* @param type the {@link TypeInformation} to consider or {@literal null} if unknown.
402+
* @param sink the {@link Collection} to write to.
403+
* @return
404+
*/
405+
@SuppressWarnings("unchecked")
406+
private List<Object> writeCollectionInternal(Collection<?> source, @Nullable TypeInformation<?> type,
407+
Collection<?> sink) {
408+
409+
TypeInformation<?> componentType = null;
410+
411+
List<Object> collection = sink instanceof List ? (List<Object>) sink : new ArrayList<>(sink);
412+
413+
if (type != null) {
414+
componentType = type.getComponentType();
415+
}
416+
417+
for (Object element : source) {
418+
419+
Class<?> elementType = element == null ? null : element.getClass();
420+
421+
if (elementType == null || getConversions().isSimpleType(elementType)) {
422+
collection.add(getPotentiallyConvertedSimpleWrite(element,
423+
componentType != null ? componentType.getType() : Object.class));
424+
} else if (element instanceof Collection || elementType.isArray()) {
425+
collection.add(writeCollectionInternal(asCollection(element), componentType, new ArrayList<>()));
426+
} else {
427+
throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
428+
}
429+
}
430+
431+
return collection;
432+
}
433+
302434
private void writeNullInternal(OutboundRow sink, RelationalPersistentProperty property) {
303435

304436
sink.put(property.getColumnName(), SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType())));
@@ -321,19 +453,38 @@ private Class<?> getPotentiallyConvertedSimpleNullType(Class<?> type) {
321453
}
322454

323455
/**
324-
* Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type.
325-
* Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
456+
* Checks whether we have a custom conversion registered for the given value into an arbitrary simple type. Returns
457+
* the converted value if so. If not, we perform special enum handling or simply return the value as is.
326458
*
327459
* @param value
328460
* @return
329461
*/
330462
@Nullable
331463
private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) {
464+
return getPotentiallyConvertedSimpleWrite(value, Object.class);
465+
}
466+
467+
/**
468+
* Checks whether we have a custom conversion registered for the given value into an arbitrary simple type. Returns
469+
* the converted value if so. If not, we perform special enum handling or simply return the value as is.
470+
*
471+
* @param value
472+
* @return
473+
*/
474+
@Nullable
475+
private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, Class<?> typeHint) {
332476

333477
if (value == null) {
334478
return null;
335479
}
336480

481+
if (Object.class != typeHint) {
482+
483+
if (getConversionService().canConvert(value.getClass(), typeHint)) {
484+
value = getConversionService().convert(value, typeHint);
485+
}
486+
}
487+
337488
Optional<Class<?>> customTarget = getConversions().getCustomWriteTarget(value.getClass());
338489

339490
if (customTarget.isPresent()) {
@@ -350,7 +501,18 @@ private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) {
350501
@Override
351502
public Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) {
352503

353-
Class<?> targetType = arrayColumns.getArrayType(property.getActualType());
504+
Class<?> actualType = null;
505+
if (value instanceof Collection) {
506+
actualType = CollectionUtils.findCommonElementType((Collection<?>) value);
507+
} else if (value.getClass().isArray()) {
508+
actualType = value.getClass().getComponentType();
509+
}
510+
511+
if (actualType == null) {
512+
actualType = property.getActualType();
513+
}
514+
515+
Class<?> targetType = arrayColumns.getArrayType(actualType);
354516

355517
if (!property.isArray() || !targetType.isAssignableFrom(value.getClass())) {
356518

@@ -427,6 +589,23 @@ private <R> RelationalPersistentEntity<R> getRequiredPersistentEntity(Class<R> t
427589
return (RelationalPersistentEntity<R>) getMappingContext().getRequiredPersistentEntity(type);
428590
}
429591

592+
/**
593+
* Returns given object as {@link Collection}. Will return the {@link Collection} as is if the source is a
594+
* {@link Collection} already, will convert an array into a {@link Collection} or simply create a single element
595+
* collection for everything else.
596+
*
597+
* @param source
598+
* @return
599+
*/
600+
private static Collection<?> asCollection(Object source) {
601+
602+
if (source instanceof Collection) {
603+
return (Collection<?>) source;
604+
}
605+
606+
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
607+
}
608+
430609
private static Map<String, ColumnMetadata> createMetadataMap(RowMetadata metadata) {
431610

432611
Map<String, ColumnMetadata> columns = new LinkedHashMap<>();

src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.lang.Nullable;
4848
import org.springframework.util.Assert;
4949
import org.springframework.util.ClassUtils;
50+
import org.springframework.util.CollectionUtils;
5051

5152
/**
5253
* Default {@link ReactiveDataAccessStrategy} implementation.
@@ -244,7 +245,17 @@ private SettableValue getArrayValue(SettableValue value, RelationalPersistentPro
244245
throw new InvalidDataAccessResourceUsageException(
245246
"Dialect " + this.dialect.getClass().getName() + " does not support array columns");
246247
}
247-
Class<?> actualType = property.getActualType();
248+
249+
Class<?> actualType = null;
250+
if (value instanceof Collection) {
251+
actualType = CollectionUtils.findCommonElementType((Collection<?>) value);
252+
} else if (value.getClass().isArray()) {
253+
actualType = value.getClass().getComponentType();
254+
}
255+
256+
if (actualType == null) {
257+
actualType = property.getActualType();
258+
}
248259

249260
if (value.isEmpty()) {
250261

src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import lombok.RequiredArgsConstructor;
99

1010
import java.util.Collection;
11+
import java.util.EnumSet;
1112
import java.util.List;
1213
import java.util.Set;
1314

@@ -111,6 +112,20 @@ public void shouldConvertArrayToBoxedArray() {
111112
assertThat(result.boxedIntegers).contains(3, 11);
112113
}
113114

115+
@Test // gh-252
116+
public void shouldReadEnums() {
117+
118+
EntityRowMapper<WithEnumCollections> mapper = getRowMapper(WithEnumCollections.class);
119+
when(rowMock.get("enum_array")).thenReturn((new String[] { "ONE", "TWO" }));
120+
when(rowMock.get("set_of_enum")).thenReturn((new String[] { "ONE", "THREE" }));
121+
when(rowMock.get("enum_set")).thenReturn((new String[] { "ONE", "TWO" }));
122+
123+
WithEnumCollections result = mapper.apply(rowMock, metadata);
124+
assertThat(result.enumArray).contains(MyEnum.ONE, MyEnum.TWO);
125+
assertThat(result.setOfEnum).contains(MyEnum.ONE, MyEnum.THREE);
126+
assertThat(result.enumSet).contains(MyEnum.ONE, MyEnum.TWO);
127+
}
128+
114129
private <T> EntityRowMapper<T> getRowMapper(Class<T> type) {
115130
return new EntityRowMapper<>(type, strategy.getConverter());
116131
}
@@ -135,4 +150,16 @@ static class EntityWithCollection {
135150
Integer[] boxedIntegers;
136151
int[] primitiveIntegers;
137152
}
153+
154+
static class WithEnumCollections {
155+
156+
MyEnum[] enumArray;
157+
Set<MyEnum> setOfEnum;
158+
EnumSet<MyEnum> enumSet;
159+
}
160+
161+
enum MyEnum {
162+
ONE, TWO, THREE;
163+
}
164+
138165
}

0 commit comments

Comments
 (0)