Skip to content

Commit f8a9fdf

Browse files
committed
Polish.
* Cleanup source code in Jackson2HashMapper; Edit Javadoc. * Add test cases for the un/flattened mapping of an Object with a timestamp (LocalDateTime) as provided by the user in Issue #2593. Closes #2593
1 parent 637964e commit f8a9fdf

File tree

4 files changed

+170
-93
lines changed

4 files changed

+170
-93
lines changed

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

+94-84
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
* Date date;
8686
* LocalDateTime localDateTime;
8787
* }
88-
*
88+
* <p>
8989
* class Address {
9090
* String city;
9191
* String country;
@@ -153,110 +153,119 @@
153153
*
154154
* @author Christoph Strobl
155155
* @author Mark Paluch
156+
* @author John Blum
156157
* @since 1.8
157158
*/
158159
public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
159160

160-
private static final boolean SOURCE_VERSION_PRESENT = ClassUtils.isPresent("javax.lang.model.SourceVersion", Jackson2HashMapper.class.getClassLoader());
161-
162-
private final HashMapperModule HASH_MAPPER_MODULE = new HashMapperModule();
161+
private static final boolean SOURCE_VERSION_PRESENT =
162+
ClassUtils.isPresent("javax.lang.model.SourceVersion", Jackson2HashMapper.class.getClassLoader());
163163

164164
private final ObjectMapper typingMapper;
165165
private final ObjectMapper untypedMapper;
166166
private final boolean flatten;
167167

168168
/**
169-
* Creates new {@link Jackson2HashMapper} with default {@link ObjectMapper}.
169+
* Creates new {@link Jackson2HashMapper} with a default {@link ObjectMapper}.
170170
*
171-
* @param flatten
171+
* @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties
172+
* will be un/flattened using {@literal dot notation}, or whether to retain the hierarchical node structure
173+
* created by Jackson.
172174
*/
173175
public Jackson2HashMapper(boolean flatten) {
174176

175177
this(new ObjectMapper() {
176178

177179
@Override
178180
protected TypeResolverBuilder<?> _constructDefaultTypeResolverBuilder(DefaultTyping applicability,
179-
PolymorphicTypeValidator ptv) {
180-
return new DefaultTypeResolverBuilder(applicability, ptv) {
181-
public boolean useForType(JavaType t) {
181+
PolymorphicTypeValidator typeValidator) {
182+
183+
return new DefaultTypeResolverBuilder(applicability, typeValidator) {
182184

183-
if (t.isPrimitive()) {
185+
public boolean useForType(JavaType type) {
186+
187+
if (type.isPrimitive()) {
184188
return false;
185189
}
186190

187-
if(flatten && t.isTypeOrSubTypeOf(Number.class)) {
191+
if (flatten && type.isTypeOrSubTypeOf(Number.class)) {
188192
return false;
189193
}
190194

191195
if (EVERYTHING.equals(_appliesFor)) {
192-
return !TreeNode.class.isAssignableFrom(t.getRawClass());
196+
return !TreeNode.class.isAssignableFrom(type.getRawClass());
193197
}
194198

195-
return super.useForType(t);
199+
return super.useForType(type);
196200
}
197201
};
198202
}
199203
}.findAndRegisterModules(), flatten);
200204

201-
typingMapper.activateDefaultTyping(typingMapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING,
202-
As.PROPERTY);
203-
typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
205+
this.typingMapper.activateDefaultTyping(this.typingMapper.getPolymorphicTypeValidator(),
206+
DefaultTyping.EVERYTHING, As.PROPERTY);
207+
this.typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
208+
204209
if(flatten) {
205-
typingMapper.disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES);
210+
this.typingMapper.disable(MapperFeature.REQUIRE_TYPE_ID_FOR_SUBTYPES);
206211
}
207212

208213
// Prevent splitting time types into arrays. E
209-
typingMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
210-
typingMapper.setSerializationInclusion(Include.NON_NULL);
211-
typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
212-
typingMapper.registerModule(HASH_MAPPER_MODULE);
214+
this.typingMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
215+
this.typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
216+
this.typingMapper.setSerializationInclusion(Include.NON_NULL);
217+
this.typingMapper.registerModule(new HashMapperModule());
213218
}
214219

215220
/**
216-
* Creates new {@link Jackson2HashMapper}.
221+
* Creates new {@link Jackson2HashMapper} initialized with a custom Jackson {@link ObjectMapper}.
217222
*
218-
* @param mapper must not be {@literal null}.
219-
* @param flatten
223+
* @param mapper Jackson {@link ObjectMapper} used to de/serialize hashed {@link Object objects};
224+
* must not be {@literal null}.
225+
* @param flatten boolean used to configure whether JSON de/serialized {@link Object} properties
226+
* will be un/flattened using {@literal dot notation}, or whether to retain the hierarchical node structure
227+
* created by Jackson.
220228
*/
221229
public Jackson2HashMapper(ObjectMapper mapper, boolean flatten) {
222230

223231
Assert.notNull(mapper, "Mapper must not be null");
224-
this.typingMapper = mapper;
225-
this.flatten = flatten;
226232

233+
this.flatten = flatten;
234+
this.typingMapper = mapper;
227235
this.untypedMapper = new ObjectMapper();
228-
untypedMapper.findAndRegisterModules();
229236
this.untypedMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
230237
this.untypedMapper.setSerializationInclusion(Include.NON_NULL);
238+
this.untypedMapper.findAndRegisterModules();
231239
}
232240

233241
@Override
234242
@SuppressWarnings("unchecked")
235243
public Map<String, Object> toHash(Object source) {
236244

237-
JsonNode tree = typingMapper.valueToTree(source);
238-
return flatten ? flattenMap(tree.fields()) : untypedMapper.convertValue(tree, Map.class);
245+
JsonNode tree = this.typingMapper.valueToTree(source);
246+
247+
return this.flatten ? flattenMap(tree.fields()) : this.untypedMapper.convertValue(tree, Map.class);
239248
}
240249

241250
@Override
251+
@SuppressWarnings("all")
242252
public Object fromHash(Map<String, Object> hash) {
243253

244254
try {
245-
246-
if (flatten) {
255+
if (this.flatten) {
247256

248257
Map<String, Object> unflattenedHash = doUnflatten(hash);
249-
byte[] unflattenedHashedBytes = untypedMapper.writeValueAsBytes(unflattenedHash);
250-
Object hashedObject = typingMapper.reader().forType(Object.class)
258+
byte[] unflattenedHashedBytes = this.untypedMapper.writeValueAsBytes(unflattenedHash);
259+
Object hashedObject = this.typingMapper.reader().forType(Object.class)
251260
.readValue(unflattenedHashedBytes);
252261

253262
return hashedObject;
254263
}
255264

256-
return typingMapper.treeToValue(untypedMapper.valueToTree(hash), Object.class);
265+
return this.typingMapper.treeToValue(this.untypedMapper.valueToTree(hash), Object.class);
257266

258-
} catch (IOException e) {
259-
throw new MappingException(e.getMessage(), e);
267+
} catch (IOException cause) {
268+
throw new MappingException(cause.getMessage(), cause);
260269
}
261270
}
262271

@@ -272,11 +281,8 @@ private Map<String, Object> doUnflatten(Map<String, Object> source) {
272281
String[] keyParts = key.split("\\.");
273282

274283
if (keyParts.length == 1 && isNotIndexed(keyParts[0])) {
275-
result.put(entry.getKey(), entry.getValue());
276-
continue;
277-
}
278-
279-
if (keyParts.length == 1 && isIndexed(keyParts[0])) {
284+
result.put(key, entry.getValue());
285+
} else if (keyParts.length == 1 && isIndexed(keyParts[0])) {
280286

281287
String indexedKeyName = keyParts[0];
282288
String nonIndexedKeyName = stripIndex(indexedKeyName);
@@ -290,21 +296,25 @@ private Map<String, Object> doUnflatten(Map<String, Object> source) {
290296
result.put(nonIndexedKeyName, createTypedListWithValue(index, entry.getValue()));
291297
}
292298
} else {
293-
treatSeparate.add(key.substring(0, key.indexOf('.')));
299+
treatSeparate.add(keyParts[0]);
294300
}
295301
}
296302

297303
for (String partial : treatSeparate) {
298304

299305
Map<String, Object> newSource = new LinkedHashMap<>();
300306

307+
// Copies all nested, dot properties from the source Map to the new Map beginning from
308+
// the next nested (dot) property
301309
for (Entry<String, Object> entry : source.entrySet()) {
302-
if (entry.getKey().startsWith(partial)) {
303-
newSource.put(entry.getKey().substring(partial.length() + 1), entry.getValue());
310+
String key = entry.getKey();
311+
if (key.startsWith(partial)) {
312+
String keyAfterDot = key.substring(partial.length() + 1);
313+
newSource.put(keyAfterDot, entry.getValue());
304314
}
305315
}
306316

307-
if (partial.endsWith("]")) {
317+
if (isNonNestedIndexed(partial)) {
308318

309319
String nonIndexPartial = stripIndex(partial);
310320
int index = getIndex(partial);
@@ -330,6 +340,10 @@ private boolean isNotIndexed(@NonNull String value) {
330340
return !isIndexed(value);
331341
}
332342

343+
private boolean isNonNestedIndexed(@NonNull String value) {
344+
return value.endsWith("]");
345+
}
346+
333347
private int getIndex(@NonNull String indexedValue) {
334348
return Integer.parseInt(indexedValue.substring(indexedValue.indexOf('[') + 1, indexedValue.length() - 1));
335349
}
@@ -346,7 +360,7 @@ private int getIndex(@NonNull String indexedValue) {
346360
private Map<String, Object> flattenMap(Iterator<Entry<String, JsonNode>> source) {
347361

348362
Map<String, Object> resultMap = new HashMap<>();
349-
this.doFlatten("", source, resultMap);
363+
doFlatten("", source, resultMap);
350364
return resultMap;
351365
}
352366

@@ -378,56 +392,52 @@ private void flattenElement(String propertyPrefix, Object source, Map<String, Ob
378392

379393
while (nodes.hasNext()) {
380394

381-
JsonNode cur = nodes.next();
382-
383-
if (cur.isArray()) {
384-
this.flattenCollection(propertyPrefix, cur.elements(), resultMap);
385-
} else {
386-
if (nodes.hasNext() && mightBeJavaType(cur)) {
395+
JsonNode currentNode = nodes.next();
387396

388-
JsonNode next = nodes.next();
397+
if (currentNode.isArray()) {
398+
flattenCollection(propertyPrefix, currentNode.elements(), resultMap);
399+
} else if (nodes.hasNext() && mightBeJavaType(currentNode)) {
389400

390-
if (next.isArray()) {
391-
this.flattenCollection(propertyPrefix, next.elements(), resultMap);
392-
}
401+
JsonNode next = nodes.next();
393402

394-
if (cur.asText().equals("java.util.Date")) {
395-
resultMap.put(propertyPrefix, next.asText());
396-
break;
397-
}
398-
if (next.isNumber()) {
399-
resultMap.put(propertyPrefix, next.numberValue());
400-
break;
401-
}
402-
if (next.isTextual()) {
403+
if (next.isArray()) {
404+
flattenCollection(propertyPrefix, next.elements(), resultMap);
405+
}
406+
if (currentNode.asText().equals("java.util.Date")) {
407+
resultMap.put(propertyPrefix, next.asText());
408+
break;
409+
}
410+
if (next.isNumber()) {
411+
resultMap.put(propertyPrefix, next.numberValue());
412+
break;
413+
}
414+
if (next.isTextual()) {
415+
resultMap.put(propertyPrefix, next.textValue());
416+
break;
417+
}
418+
if (next.isBoolean()) {
419+
resultMap.put(propertyPrefix, next.booleanValue());
420+
break;
421+
}
422+
if (next.isBinary()) {
403423

404-
resultMap.put(propertyPrefix, next.textValue());
405-
break;
424+
try {
425+
resultMap.put(propertyPrefix, next.binaryValue());
406426
}
407-
if (next.isBoolean()) {
408-
409-
resultMap.put(propertyPrefix, next.booleanValue());
410-
break;
427+
catch (IOException cause) {
428+
String message = String.format("Cannot read binary value of '%s'", propertyPrefix);
429+
throw new IllegalStateException(message, cause);
411430
}
412-
if (next.isBinary()) {
413431

414-
try {
415-
resultMap.put(propertyPrefix, next.binaryValue());
416-
} catch (IOException cause) {
417-
String message = String.format("Cannot read binary value of '%s'", propertyPrefix);
418-
throw new IllegalStateException(message, cause);
419-
}
420-
421-
break;
422-
}
432+
break;
423433
}
424434
}
425435
}
426-
427436
} else if (element.isContainerNode()) {
428-
this.doFlatten(propertyPrefix, element.fields(), resultMap);
437+
doFlatten(propertyPrefix, element.fields(), resultMap);
429438
} else {
430-
resultMap.put(propertyPrefix, new DirectFieldAccessFallbackBeanWrapper(element).getPropertyValue("_value"));
439+
resultMap.put(propertyPrefix, new DirectFieldAccessFallbackBeanWrapper(element)
440+
.getPropertyValue("_value"));
431441
}
432442
}
433443

src/test/java/org/springframework/data/redis/mapping/Jackson2HashMapperFlatteningUnitTests.java

+32
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,47 @@
1515
*/
1616
package org.springframework.data.redis.mapping;
1717

18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.time.LocalDateTime;
21+
import java.time.Month;
22+
import java.util.Map;
23+
24+
import org.junit.jupiter.api.Test;
25+
1826
import org.springframework.data.redis.hash.Jackson2HashMapper;
1927

28+
import lombok.Data;
29+
2030
/**
2131
* @author Christoph Strobl
32+
* @author John Blum
2233
* @since 2023/06
2334
*/
2435
public class Jackson2HashMapperFlatteningUnitTests extends Jackson2HashMapperUnitTests {
2536

2637
Jackson2HashMapperFlatteningUnitTests() {
2738
super(new Jackson2HashMapper(true));
2839
}
40+
41+
@Test // GH-2593
42+
void timestampHandledCorrectly() {
43+
44+
Map<String, Object> hash =
45+
Map.of("@class", Session.class.getName(), "lastAccessed", "2023-06-05T18:36:30");
46+
47+
//Map<String, Object> hash = Map.of("lastAccessed", "2023-06-05T18:36:30");
48+
49+
Session session = (Session) getMapper().fromHash(hash);
50+
51+
assertThat(session).isNotNull();
52+
assertThat(session.getLastAccessed()).isEqualTo(LocalDateTime.of(2023, Month.JUNE, 5,
53+
18, 36, 30));
54+
}
55+
56+
57+
@Data
58+
private static class Session {
59+
private LocalDateTime lastAccessed;
60+
}
2961
}

0 commit comments

Comments
 (0)