com.distelli.jackson.transform.Transform Maven / Gradle / Ivy
package com.distelli.jackson.transform;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Array;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.function.Function;
import java.util.Set;
import java.util.HashSet;
/**
* Define custom serialize/deserialize of an object based on the
* properties of that object.
*
* Usage:
*
* {@code
* ObjectMapper om = new ObjectMapper();
* TransformModule module = new TransformModule();
* module.createTransform(Address.class)
* put("hk", new TypeReference(){},
* (addr) -> addr.getDomain(),
* (addr, hk) -> addr.withDomain(hk));
* put("rk", new TypeReference(){},
* (addr) -> addr.getAddressId(),
* (addr, rk) -> addr.withAddressId(rk));
* put("addr", new TypeReference(){},
* (addr) -> addr.getAddress(),
* (addr, addrStr) -> addr.withAddress(addrStr));
* om.registerModule(module);
* }
*
* ...now use om.convertValue(thing, Address.class)
* to convert a thing that has "hk", "rk", and "addr" fields into
* an Address object.
*
* ...or use om.convertValue(address, Thing.class)
* to convert an Address
object into a Thing
* instance that has "hk", "rk", and "addr" fields.
*/
public class Transform {
private Type type;
private Class rawType;
private Type builderType;
private Class rawBuilderType;
private TransformModule module;
private Map> properties;
private NewInstance constructor = null;
private Function build = null;
private Transform super T, ? super B> extend;
protected Transform(Type type, Type builderType, TransformModule module) {
this.type = type;
this.builderType = builderType;
this.module = module;
rawType = getRawType(type);
rawBuilderType = getRawType(builderType);
properties = new LinkedHashMap>();
module.addSerializer(rawType, new CustomSerializer());
module.addDeserializer(rawType, new CustomDeserializer());
}
public Transform constructor(NewInstance constructor) {
this.constructor = constructor;
return this;
}
public Transform build(Function build) {
this.build = build;
return this;
}
/**
* Inherit property transforms from a super type transform.
*
* @param extend is the transformer to extend this transform from.
* @return this
*/
public Transform extend(Transform super T, ? super B> extend) {
if ( ! extend.rawType.isAssignableFrom(rawType) ) {
throw new IllegalArgumentException(extend.type + " is not a superclass of " + type);
}
if ( ! extend.rawBuilderType.isAssignableFrom(rawBuilderType) ) {
throw new IllegalArgumentException(extend.builderType + " is not a superclass of " + builderType);
}
this.extend = extend;
return this;
}
public Transform put(String propertyName, Class propertyType) {
return put(propertyName, propertyType, propertyName);
}
public Transform put(String propertyName, TypeReference propertyType) {
return put(propertyName, propertyType, propertyName);
}
public Transform put(String propertyName, Class propertyType, String fromPropertyName) {
return put(propertyName, new ClassTypeReference(propertyType), fromPropertyName);
}
public Transform put(String propertyName, TypeReference propertyType, String fromPropertyName) {
Class rawPropertyType = getRawType(propertyType.getType());
return put(propertyName, propertyType,
getToProperty(fromPropertyName, rawPropertyType),
getFromProperty(fromPropertyName, rawPropertyType));
}
public Transform put(String propertyName, Class propertyType, ToProperty toProperty) {
return put(propertyName, propertyType, toProperty, null);
}
public Transform put(String propertyName, TypeReference propertyType, ToProperty toProperty) {
return put(propertyName, propertyType, toProperty, null);
}
public Transform put(String propertyName, Class propertyType, ToProperty toProperty, FromProperty fromProperty) {
return put(propertyName, new ClassTypeReference(propertyType), toProperty, fromProperty);
}
public Transform put(String propertyName, TypeReference propertyType, ToProperty toProperty, FromProperty fromProperty) {
properties.put(propertyName, new Property(toProperty, fromProperty, propertyType));
return this;
}
public B newInstance(TreeNode tree, ObjectCodec codec) throws Exception {
if ( null == constructor ) {
return (B)rawBuilderType.newInstance();
}
return constructor.newInstance(tree, new WrappedObjectCodec(codec));
}
public T buildInstance(B builder) throws Exception {
// Nothing to build?
if ( type.equals(builderType) ) return (T)builder;
if ( null == build ) {
Method buildMethod = rawBuilderType.getMethod("build");
return (T)buildMethod.invoke(builder);
}
return build.apply(builder);
}
private void writeProperties(T value, JsonGenerator gen, Set visited) throws IOException {
for ( Map.Entry> entry : properties.entrySet() ) {
if ( ! entry.getValue().hasToProperty() ) continue;
if ( null != visited ) visited.add(entry.getKey());
Object propertyValue = entry.getValue().toProperty(value);
if ( null == propertyValue ) continue;
gen.writeObjectField(entry.getKey(), propertyValue);
}
}
private void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
if ( null == value ) {
gen.writeNull();
return;
}
Transform delegate = module.getTransform(value.getClass());
if ( null != delegate && this != delegate ) {
delegate.serialize(value, gen, serializers);
return;
}
gen.writeStartObject();
Set visited = null;
if ( null != extend ) visited = new HashSet<>();
Transform transform = Transform.this;
for (int i=0; null != transform; i++, transform = transform.extend ) {
transform.writeProperties(value, gen, visited);
if ( i > 100 ) {
throw new IllegalStateException("Recursive extend in transform of type "+type+"?");
}
}
gen.writeEndObject();
}
private class CustomSerializer extends JsonSerializer {
@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
Transform.this.serialize(value, gen, serializers);
}
}
private class CustomDeserializer extends JsonDeserializer {
public T deserialize(JsonParser parse, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if ( ! parse.hasCurrentToken() ) parse.nextToken();
if ( JsonToken.VALUE_NULL == parse.getCurrentToken() ) {
return null;
}
B builder = null;
TreeNode treeNode = ( null != constructor ) ? parse.readValueAsTree() : null;
try {
builder = newInstance(treeNode, parse.getCodec());
} catch ( Exception ex ) {
throw ctxt.instantiationException(rawBuilderType, ex);
}
if ( null == builder ) return null;
Transform transform = null;
if ( builderType.equals(type) ) {
transform = module.getTransform(builder.getClass());
}
if ( null == transform ) transform = Transform.this;
if ( null != treeNode ) {
parse = new TreeTraversingParser((JsonNode)treeNode, parse.getCodec());
parse.nextToken();
}
if ( ! parse.isExpectedStartObjectToken() ) {
throw ctxt.wrongTokenException(parse, parse.getCurrentToken(), "Expected JSON object");
}
parse.nextToken();
if ( parse.hasTokenId(JsonTokenId.ID_FIELD_NAME) ) {
String propertyName = parse.getCurrentName();
do {
parse.nextToken();
Property property = transform.getProperty(propertyName);
if ( null != property ) {
property.fromProperty(builder, parse.readValueAs(property.getType()));
} else {
parse.skipChildren();
}
} while ( (propertyName = parse.nextFieldName()) != null );
}
try {
return buildInstance(builder);
} catch ( Exception ex ) {
throw ctxt.instantiationException(rawType, ex);
}
}
}
private Property super T, Object, ? super B> getProperty(String propertyName) {
Transform super T, ? super B> transform = this;
for (int i=0; null != transform; i++, transform = transform.extend) {
Property super T, Object, ? super B> prop = transform.properties.get(propertyName);
if ( null != prop ) return prop;
if ( i > 100 ) {
throw new IllegalStateException("Recursive extend in transform of type "+type+"?");
}
}
return null;
}
private ToProperty getToProperty(String fromPropertyName, Class rawPropertyType) {
String fieldCause;
try {
Field field = rawType.getDeclaredField(fromPropertyName);
if ( isAssignableFrom(field.getType(), rawPropertyType) ) {
field.setAccessible(true);
return (obj) -> {
try {
return (U)field.get(obj);
} catch ( RuntimeException ex ) {
throw ex;
} catch ( Exception ex ) {
throw new RuntimeException(ex);
}
};
} else {
fieldCause = "Type of field '"+fromPropertyName+"' is not assignable from '"+
rawPropertyType.getSimpleName()+"'";
}
} catch ( Exception ex ) {
fieldCause = String.format("%s: %s", ex.getClass().getSimpleName(), ex.getMessage());
}
String getterName = "get"+ucfirst(fromPropertyName);
String methodCause;
try {
Method getter = rawType.getMethod(getterName);
if ( isAssignableFrom(getter.getReturnType(), rawPropertyType) ) {
getter.setAccessible(true);
return (obj) -> {
try {
return (U)getter.invoke(obj);
} catch ( RuntimeException ex ) {
throw ex;
} catch ( Exception ex ) {
throw new RuntimeException(ex);
}
};
} else {
methodCause = "Return type of '"+getterName+"' is not assignable from '"+
rawPropertyType.getSimpleName()+"'";
}
} catch ( Exception ex ) {
methodCause = String.format("%s: %s", ex.getClass().getSimpleName(), ex.getMessage());
}
throw new NoSuchPropertyException(
"Unable to find property named '"+fromPropertyName+
"' in class '" + rawType.getTypeName()+ "': ["+fieldCause+"] ["+methodCause+"]");
}
private FromProperty getFromProperty(String fromPropertyName, Class rawPropertyType) {
String fieldCause;
try {
Field field = rawBuilderType.getDeclaredField(fromPropertyName);
if ( isAssignableFrom(field.getType(), rawPropertyType) ) {
field.setAccessible(true);
return (builder, val) -> {
try {
field.set(builder, val);
} catch ( RuntimeException ex ) {
throw ex;
} catch ( Exception ex ) {
throw new RuntimeException(ex);
}
};
} else {
fieldCause = "Type of field '"+fromPropertyName+"' is not assignable from '"+
rawPropertyType.getSimpleName()+"'";
}
} catch ( Exception ex ) {
fieldCause = String.format("%s: %s", ex.getClass().getSimpleName(), ex.getMessage());
}
String setterName = "set"+ucfirst(fromPropertyName);
String methodCause;
try {
Method setter = null;
try {
setter = rawBuilderType.getMethod(setterName, rawPropertyType);
} catch ( NoSuchMethodException ex ) {
if ( ! rawBuilderType.equals(rawType) ) {
setter = rawBuilderType.getMethod(fromPropertyName, rawPropertyType);
} else {
throw ex;
}
}
setter.setAccessible(true);
Method finalSetter = setter;
return (builder, val) -> {
try {
finalSetter.invoke(builder, val);
} catch ( ReflectiveOperationException ex ) {
throw new RuntimeException(ex);
}
};
} catch ( Exception ex ) {
methodCause = String.format("%s: %s", ex.getClass().getSimpleName(), ex.getMessage());
}
throw new NoSuchPropertyException(
"Unable to find property named '"+fromPropertyName+
"' in class '" + rawBuilderType.getTypeName()+ "': ["+fieldCause+"] ["+methodCause+"]");
}
private static String ucfirst(String str) {
if ( null == str || str.length() < 1 ) return str;
return str.substring(0,1).toUpperCase() + str.substring(1);
}
private static Class> getRawType(Type type) {
if ( type instanceof Class ) {
return (Class>)type;
}
if ( type instanceof ParameterizedType ) {
return (Class>)((ParameterizedType)type).getRawType();
}
if ( type instanceof GenericArrayType ) {
Class> rawType = getRawType(((GenericArrayType)type).getGenericComponentType());
return Array.newInstance(rawType, 0).getClass();
}
throw new IllegalArgumentException("Unexpected type="+type+" can not be converted to raw Class type.");
}
private static boolean isAssignableFrom(Class type, Class from) {
if ( type.isAssignableFrom(from) ) return true;
if ( ! type.isPrimitive() ) return false;
return TO_WRAPPER.get(type).isAssignableFrom(from);
}
public final static Map, Class>> TO_WRAPPER = new HashMap, Class>>();
static {
TO_WRAPPER.put(boolean.class, Boolean.class);
TO_WRAPPER.put(byte.class, Byte.class);
TO_WRAPPER.put(short.class, Short.class);
TO_WRAPPER.put(char.class, Character.class);
TO_WRAPPER.put(int.class, Integer.class);
TO_WRAPPER.put(long.class, Long.class);
TO_WRAPPER.put(float.class, Float.class);
TO_WRAPPER.put(double.class, Double.class);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy