@@ -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.BeanDes erializerModifi er ;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler ;
import com.fasterxml.jackson.databind.S erializerProvid er ;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer ;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolv er ;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializ er ;
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 ;
}
}
}