Skip to content

Commit 2d6dac8

Browse files
DATAREDIS-489 - Add type hints for Object types.
We now store the type hint for simple types when the declaring bean property does not match the actual value type.
1 parent 5de59d2 commit 2d6dac8

File tree

3 files changed

+174
-18
lines changed

3 files changed

+174
-18
lines changed

src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.data.mapping.PersistentPropertyAccessor;
4646
import org.springframework.data.mapping.PreferredConstructor;
4747
import org.springframework.data.mapping.PropertyHandler;
48+
import org.springframework.data.mapping.model.MappingException;
4849
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
4950
import org.springframework.data.mapping.model.PropertyValueProvider;
5051
import org.springframework.data.redis.core.index.Indexed;
@@ -103,6 +104,8 @@
103104
*/
104105
public class MappingRedisConverter implements RedisConverter, InitializingBean {
105106

107+
private static final String TYPE_HINT_ALIAS = "_class";
108+
106109
private final RedisMappingContext mappingContext;
107110
private final GenericConversionService conversionService;
108111
private final EntityInstantiators entityInstantiators;
@@ -247,9 +250,9 @@ else if (persistentProperty.isCollectionLike()) {
247250

248251
RedisData source = new RedisData(bucket);
249252

250-
byte[] type = bucket.get(currentPath + "._class");
253+
byte[] type = bucket.get(currentPath + "." + TYPE_HINT_ALIAS);
251254
if (type != null && type.length > 0) {
252-
source.getBucket().put("_class", type);
255+
source.getBucket().put(TYPE_HINT_ALIAS, type);
253256
}
254257

255258
accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, source));
@@ -265,8 +268,8 @@ else if (persistentProperty.isCollectionLike()) {
265268
}
266269
}
267270

268-
accessor.setProperty(persistentProperty,
269-
fromBytes(source.getBucket().get(currentPath), persistentProperty.getActualType()));
271+
Class<?> typeToUse = getTypeHint(currentPath, source.getBucket(), persistentProperty.getActualType());
272+
accessor.setProperty(persistentProperty, fromBytes(source.getBucket().get(currentPath), typeToUse));
270273
}
271274
}
272275

@@ -376,16 +379,17 @@ private void writeInternal(final String keyspace, final String path, final Objec
376379

377380
if (customConversions.hasCustomWriteTarget(value.getClass())) {
378381

379-
if (customConversions.getCustomWriteTarget(value.getClass()).equals(byte[].class)) {
382+
if (!StringUtils.hasText(path) && customConversions.getCustomWriteTarget(value.getClass()).equals(byte[].class)) {
380383
sink.getBucket().put(StringUtils.hasText(path) ? path : "_raw", conversionService.convert(value, byte[].class));
381384
} else {
382-
writeToBucket(path, value, sink);
385+
writeToBucket(path, value, sink, typeHint.getType());
383386
}
384387
return;
385388
}
386389

387390
if (value.getClass() != typeHint.getType()) {
388-
sink.getBucket().put((!path.isEmpty() ? path + "._class" : "_class"), toBytes(value.getClass().getName()));
391+
sink.getBucket().put((!path.isEmpty() ? path + "." + TYPE_HINT_ALIAS : TYPE_HINT_ALIAS),
392+
toBytes(value.getClass().getName()));
389393
}
390394

391395
final KeyValuePersistentEntity<?> entity = mappingContext.getPersistentEntity(value.getClass());
@@ -416,7 +420,7 @@ public void doWithPersistentProperty(KeyValuePersistentProperty persistentProper
416420
} else {
417421

418422
Object propertyValue = accessor.getProperty(persistentProperty);
419-
sink.getBucket().put(propertyStringPath, toBytes(propertyValue));
423+
writeToBucket(propertyStringPath, propertyValue, sink, persistentProperty.getType());
420424
}
421425
}
422426
});
@@ -494,15 +498,15 @@ private void writeCollection(String keyspace, String path, Collection<?> values,
494498
String currentPath = path + ".[" + i + "]";
495499

496500
if (customConversions.hasCustomWriteTarget(value.getClass())) {
497-
writeToBucket(currentPath, value, sink);
501+
writeToBucket(currentPath, value, sink, typeHint.getType());
498502
} else {
499503
writeInternal(keyspace, currentPath, value, typeHint, sink);
500504
}
501505
i++;
502506
}
503507
}
504508

