Skip to content

Commit f092371

Browse files
OMG it works - who would have thought that
1 parent fcb0095 commit f092371

File tree

3 files changed

+208
-24
lines changed

3 files changed

+208
-24
lines changed

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

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,32 @@
3030
import tools.jackson.databind.ValueSerializer;
3131
import tools.jackson.databind.cfg.MapperBuilder;
3232
import tools.jackson.databind.deser.jdk.JavaUtilCalendarDeserializer;
33+
import tools.jackson.databind.deser.jdk.JavaUtilDateDeserializer;
34+
import tools.jackson.databind.deser.jdk.NumberDeserializers.BigDecimalDeserializer;
35+
import tools.jackson.databind.deser.jdk.NumberDeserializers.BigIntegerDeserializer;
36+
import tools.jackson.databind.deser.std.StdDeserializer;
37+
import tools.jackson.databind.exc.MismatchedInputException;
3338
import tools.jackson.databind.json.JsonMapper;
3439
import tools.jackson.databind.json.JsonMapper.Builder;
3540
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
41+
import tools.jackson.databind.jsontype.TypeDeserializer;
42+
import tools.jackson.databind.jsontype.TypeSerializer;
43+
import tools.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer;
3644
import tools.jackson.databind.module.SimpleDeserializers;
3745
import tools.jackson.databind.module.SimpleSerializers;
3846
import tools.jackson.databind.ser.Serializers;
3947
import tools.jackson.databind.ser.jdk.JavaUtilCalendarSerializer;
4048
import tools.jackson.databind.ser.jdk.JavaUtilDateSerializer;
4149

50+
import java.math.BigDecimal;
51+
import java.math.BigInteger;
4252
import java.time.ZoneId;
4353
import java.util.ArrayList;
4454
import java.util.Arrays;
4555
import java.util.Calendar;
56+
import java.util.Collection;
4657
import java.util.Collections;
47-
import java.util.GregorianCalendar;
58+
import java.util.Date;
4859
import java.util.HashMap;
4960
import java.util.Iterator;
5061
import java.util.LinkedHashMap;
@@ -176,10 +187,9 @@ public Jackson3HashMapper(
176187
public static void preconfigure(MapperBuilder<? extends ObjectMapper, ? extends MapperBuilder<?, ?>> builder) {
177188
builder.findAndAddModules().addModules(new HashMapperModule())
178189
.activateDefaultTypingAsProperty(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class)
179-
.allowIfSubType((ctx, clazz) -> true).build(), DefaultTyping.NON_FINAL_AND_ENUMS, "@class")
190+
.allowIfSubType((ctx, clazz) -> true).build(), DefaultTyping.NON_FINAL, "@class")
180191
.configure(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY, false)
181192
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
182-
//.configure(DeserializationFeature., false)
183193
.changeDefaultPropertyInclusion(value -> value.withValueInclusion(Include.NON_NULL));
184194
}
185195

@@ -197,7 +207,7 @@ public Jackson3HashMapper(ObjectMapper mapper, boolean flatten) {
197207

198208
this.flatten = flatten;
199209
this.typingMapper = mapper;
200-
this.untypedMapper = mapper.rebuild().deactivateDefaultTyping().build();
210+
this.untypedMapper = JsonMapper.shared();
201211
}
202212

