All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 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 getProperty(String propertyName) {
        Transform transform = this;
        for (int i=0; null != transform; i++, transform = transform.extend) {
            Property 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);
    }
}