505-
private void writeToBucket(String path, Object value, RedisData sink) {
509+
private void writeToBucket(String path, Object value, RedisData sink, Class<?> propertyType) {
506510

507511
if (value == null) {
508512
return;
@@ -512,6 +516,12 @@ private void writeToBucket(String path, Object value, RedisData sink) {
512516

513517
Class<?> targetType = customConversions.getCustomWriteTarget(value.getClass());
514518

519+
if (!ClassUtils.isAssignable(Map.class, targetType) && customConversions.isSimpleType(value.getClass())
520+
&& value.getClass() != propertyType) {
521+
sink.getBucket().put((!path.isEmpty() ? path + "." + TYPE_HINT_ALIAS : TYPE_HINT_ALIAS),
522+
toBytes(value.getClass().getName()));
523+
}
524+
515525
if (ClassUtils.isAssignable(Map.class, targetType)) {
516526

517527
Map<?, ?> map = (Map<?, ?>) conversionService.convert(value, targetType);
@@ -547,7 +557,13 @@ private Collection<?> readCollectionOfSimpleTypes(String path, Class<?> collecti
547557
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, partial.size());
548558

549559
for (String key : keys) {
550-
target.add(fromBytes(partial.get(key), valueType));
560+
561+
if (key.endsWith(TYPE_HINT_ALIAS)) {
562+
continue;
563+
}
564+
565+
Class<?> typeToUse = getTypeHint(key, source.getBucket(), valueType);
566+
target.add(fromBytes(partial.get(key), typeToUse));
551567
}
552568

553569
return target;
@@ -572,9 +588,9 @@ private Collection<?> readCollectionOfComplexTypes(String path, Class<?> collect
572588

573589
Bucket elementData = source.extract(key);
574590

575-
byte[] typeInfo = elementData.get(key + "._class");
591+
byte[] typeInfo = elementData.get(key + "." + TYPE_HINT_ALIAS);
576592
if (typeInfo != null && typeInfo.length > 0) {
577-
elementData.put("_class", typeInfo);
593+
elementData.put(TYPE_HINT_ALIAS, typeInfo);
578594
}
579595

580596
Object o = readInternal(key, valueType, new RedisData(elementData));
@@ -606,7 +622,7 @@ private void writeMap(String keyspace, String path, Class<?> mapValueType, Map<?
606622
String currentPath = path + ".[" + entry.getKey() + "]";
607623

608624
if (customConversions.hasCustomWriteTarget(entry.getValue().getClass())) {
609-
writeToBucket(currentPath, entry.getValue(), sink);
625+
writeToBucket(currentPath, entry.getValue(), sink, mapValueType);
610626
} else {
611627
writeInternal(keyspace, currentPath, entry.getValue(), ClassTypeInformation.from(mapValueType), sink);
612628
}
@@ -630,6 +646,10 @@ private void writeMap(String keyspace, String path, Class<?> mapValueType, Map<?
630646

631647
for (Entry<String, byte[]> entry : partial.entrySet()) {
632648

649+
if (entry.getKey().endsWith(TYPE_HINT_ALIAS)) {
650+
continue;
651+
}
652+
633653
String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])";
634654
Pattern pattern = Pattern.compile(regex);
635655

@@ -639,7 +659,9 @@ private void writeMap(String keyspace, String path, Class<?> mapValueType, Map<?
639659
String.format("Cannot extract map value for key '%s' in path '%s'.", entry.getKey(), path));
640660
}
641661
String key = matcher.group(2);
642-
target.put(key, fromBytes(entry.getValue(), valueType));
662+
663+
Class<?> typeToUse = getTypeHint(path + ".[" + key + "]", source.getBucket(), valueType);
664+
target.put(key, fromBytes(entry.getValue(), typeToUse));
643665
}
644666

645667
return target;
@@ -674,9 +696,9 @@ private void writeMap(String keyspace, String path, Class<?> mapValueType, Map<?
674696

675697
Bucket partial = source.getBucket().extract(key);
676698

677-
byte[] typeInfo = partial.get(key + "._class");
699+
byte[] typeInfo = partial.get(key + "." + TYPE_HINT_ALIAS);
678700
if (typeInfo != null && typeInfo.length > 0) {
679-
partial.put("_class", typeInfo);
701+
partial.put(TYPE_HINT_ALIAS, typeInfo);
680702
}
681703

682704
Object o = readInternal(key, valueType, new RedisData(partial));
@@ -686,6 +708,24 @@ private void writeMap(String keyspace, String path, Class<?> mapValueType, Map<?
686708
return target;
687709
}
688710

711+
private Class<?> getTypeHint(String path, Bucket bucket, Class<?> fallback) {
712+
713+
byte[] typeInfo = bucket.get(path + "." + TYPE_HINT_ALIAS);
714+
715+
if (typeInfo == null || typeInfo.length < 1) {
716+
return fallback;
717+
}
718+
719+
String typeName = fromBytes(typeInfo, String.class);
720+
try {
721+
return ClassUtils.forName(typeName, this.getClass().getClassLoader());
722+
} catch (ClassNotFoundException e) {
723+
throw new MappingException(String.format("Cannot find class for type %s. ", typeName), e);
724+
} catch (LinkageError e) {
725+
throw new MappingException(String.format("Cannot find class for type %s. ", typeName), e);
726+
}
727+
}
728+
689729
/**
690730
* Convert given source to binary representation using the underlying {@link ConversionService}.
691731
*
@@ -789,7 +829,7 @@ private static class RedisTypeAliasAccessor implements TypeAliasAccessor<RedisDa
789829
private final ConversionService conversionService;
790830

791831
RedisTypeAliasAccessor(ConversionService conversionService) {
792-
this(conversionService, "_class");
832+
this(conversionService, TYPE_HINT_ALIAS);
793833
}
794834

795835
RedisTypeAliasAccessor(ConversionService conversionService, String typeKey) {

src/test/java/org/springframework/data/redis/core/convert/ConversionTestEntities.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import java.time.Period;
2424
import java.time.ZoneId;
2525
import java.time.ZonedDateTime;
26+
import java.util.ArrayList;
2627
import java.util.Date;
28+
import java.util.HashMap;
2729
import java.util.List;
2830
import java.util.Map;
2931
import java.util.concurrent.TimeUnit;
@@ -160,4 +162,9 @@ public static class Size {
160162
int length;
161163
}
162164

165+
static class TypeWithObjectValueTypes {
166+
Object object;
167+
Map<String, Object> map = new HashMap<String, Object>();
168+
List<Object> list = new ArrayList<Object>();
169+
}
163170
}

src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.springframework.data.redis.core.convert.ConversionTestEntities.Species;
6666
import org.springframework.data.redis.core.convert.ConversionTestEntities.TaVeren;
6767
import org.springframework.data.redis.core.convert.ConversionTestEntities.TheWheelOfTime;
68+
import org.springframework.data.redis.core.convert.ConversionTestEntities.TypeWithObjectValueTypes;
6869
import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings;
6970
import org.springframework.data.redis.core.mapping.RedisMappingContext;
7071
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
@@ -1325,6 +1326,114 @@ public void readShouldConsiderMapConvertersForValuesInList() {
13251326
assertThat(target.species.get(0).name, is("trolloc"));
13261327
}
13271328

1329+
/**
1330+
* @see DATAREDIS-489
1331+
*/
1332+
@Test
1333+
public void writeShouldAppendTyeHintToObjectPropertyValueTypesCorrectly() {
1334+
1335+
TypeWithObjectValueTypes sample = new TypeWithObjectValueTypes();
1336+
sample.object = "bar";
1337+
1338+
Bucket bucket = write(sample).getBucket();
1339+
1340+
assertThat(bucket,
1341+
isBucket().containingUtf8String("object", "bar").containingUtf8String("object._class", "java.lang.String"));
1342+
}
1343+
1344+
/**
1345+
* @see DATAREDIS-489
1346+
*/
1347+
@Test
1348+
public void shouldWriteReadObjectPropertyValueTypeCorrectly() {
1349+
1350+
TypeWithObjectValueTypes di = new TypeWithObjectValueTypes();
1351+
di.object = "foo";
1352+
1353+
RedisData rd = write(di);
1354+
1355+
TypeWithObjectValueTypes result = converter.read(TypeWithObjectValueTypes.class, rd);
1356+
assertThat(result.object, instanceOf(String.class));
1357+
}
1358+
1359+
/**
1360+
* @see DATAREDIS-489
1361+
*/
1362+
@Test
1363+
public void writeShouldAppendTyeHintToObjectMapValueTypesCorrectly() {
1364+
1365+
TypeWithObjectValueTypes sample = new TypeWithObjectValueTypes();
1366+
sample.map.put("string", "bar");
1367+
sample.map.put("long", new Long(1L));
1368+
sample.map.put("date", new Date());
1369+
1370+
Bucket bucket = write(sample).getBucket();
1371+
1372+
assertThat(bucket, isBucket().containingUtf8String("map.[string]", "bar")
1373+
.containingUtf8String("map.[string]._class", "java.lang.String"));
1374+
assertThat(bucket,
1375+
isBucket().containingUtf8String("map.[long]", "1").containingUtf8String("map.[long]._class", "java.lang.Long"));
1376+
assertThat(bucket, isBucket().containingUtf8String("map.[date]._class", "java.util.Date"));
1377+
}
1378+
1379+
/**
1380+
* @see DATAREDIS-489
1381+
*/
1382+
@Test
1383+
public void shouldWriteReadObjectMapValueTypeCorrectly() {
1384+
1385+
TypeWithObjectValueTypes sample = new TypeWithObjectValueTypes();
1386+
sample.map.put("string", "bar");
1387+
sample.map.put("long", new Long(1L));
1388+
sample.map.put("date", new Date());
1389+
1390+
RedisData rd = write(sample);
1391+
1392+
TypeWithObjectValueTypes result = converter.read(TypeWithObjectValueTypes.class, rd);
1393+
assertThat(result.map.get("string"), instanceOf(String.class));
1394+
assertThat(result.map.get("long"), instanceOf(Long.class));
1395+
assertThat(result.map.get("date"), instanceOf(Date.class));
1396+
}
1397+
1398+
/**
1399+
* @see DATAREDIS-489
1400+
*/
1401+
@Test
1402+
public void writeShouldAppendTyeHintToObjectListValueTypesCorrectly() {
1403+
1404+
TypeWithObjectValueTypes sample = new TypeWithObjectValueTypes();
1405+
sample.list.add("string");
1406+
sample.list.add(new Long(1L));
1407+
sample.list.add(new Date());
1408+
1409+
Bucket bucket = write(sample).getBucket();
1410+
1411+
assertThat(bucket, isBucket().containingUtf8String("list.[0]", "string").containingUtf8String("list.[0]._class",
1412+
"java.lang.String"));
1413+
assertThat(bucket,
1414+
isBucket().containingUtf8String("list.[1]", "1").containingUtf8String("list.[1]._class", "java.lang.Long"));
1415+
assertThat(bucket, isBucket().containingUtf8String("list.[2]._class", "java.util.Date"));
1416+
}
1417+
1418+
/**
1419+
* @see DATAREDIS-489
1420+
*/
1421+
@Test
1422+
public void shouldWriteReadObjectListValueTypeCorrectly() {
1423+
1424+
TypeWithObjectValueTypes sample = new TypeWithObjectValueTypes();
1425+
sample.list.add("string");
1426+
sample.list.add(new Long(1L));
1427+
sample.list.add(new Date());
1428+
1429+
RedisData rd = write(sample);
1430+
1431+
TypeWithObjectValueTypes result = converter.read(TypeWithObjectValueTypes.class, rd);
1432+
assertThat(result.list.get(0), instanceOf(String.class));
1433+
assertThat(result.list.get(1), instanceOf(Long.class));
1434+
assertThat(result.list.get(2), instanceOf(Date.class));
1435+
}
1436+
13281437
private RedisData write(Object source) {
13291438

13301439
RedisData rdo = new RedisData();

0 commit comments

Comments
 (0)