203213
@Override
@@ -216,7 +226,6 @@ public Object fromHash(Map<String, Object> hash) {
216226
if (this.flatten) {
217227

218228
Map<String, Object> unflattenedHash = doUnflatten(hash);
219-
System.out.println("unflat: " + unflattenedHash);
220229
byte[] unflattenedHashedBytes = this.untypedMapper.writeValueAsBytes(unflattenedHash);
221230
Object hashedObject = this.typingMapper.reader().forType(Object.class).readValue(unflattenedHashedBytes);
222231

@@ -328,9 +337,7 @@ private void doFlatten(String propertyPrefix, Set<Entry<String, JsonNode>> input
328337
propertyPrefix = propertyPrefix + ".";
329338
}
330339

331-
Iterator<Entry<String, JsonNode>> entries = inputMap.iterator();
332-
while (entries.hasNext()) {
333-
Entry<String, JsonNode> entry = entries.next();
340+
for(Entry<String, JsonNode> entry : inputMap) {
334341
flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap);
335342
}
336343
}
@@ -351,24 +358,24 @@ private void flattenElement(String propertyPrefix, Object source, Map<String, Ob
351358
JsonNode currentNode = nodes.next();
352359

353360
if (currentNode.isArray()) {
354-
flattenCollection(propertyPrefix, currentNode.values().iterator(), resultMap);
361+
flattenCollection(propertyPrefix, currentNode.values(), resultMap);
355362
} else if (nodes.hasNext() && mightBeJavaType(currentNode)) {
356363

357364
JsonNode next = nodes.next();
358365

359366
if (next.isArray()) {
360-
flattenCollection(propertyPrefix, next.values().iterator(), resultMap);
367+
flattenCollection(propertyPrefix, next.values(), resultMap);
361368
}
362-
if (currentNode.asText().equals("java.util.Date")) {
363-
resultMap.put(propertyPrefix, next.asText());
369+
if (currentNode.asString().equals("java.util.Date")) {
370+
resultMap.put(propertyPrefix, next.asString());
364371
break;
365372
}
366373
if (next.isNumber()) {
367374
resultMap.put(propertyPrefix, next.numberValue());
368375
break;
369376
}
370-
if (next.isTextual()) {
371-
resultMap.put(propertyPrefix, next.textValue());
377+
if (next.isString()) {
378+
resultMap.put(propertyPrefix, next.stringValue());
372379
break;
373380
}
374381
if (next.isBoolean()) {
@@ -392,7 +399,7 @@ private void flattenElement(String propertyPrefix, Object source, Map<String, Ob
392399
} else {
393400

394401
switch (element.getNodeType()) {
395-
case STRING -> resultMap.put(propertyPrefix, element.textValue());
402+
case STRING -> resultMap.put(propertyPrefix, element.stringValue());
396403
case NUMBER -> resultMap.put(propertyPrefix, element.numberValue());
397404
case BOOLEAN -> resultMap.put(propertyPrefix, element.booleanValue());
398405
case BINARY -> {
@@ -410,7 +417,7 @@ private void flattenElement(String propertyPrefix, Object source, Map<String, Ob
410417

411418
private boolean mightBeJavaType(JsonNode node) {
412419

413-
String textValue = node.asText();
420+
String textValue = node.asString();
414421

415422
if (!SOURCE_VERSION_PRESENT) {
416423
return Arrays.asList("java.util.Date", "java.math.BigInteger", "java.math.BigDecimal").contains(textValue);
@@ -419,10 +426,11 @@ private boolean mightBeJavaType(JsonNode node) {
419426
return javax.lang.model.SourceVersion.isName(textValue);
420427
}
421428

422-
private void flattenCollection(String propertyPrefix, Iterator<JsonNode> list, Map<String, Object> resultMap) {
429+
private void flattenCollection(String propertyPrefix, Collection<JsonNode> list, Map<String, Object> resultMap) {
423430

424-
for (int counter = 0; list.hasNext(); counter++) {
425-
JsonNode element = list.next();
431+
Iterator<JsonNode> iterator = list.iterator();
432+
for (int counter = 0; iterator.hasNext(); counter++) {
433+
JsonNode element = iterator.next();
426434
flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap);
427435
}
428436
}
@@ -473,38 +481,96 @@ public Version version() {
473481
public void setupModule(SetupContext context) {
474482

475483
List<ValueSerializer<?>> valueSerializers = new ArrayList<>();
476-
valueSerializers.add(new JavaUtilDateSerializer(true, null));
484+
valueSerializers.add(new JavaUtilDateSerializer(true, null) {
485+
@Override
486+
public void serializeWithType(Date value, JsonGenerator g, SerializationContext ctxt, TypeSerializer typeSer)
487+
throws JacksonException {
488+
serialize(value, g, ctxt);
489+
}
490+
});
477491
valueSerializers.add(new UTCCalendarSerializer());
478492

479493
Serializers serializers = new SimpleSerializers(valueSerializers);
480494
context.addSerializers(serializers);
481495

482496
Map<Class<?>, ValueDeserializer<?>> valueDeserializers = new LinkedHashMap<>();
483-
valueDeserializers.put(GregorianCalendar.class, new UTCCalendarDeserializer());
497+
valueDeserializers.put(java.util.Calendar.class,
498+
new UntypedFallbackDeserializer<>(new UntypedUTCCalendarDeserializer()));
499+
valueDeserializers.put(java.util.Date.class, new UntypedFallbackDeserializer<>(new JavaUtilDateDeserializer()));
500+
valueDeserializers.put(BigInteger.class, new UntypedFallbackDeserializer<>(new BigIntegerDeserializer()));
501+
valueDeserializers.put(BigDecimal.class, new UntypedFallbackDeserializer<>(new BigDecimalDeserializer()));
484502

485503
context.addDeserializers(new SimpleDeserializers(valueDeserializers));
486504
}
505+
506+
}
507+
508+
static class UntypedFallbackDeserializer<T> extends StdDeserializer<T> {
509+
510+
private final StdDeserializer<?> delegate;
511+
512+
protected UntypedFallbackDeserializer(StdDeserializer<?> delegate) {
513+
super(Object.class);
514+
this.delegate = delegate;
515+
}
516+
517+
@Override
518+
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
519+
throws JacksonException {
520+
521+
if (!(typeDeserializer instanceof AsPropertyTypeDeserializer asPropertySerializer)) {
522+
return super.deserializeWithType(p, ctxt, typeDeserializer);
523+
}
524+
525+
try {
526+
return super.deserializeWithType(p, ctxt, typeDeserializer);
527+
} catch (MismatchedInputException e) {
528+
if (!asPropertySerializer.baseType().isTypeOrSuperTypeOf(delegate.handledType())) {
529+
throw e;
530+
}
531+
}
532+
533+
return deserialize(p, ctxt);
534+
535+
}
536+
537+
@Override
538+
@SuppressWarnings("unchecked")
539+
public T deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
540+
return (T) delegate.deserialize(p, ctxt);
541+
}
487542
}
488543

489544
static class UTCCalendarSerializer extends JavaUtilCalendarSerializer {
490545

546+
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
547+
491548
@Override
492549
public void serialize(Calendar value, JsonGenerator g, SerializationContext provider) throws JacksonException {
493550

494551
Calendar utc = Calendar.getInstance();
495552
utc.setTimeInMillis(value.getTimeInMillis());
496-
utc.setTimeZone(TimeZone.getTimeZone("UTC"));
553+
utc.setTimeZone(UTC);
497554
super.serialize(utc, g, provider);
498555
}
556+
557+
@Override
558+
public void serializeWithType(Calendar value, JsonGenerator g, SerializationContext ctxt, TypeSerializer typeSer)
559+
throws JacksonException {
560+
serialize(value, g, ctxt);
561+
}
499562
}
500563

501-
static class UTCCalendarDeserializer extends JavaUtilCalendarDeserializer {
564+
static class UntypedUTCCalendarDeserializer extends JavaUtilCalendarDeserializer {
565+
566+
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
567+
502568
@Override
503569
public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
504570

505571
Calendar cal = super.deserialize(p, ctxt);
506572

507-
Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
573+
Calendar utc = Calendar.getInstance(UTC);
508574
utc.setTimeInMillis(cal.getTimeInMillis());
509575
utc.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
510576

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 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.mapping;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Map;
21+
22+
import org.junit.jupiter.api.Disabled;
23+
import org.springframework.data.redis.hash.Jackson2HashMapper;
24+
import org.springframework.data.redis.hash.Jackson3HashMapper;
25+
26+
/**
27+
* @author Christoph Strobl
28+
*/
29+
public class Jackson3CompatibilityTests extends Jackson3HashMapperUnitTests {
30+
31+
private final Jackson2HashMapper jackson2HashMapper;
32+
33+
public Jackson3CompatibilityTests() {
34+
super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, false));
35+
this.jackson2HashMapper = new Jackson2HashMapper(false);
36+
}
37+
38+
@Override
39+
@Disabled("with jackson 2 this used to render the timestamp as string. Now its a long and in line with calendar timestamp")
40+
void dateValueShouldBeTreatedCorrectly() {
41+
super.dateValueShouldBeTreatedCorrectly();
42+
}
43+
44+
@Override
45+
@Disabled("with jackson 2 used to render the enum and its type hint in an array. Now its just the enum value")
46+
void enumsShouldBeTreatedCorrectly() {
47+
super.enumsShouldBeTreatedCorrectly();
48+
}
49+
50+
@Override
51+
protected void assertBackAndForwardMapping(Object o) {
52+
53+
Map<String, Object> hash3 = getMapper().toHash(o);
54+
Map<String, Object> hash2 = jackson2HashMapper.toHash(o);
55+
56+
assertThat(hash3).containsAllEntriesOf(hash2);
57+
assertThat(getMapper().fromHash(hash2)).isEqualTo(o);
58+
}
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 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.mapping;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.util.Map;
21+
22+
import org.junit.jupiter.api.Disabled;
23+
import org.springframework.data.redis.hash.Jackson2HashMapper;
24+
import org.springframework.data.redis.hash.Jackson3HashMapper;
25+
26+
/**
27+
* @author Christoph Strobl
28+
*/
29+
public class Jackson3FlatteningCompatibilityTests extends Jackson3HashMapperUnitTests {
30+
31+
private final Jackson2HashMapper jackson2HashMapper;
32+
33+
public Jackson3FlatteningCompatibilityTests() {
34+
super(new Jackson3HashMapper(Jackson3HashMapper::preconfigure, true));
35+
this.jackson2HashMapper = new Jackson2HashMapper(true);
36+
}
37+
38+
@Override
39+
@Disabled("with jackson 2 this used to render the timestamp as string. Now its a long and in line with calendar timestamp")
40+
void dateValueShouldBeTreatedCorrectly() {
41+
super.dateValueShouldBeTreatedCorrectly();
42+
}
43+
44+
@Override
45+
@Disabled("with jackson 2 used to render the enum and its type hint in an array. Now its just the enum value")
46+
void enumsShouldBeTreatedCorrectly() {
47+
super.enumsShouldBeTreatedCorrectly();
48+
}
49+
50+
@Override
51+
protected void assertBackAndForwardMapping(Object o) {
52+
53+
Map<String, Object> hash3 = getMapper().toHash(o);
54+
Map<String, Object> hash2 = jackson2HashMapper.toHash(o);
55+
56+
assertThat(hash3).containsAllEntriesOf(hash2);
57+
assertThat(getMapper().fromHash(hash2)).isEqualTo(o);
58+
}
59+
}

0 commit comments

Comments
 (0)