Skip to content

Commit 73b4986

Browse files
mp911dechristophstrobl
authored andcommitted
Retain target type hint when deserializing Stream records.
We now retain the target type when obtaining a HashMapper through StreamObjectMapper. To achieve this, we introduced the HashObjectReader interface accepting a target type. Resolves: #2198 Related: #1566 Original Pull Request: #2253
1 parent 1932a4c commit 73b4986

File tree

9 files changed

+254
-37
lines changed

9 files changed

+254
-37
lines changed

src/main/java/org/springframework/data/redis/core/StreamObjectMapper.java

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collections;
20+
import java.util.LinkedHashMap;
2021
import java.util.List;
2122
import java.util.Map;
22-
import java.util.stream.Collectors;
2323

2424
import org.springframework.core.convert.ConversionService;
2525
import org.springframework.core.convert.support.DefaultConversionService;
@@ -29,6 +29,7 @@
2929
import org.springframework.data.redis.connection.stream.StreamRecords;
3030
import org.springframework.data.redis.core.convert.RedisCustomConversions;
3131
import org.springframework.data.redis.hash.HashMapper;
32+
import org.springframework.data.redis.hash.HashObjectReader;
3233
import org.springframework.data.redis.hash.ObjectHashMapper;
3334
import org.springframework.lang.Nullable;
3435
import org.springframework.util.Assert;
@@ -72,25 +73,7 @@ class StreamObjectMapper {
7273
this.mapper = (HashMapper) mapper;
7374

7475
if (mapper instanceof ObjectHashMapper) {
75-
76-
ObjectHashMapper ohm = (ObjectHashMapper) mapper;
77-
this.objectHashMapper = new HashMapper<Object, Object, Object>() {
78-
79-
@Override
80-
public Map<Object, Object> toHash(Object object) {
81-
return (Map) ohm.toHash(object);
82-
}
83-
84-
@Override
85-
public Object fromHash(Map<Object, Object> hash) {
86-
87-
Map<byte[], byte[]> map = hash.entrySet().stream()
88-
.collect(Collectors.toMap(e -> conversionService.convert(e.getKey(), byte[].class),
89-
e -> conversionService.convert(e.getValue(), byte[].class)));
90-
91-
return ohm.fromHash(map);
92-
}
93-
};
76+
this.objectHashMapper = new BinaryObjectHashMapperAdapter((ObjectHashMapper) mapper);
9477
} else {
9578
this.objectHashMapper = null;
9679
}
@@ -174,9 +157,27 @@ static <K, V, HK, HV> List<ObjectRecord<K, V>> toObjectRecords(@Nullable List<Ma
174157
return transformed;
175158
}
176159

177-
@SuppressWarnings("unchecked")
160+
@SuppressWarnings({ "unchecked", "rawtypes" })
178161
final <V, HK, HV> HashMapper<V, HK, HV> getHashMapper(Class<V> targetType) {
179-
return (HashMapper) doGetHashMapper(conversionService, targetType);
162+
163+
HashMapper hashMapper = doGetHashMapper(conversionService, targetType);
164+
165+
if (hashMapper instanceof HashObjectReader) {
166+
167+
return new HashMapper<V, HK, HV>() {
168+
@Override
169+
public Map<HK, HV> toHash(V object) {
170+
return hashMapper.toHash(object);
171+
}
172+
173+
@Override
174+
public V fromHash(Map<HK, HV> hash) {
175+
return ((HashObjectReader<HK, HV>) hashMapper).fromHash(targetType, hash);
176+
}
177+
};
178+
}
179+
180+
return hashMapper;
180181
}
181182

182183
/**
@@ -208,4 +209,46 @@ boolean isSimpleType(Class<?> targetType) {
208209
ConversionService getConversionService() {
209210
return conversionService;
210211
}
212+
213+
private static class BinaryObjectHashMapperAdapter
214+
implements HashMapper<Object, Object, Object>, HashObjectReader<Object, Object> {
215+
216+
private final ObjectHashMapper ohm;
217+
218+
public BinaryObjectHashMapperAdapter(ObjectHashMapper ohm) {
219+
this.ohm = ohm;
220+
}
221+
222+
@Override
223+
@SuppressWarnings({ "unchecked", "rawtypes" })
224+
public Map<Object, Object> toHash(Object object) {
225+
return (Map) ohm.toHash(object);
226+
}
227+
228+
@Override
229+
public Object fromHash(Map<Object, Object> hash) {
230+
return ohm.fromHash(toMap(hash));
231+
}
232+
233+
@Override
234+
public <R> R fromHash(Class<R> type, Map<Object, Object> hash) {
235+
return ohm.fromHash(type, toMap(hash));
236+
}
237+
238+
private static Map<byte[], byte[]> toMap(Map<Object, Object> hash) {
239+
240+
Map<byte[], byte[]> target = new LinkedHashMap<>(hash.size());
241+
242+
for (Map.Entry<Object, Object> entry : hash.entrySet()) {
243+
target.put(toBytes(entry.getKey()), toBytes(entry.getValue()));
244+
}
245+
246+
return target;
247+
}
248+
249+
@Nullable
250+
private static byte[] toBytes(Object value) {
251+
return value instanceof byte[] ? (byte[]) value : conversionService.convert(value, byte[].class);
252+
}
253+
}
211254
}

src/main/java/org/springframework/data/redis/hash/BeanUtilsHashMapper.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@
2121

2222
import org.apache.commons.beanutils.BeanUtils;
2323

24+
import org.springframework.util.Assert;
25+
2426
/**
2527
* HashMapper based on Apache Commons BeanUtils project. Does NOT supports nested properties.
2628
*
2729
* @author Costin Leau
2830
* @author Christoph Strobl
2931
* @author Mark Paluch
3032
*/
31-
public class BeanUtilsHashMapper<T> implements HashMapper<T, String, String> {
33+
public class BeanUtilsHashMapper<T> implements HashMapper<T, String, String>, HashObjectReader<String, String> {
3234

3335
private final Class<T> type;
3436

@@ -47,8 +49,20 @@ public BeanUtilsHashMapper(Class<T> type) {
4749
*/
4850
@Override
4951
public T fromHash(Map<String, String> hash) {
52+
return fromHash(type, hash);
53+
}
54+
55+
/*
56+
* (non-Javadoc)
57+
* @see org.springframework.data.redis.hash.HashMapper#fromHash(java.lang.Class, java.util.Map)
58+
*/
59+
@Override
60+
public <R> R fromHash(Class<R> type, Map<String, String> hash) {
61+
62+
Assert.notNull(type, "Type must not be null");
63+
Assert.notNull(hash, "Hash must not be null");
5064

51-
T instance = org.springframework.beans.BeanUtils.instantiateClass(type);
65+
R instance = org.springframework.beans.BeanUtils.instantiateClass(type);
5266

5367
try {
5468

src/main/java/org/springframework/data/redis/hash/HashMapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* @param <V> Redis Hash value type
2727
* @author Costin Leau
2828
* @author Mark Paluch
29+
* @see HashObjectReader
2930
*/
3031
public interface HashMapper<T, K, V> {
3132

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.hash;
17+
18+
import java.util.Map;
19+
20+
/**
21+
* Core mapping contract to materialize an object using particular Java class from a Redis Hash.
22+
*
23+
* @param <K> Redis Hash field type
24+
* @param <V> Redis Hash value type
25+
* @author Mark Paluch
26+
* @since 2.7
27+
* @see HashMapper
28+
*/
29+
public interface HashObjectReader<K, V> {
30+
31+
/**
32+
* Materialize an object of the {@link Class type} from a {@code hash}.
33+
*
34+
* @param hash must not be {@literal null}.
35+
* @return the materialized object from the given {@code hash}.
36+
*/
37+
<R> R fromHash(Class<R> type, Map<K, V> hash);
38+
}

src/main/java/org/springframework/data/redis/hash/Jackson2HashMapper.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 the original author or authors.
2+
* Copyright 2016-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -48,6 +48,7 @@
4848
import com.fasterxml.jackson.databind.JsonDeserializer;
4949
import com.fasterxml.jackson.databind.JsonNode;
5050
import com.fasterxml.jackson.databind.JsonSerializer;
51+
import com.fasterxml.jackson.databind.MapperFeature;
5152
import com.fasterxml.jackson.databind.ObjectMapper;
5253
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
5354
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -148,7 +149,7 @@
148149
* @author Mark Paluch
149150
* @since 1.8
150151
*/
151-
public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
152+
public class Jackson2HashMapper implements HashMapper<Object, String, Object>, HashObjectReader<String, Object> {
152153

153154
private final HashMapperModule HASH_MAPPER_MODULE = new HashMapperModule();
154155

@@ -176,6 +177,12 @@ public boolean useForType(JavaType t) {
176177
}
177178

178179
if (EVERYTHING.equals(_appliesFor)) {
180+
// yuck! Isn't there a better way to distinguish whether there's a registered serializer so that we don't
181+
// use type builders?
182+
if (t.getRawClass().getPackage().getName().startsWith("java.time")) {
183+
return false;
184+
}
185+
179186
return !TreeNode.class.isAssignableFrom(t.getRawClass());
180187
}
181188

@@ -188,12 +195,14 @@ public boolean useForType(JavaType t) {
188195
typingMapper.activateDefaultTyping(typingMapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING,
189196
As.PROPERTY);
190197
typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
198+
typingMapper.configure(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL, true);
191199

192200
// Prevent splitting time types into arrays. E
193201
typingMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
194202
typingMapper.setSerializationInclusion(Include.NON_NULL);
195203
typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
196204
typingMapper.registerModule(HASH_MAPPER_MODULE);
205+
typingMapper.findAndRegisterModules();
197206
}
198207

199208
/**
@@ -209,7 +218,7 @@ public Jackson2HashMapper(ObjectMapper mapper, boolean flatten) {
209218
this.flatten = flatten;
210219

211220
this.untypedMapper = new ObjectMapper();
212-
untypedMapper.findAndRegisterModules();
221+
this.untypedMapper.findAndRegisterModules();
213222
this.untypedMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
214223
this.untypedMapper.setSerializationInclusion(Include.NON_NULL);
215224
}
@@ -232,16 +241,25 @@ public Map<String, Object> toHash(Object source) {
232241
*/
233242
@Override
234243
public Object fromHash(Map<String, Object> hash) {
244+
return fromHash(Object.class, hash);
245+
}
246+
247+
/*
248+
* (non-Javadoc)
249+
* @see org.springframework.data.redis.hash.HashMapper#fromHash(Class, java.util.Map)
250+
*/
251+
@Override
252+
public <R> R fromHash(Class<R> type, Map<String, Object> hash) {
235253

236254
try {
237255

238256
if (flatten) {
239257

240-
return typingMapper.reader().forType(Object.class)
258+
return typingMapper.reader().forType(type)
241259
.readValue(untypedMapper.writeValueAsBytes(doUnflatten(hash)));
242260
}
243261

244-
return typingMapper.treeToValue(untypedMapper.valueToTree(hash), Object.class);
262+
return typingMapper.treeToValue(untypedMapper.valueToTree(hash), type);
245263

246264
} catch (IOException e) {
247265
throw new MappingException(e.getMessage(), e);

src/main/java/org/springframework/data/redis/hash/ObjectHashMapper.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
* @author Mark Paluch
7070
* @since 1.8
7171
*/
72-
public class ObjectHashMapper implements HashMapper<Object, byte[], byte[]> {
72+
public class ObjectHashMapper implements HashMapper<Object, byte[], byte[]>, HashObjectReader<byte[], byte[]> {
7373

7474
@Nullable private volatile static ObjectHashMapper sharedInstance;
7575

@@ -169,12 +169,20 @@ public Map<byte[], byte[]> toHash(Object source) {
169169
*/
170170
@Override
171171
public Object fromHash(Map<byte[], byte[]> hash) {
172+
return fromHash(Object.class, hash);
173+
}
172174

173-
if (hash == null || hash.isEmpty()) {
174-
return null;
175-
}
175+
/*
176+
* (non-Javadoc)
177+
* @see org.springframework.data.redis.hash.HashMapper#fromHash(java.lang.Class, java.util.Map)
178+
*/
179+
@Override
180+
public <R> R fromHash(Class<R> type, Map<byte[], byte[]> hash) {
181+
182+
Assert.notNull(type, "Type must not be null");
183+
Assert.notNull(hash, "Hash must not be null");
176184

177-
return converter.read(Object.class, new RedisData(hash));
185+
return converter.read(type, new RedisData(hash));
178186
}
179187

180188
/**
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.core;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import lombok.Data;
21+
22+
import java.util.Collections;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.data.redis.hash.Jackson2HashMapper;
27+
import org.springframework.data.redis.hash.ObjectHashMapper;
28+
29+
/**
30+
* Unit tests for {@link StreamObjectMapper}.
31+
*
32+
* @author Mark Paluch
33+
*/
34+
class StreamObjectMapperUnitTests {
35+
36+
@Test // GH-2198
37+
void shouldRetainTypeHintUsingObjectHashMapper() {
38+
39+
StreamObjectMapper mapper = new StreamObjectMapper(ObjectHashMapper.getSharedInstance());
40+
41+
MyType result = mapper.getHashMapper(MyType.class)
42+
.fromHash(Collections.singletonMap("value".getBytes(), "hello".getBytes()));
43+
44+
assertThat(result.value).isEqualTo("hello");
45+
}
46+
47+
@Test // GH-2198
48+
void shouldRetainTypeHintUsingJackson() {
49+
50+
StreamObjectMapper mapper = new StreamObjectMapper(new Jackson2HashMapper(true));
51+
52+
MyType result = mapper.getHashMapper(MyType.class).fromHash(Collections.singletonMap("value", "hello"));
53+
54+
assertThat(result.value).isEqualTo("hello");
55+
}
56+
57+
@Data
58+
static class MyType {
59+
String value;
60+
}
61+
62+
}

0 commit comments

Comments
 (0)