DATAREDIS-1001 - Polishing.

Use untyped serializers/serializers for Calendar and Date. Serialize JSR-310 types to String.

Original pull request: #467.
This commit is contained in:
Mark Paluch
2019-07-04 14:43:39 +02:00
parent bc4cce7946
commit f76ff916c8
5 changed files with 209 additions and 89 deletions

View File

@@ -117,6 +117,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>

View File

@@ -430,6 +430,8 @@ public class Person {
String firstname;
String lastname;
Address address;
Date date;
LocalDateTime localDateTime;
}
public class Address {
@@ -454,6 +456,12 @@ The following table shows how the data in the preceding class would appear in no
|address
|`{ "city" : "Castle Black", "country" : "The North" }`
|date
|`1561543964015`
|localDateTime
|`2018-01-02T12:13:14`
|====
The following table shows how the data in the preceding class would appear in flat mapping:
@@ -475,10 +483,18 @@ The following table shows how the data in the preceding class would appear in fl
|address.country
|`The North`
|date
|`1561543964015`
|localDateTime
|`2018-01-02T12:13:14`
|====
NOTE: Flattening requires all property names to not interfere with the JSON path. Using dots or brackets in map keys or as property names is not supported when you use flattening. The resulting hash cannot be mapped back into an Object.
NOTE: `java.util.Date` and `java.util.Calendar` are represented with milliseconds. JSR-310 Date/Time types are serialized to their `toString` form if `jackson-datatype-jsr310` is on the class path.
:leveloffset: 2
include::{referenceDir}/redis-messaging.adoc[]

View File

@@ -18,6 +18,7 @@ package org.springframework.data.redis.hash;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
@@ -36,99 +37,105 @@ import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.ser.std.CalendarSerializer;
import com.fasterxml.jackson.databind.ser.std.DateSerializer;
/**
* {@link ObjectMapper} based {@link HashMapper} implementation that allows flattening. Given an entity {@code Person}
* with an {@code Address} like below the flattening will create individual hash entries for all nested properties and
* resolve complex types into simple types, as far as possible.
* <p>
* <p/>
* Flattening requires all property names to not interfere with JSON paths. Using dots or brackets in map keys or as
* property names is not supported using flattening. The resulting hash cannot be mapped back into an Object.
* <strong>Example</strong>
* <p/>
* <h3>Example</h3>
*
* <pre>
* <code>
* <pre class="code">
* class Person {
* String firstname;
* String lastname;
* Address address;
* String firstname;
* String lastname;
* Address address;
* Date date;
* LocalDateTime localDateTime;
* }
*
* class Address {
* String city;
* String country;
* String city;
* String country;
* }
* </code>
* </pre>
*
* <strong>Normal</strong>
* <h3>Normal</h3>
* <table>
* <tr>
* <th>Hash field</th>
* <th>Value
* <th>
* <th>Value</th>
* </tr>
* <tr>
* <td>firstname</td>
* <td>Jon
* <td>
* <td>Jon</td>
* </tr>
* <tr>
* <td>lastname</td>
* <td>Snow
* <td>
* <td>Snow</td>
* </tr>
* <tr>
* <td>address</td>
* <td>{ "city" : "Castle Black", "country" : "The North" }
* <td>
* <td>{ "city" : "Castle Black", "country" : "The North" }</td>
* </tr>
* <tr>
* <td>date</td>
* <td>1561543964015</td>
* </tr>
* <tr>
* <td>localDateTime</td>
* <td>2018-01-02T12:13:14</td>
* </tr>
* </table>
* <br />
* <strong>Flat</strong>:
* <h3>Flat</h3>
* <table>
* <tr>
* <th>Hash field</th>
* <th>Value
* <th>
* <th>Value</th>
* </tr>
*  
* <tr>
* <td>firstname</td>
* <td>Jon
* <td>
* <td>Jon</td>
* </tr>
* <tr>
* <td>lastname</td>
* <td>Snow
* <td>
* <td>Snow</td>
* </tr>
* <tr>
* <td>address.city</td>
* <td>Castle Black
* <td>
* <td>Castle Black</td>
* </tr>
* <tr>
* <td>address.country</td>
* <td>The North
* <td>
* <td>The North</td>
* </tr>
* <tr>
* <td>date</td>
* <td>1561543964015</td>
* </tr>
* <tr>
* <td>localDateTime</td>
* <td>2018-01-02T12:13:14</td>
* </tr>
* </table>
*
@@ -138,6 +145,8 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
*/
public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
private final HashMapperModule HASH_MAPPER_MODULE = new HashMapperModule();
private final ObjectMapper typingMapper;
private final ObjectMapper untypedMapper;
private final boolean flatten;
@@ -149,56 +158,16 @@ public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
*/
public Jackson2HashMapper(boolean flatten) {
this(new ObjectMapper(), flatten);
this(new ObjectMapper().findAndRegisterModules(), flatten);
typingMapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
// Prevent splitting time types into arrays. E
typingMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
typingMapper.setSerializationInclusion(Include.NON_NULL);
typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
typingMapper.addHandler(new DeserializationProblemHandler() {
@Override
public JavaType handleMissingTypeId(DeserializationContext ctxt, JavaType baseType, TypeIdResolver idResolver,
String failureMsg) {
return TypeFactory.defaultInstance().constructSimpleType(java.util.Date.class, new JavaType[] {});
}
});
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass().equals(java.util.Date.class)) {
return new JsonDeserializer<Object>() {
JsonDeserializer<?> delegate = new UntypedObjectDeserializer(null, null);
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Object val = delegate.deserialize(p, ctxt);
if (val instanceof Date) {
return val;
}
try {
return ctxt.getConfig().getDateFormat().parse(val.toString());
} catch (ParseException e) {
e.printStackTrace();
return new Date(NumberUtils.parseNumber(val.toString(), Long.class));
}
}
};
}
return deserializer;
}
});
typingMapper.registerModule(module);
typingMapper.registerModule(HASH_MAPPER_MODULE);
}
/**
@@ -210,11 +179,11 @@ public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
public Jackson2HashMapper(ObjectMapper mapper, boolean flatten) {
Assert.notNull(mapper, "Mapper must not be null!");
this.typingMapper = mapper;
this.flatten = flatten;
this.untypedMapper = new ObjectMapper();
untypedMapper.findAndRegisterModules();
this.untypedMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
this.untypedMapper.setSerializationInclusion(Include.NON_NULL);
}
@@ -377,7 +346,7 @@ public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
@SuppressWarnings("unchecked")
private void appendValueToTypedList(String key, Object value, List<Object> destination) {
int index = Integer.valueOf(key.substring(key.indexOf('[') + 1, key.length() - 1));
int index = Integer.parseInt(key.substring(key.indexOf('[') + 1, key.length() - 1));
List<Object> resultList = ((List<Object>) destination.get(1));
if (resultList.size() < index) {
resultList.add(value);
@@ -395,4 +364,121 @@ public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
listWithTypeHint.add(values);
return listWithTypeHint;
}
private static class HashMapperModule extends SimpleModule {
HashMapperModule() {
addSerializer(java.util.Date.class, new UntypedSerializer<>(new DateToTimestampSerializer()));
addSerializer(java.util.Calendar.class, new UntypedSerializer<>(new CalendarToTimestampSerializer()));
addDeserializer(java.util.Date.class, new UntypedDateDeserializer());
addDeserializer(java.util.Calendar.class, new UntypedCalendarDeserializer());
}
}
/**
* {@link JsonDeserializer} for {@link Date} objects without considering type hints.
*/
private static class UntypedDateDeserializer extends JsonDeserializer<Date> {
private final JsonDeserializer<?> delegate = new UntypedObjectDeserializer(null, null);
@Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
return deserialize(p, ctxt);
}
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Object val = delegate.deserialize(p, ctxt);
if (val instanceof Date) {
return (Date) val;
}
try {
return ctxt.getConfig().getDateFormat().parse(val.toString());
} catch (ParseException e) {
return new Date(NumberUtils.parseNumber(val.toString(), Long.class));
}
}
}
/**
* {@link JsonDeserializer} for {@link Calendar} objects without considering type hints.
*/
private static class UntypedCalendarDeserializer extends JsonDeserializer<Calendar> {
private final UntypedDateDeserializer dateDeserializer = new UntypedDateDeserializer();
@Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
return deserialize(p, ctxt);
}
@Override
public Calendar deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Date date = dateDeserializer.deserialize(p, ctxt);
if (date == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return calendar;
}
}
/**
* Untyped {@link JsonSerializer} to serialize plain values without writing JSON type hints.
*
* @param <T>
*/
private static class UntypedSerializer<T> extends JsonSerializer<T> {
private final JsonSerializer<T> delegate;
UntypedSerializer(JsonSerializer<T> delegate) {
this.delegate = delegate;
}
@Override
public void serializeWithType(T value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer)
throws IOException {
serialize(value, gen, serializers);
}
@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
serializers.defaultSerializeNull(gen);
} else {
delegate.serialize(value, gen, serializers);
}
}
}
private static class DateToTimestampSerializer extends DateSerializer {
// Prevent splitting to array.
@Override
protected boolean _asTimestamp(SerializerProvider serializers) {
return true;
}
}
private static class CalendarToTimestampSerializer extends CalendarSerializer {
// Prevent splitting to array.
@Override
protected boolean _asTimestamp(SerializerProvider serializers) {
return true;
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.mapping;
import static org.hamcrest.core.IsEqual.*;
import static org.junit.Assert.*;
import java.util.Map;
@@ -42,7 +43,7 @@ public abstract class AbstractHashMapperTest {
HashMapper mapper = mapperFor(o.getClass());
Map hash = mapper.toHash(o);
assertThat(mapper.fromHash(hash), IsEqual.equalTo(o));
assertThat(mapper.fromHash(hash), equalTo(o));
}
@Test

View File

@@ -17,8 +17,11 @@ package org.springframework.data.redis.mapping;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
@@ -38,6 +41,7 @@ import org.springframework.data.redis.hash.Jackson2HashMapper;
* Unit tests for {@link Jackson2HashMapper}.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(Parameterized.class)
public class Jackson2HashMapperUnitTests extends AbstractHashMapperTest {
@@ -165,9 +169,12 @@ public class Jackson2HashMapperUnitTests extends AbstractHashMapperTest {
@Test // DATAREDIS-1001
public void dateValueShouldBeTreatedCorrectly() {
WithDate source = new WithDate();
WithDates source = new WithDates();
source.string = "id-1";
source.date = new Date(1561543964015L);
source.calendar = Calendar.getInstance();
source.localDate = LocalDate.parse("2018-01-02");
source.localDateTime = LocalDateTime.parse("2018-01-02T12:13:14");
assertBackAndForwardMapping(source);
}
@@ -187,9 +194,12 @@ public class Jackson2HashMapperUnitTests extends AbstractHashMapperTest {
}
@Data
static class WithDate {
static class WithDates {
private String string;
private Date date;
private Calendar calendar;
private LocalDate localDate;
private LocalDateTime localDateTime;
}
}