com.nginious.http.serialize.JsonSerializerCreator.java Source code

Java tutorial

Introduction

Here is the source code for com.nginious.http.serialize.JsonSerializerCreator.java

Source

/**
 * Copyright 2012 NetDigital Sweden AB
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

package com.nginious.http.serialize;

import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.nginious.http.annotation.Serializable;
import com.nginious.http.application.ApplicationClassLoader;

/**
 * <p>
 * Creates serializers for serializing beans to JSON format. The serializer class is created runtime
 * by building the necessary bytecode for the class. The created class is a subclass of {@link JsonSerializer}
 * and overrides the method {@link JsonSerializer#serializeProperties(org.json.JSONObject, Object)}.
 * </p>
 * 
 * <p>
 * The following outlines the steps used for creating a serializer class
 * <ul>
 * <li>A subclass of {@link JsonSerializer} is created by generating the appropriate bytecode.</li>
 * <li>The serializer class name is the same as the bean class with "JsonSerializer" appended.</li>
 * <li>The serializer class is placed in the same package as the bean class.</li>
 * <li>The bean class is introspected searching for matching get and set property methods.</li>
 * <li>Bean set methods can be annotated with {@link Serializable}.</li>
 * <li>For each found property the appropriate bytecode is generated for calling each bean get methd and serializing the property.</li>
 * <li>The creator generates bytecode which calls methods in {@link JsonSerializer} to serialize individual properties. See list below for supported types.</li>
 * </ul>
 * </p>
 * 
 * <p>
 * A bean is regarded as JSON serializable if it is annotated with the {@link Serializable} annotation type where the
 * serializable property is set to <code>true</code> and types list includes the text "json".
 * </p>
 * 
 * <p>
 * A bean property is regarded as JSON serializable if it's not annotated with the {@link Serializable} annotation or if it's
 * annotated with the {@link Serializable} annotation type where the serializable property is set to <code>true</code>
 * and types list includes the text "json".
 * </p>
 * 
 * <p>
 * The following property types are supported
 * <ul>
 * <li>boolean - serialized by {@link JsonSerializer#serializeBoolean(org.json.JSONObject, String, boolean)}</li>
 * <li>double - serialized by {@link JsonSerializer#serializeDouble(org.json.JSONObject, String, double)}</li>
 * <li>float - serialized by {@link JsonSerializer#serializeFloat(org.json.JSONObject, String, float)}</li>
 * <li>int - serialized by {@link JsonSerializer#serializeInt(org.json.JSONObject, String, int)}</li>
 * <li>long - serialized by {@link JsonSerializer#serializeLong(org.json.JSONObject, String, long)}</li>
 * <li>short - serialized by {@link JsonSerializer#serializeShort(org.json.JSONObject, String, short)}</li>
 * <li>java.util.Calendar - serialized by {@link JsonSerializer#serializeCalendar(org.json.JSONObject, String, Calendar)}</li>
 * <li>java.util.Date - serialized by {@link JsonSerializer#serializeDate(org.json.JSONObject, String, Date)}</li>
 * <li>java.lang.String - serialized by {@link JsonSerializer#serializeString(org.json.JSONObject, String, String)}</li>
 * <li>java.lang.Object - serialized by {@link JsonSerializer#serializeObject(org.json.JSONObject, String, Object)}</li>
 * 
 * </ul>
 * 
 * In addition to the above types, if a property is a serializable bean as defined above the 
 * {@link JsonSerializerCreator#create(Class)} method is called recursively to create a serializer for the property. 
 * The created serializer is then used for serializing the property.
 * </p>
 * 
 * @author Bojan Pisler, NetDigital Sweden AB
 *
 */
class JsonSerializerCreator extends SerializerCreator<JsonSerializer<?>> {

    private ApplicationClassLoader classLoader;

    /**
     * Constructs a new JSON serializer creator with the specified class loader.
     * 
     * @param classLoader the class loader to use for loaindg created serializer classes
     */
    JsonSerializerCreator(ApplicationClassLoader classLoader) {
        super();
        this.classLoader = classLoader;
    }

    /**
     * Creates a JSON serializer for the specified bean class unless a serializer has already been
     * created. Created serializers are cached and returned on subsequent calls to this method.
     * 
     * @param factory serializer factory
     * @param <T> class type for bean
     * @param beanClazz bean class for which a serializer should be created
     * @return the created serializer
     * @throws SerializerFactoryException if unable to create serializer or class is not a bean
     */
    @SuppressWarnings("unchecked")
    <T> JsonSerializer<T> create(SerializerFactoryImpl factory, Class<T> beanClazz)
            throws SerializerFactoryException {
        JsonSerializer<T> serializer = (JsonSerializer<T>) serializers.get(beanClazz);

        if (serializer != null) {
            return serializer;
        }

        try {
            synchronized (this) {
                serializer = (JsonSerializer<T>) serializers.get(beanClazz);

                if (serializer != null) {
                    return serializer;
                }

                checkSerializability(beanClazz, "json");
                String intBeanClazzName = Serialization.createInternalClassName(beanClazz);
                Method[] methods = beanClazz.getMethods();

                String intSerializerClazzName = new StringBuffer(intBeanClazzName).append("JsonSerializer")
                        .toString();

                // Create class
                ClassWriter writer = new ClassWriter(0);
                String signature = Serialization.createClassSignature("com/nginious/http/serialize/JsonSerializer",
                        intBeanClazzName);
                writer.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, intSerializerClazzName, signature,
                        "com/nginious/http/serialize/JsonSerializer", null);

                // Create constructor
                Serialization.createConstructor(writer, "com/nginious/http/serialize/JsonSerializer");

                // Create serialize method
                MethodVisitor visitor = createSerializeMethod(writer, intBeanClazzName);

                for (Method method : methods) {
                    Serializable info = method.getAnnotation(Serializable.class);
                    boolean canSerialize = info == null
                            || (info != null && info.serialize() && info.types().indexOf("json") > -1);

                    if (canSerialize && method.getName().startsWith("get") && !method.getName().equals("getClass")
                            && method.getReturnType() != null && method.getParameterTypes().length == 0) {
                        Class<?> returnType = method.getReturnType();
                        String propertyName = getPropertyName(method);

                        if (returnType.isPrimitive()) {
                            if (returnType.equals(boolean.class)) {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeBoolean",
                                        "Z", "Z", intBeanClazzName, method.getName(), propertyName);
                            } else if (returnType.equals(double.class)) {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeDouble",
                                        "D", "D", intBeanClazzName, method.getName(), propertyName);
                            } else if (returnType.equals(float.class)) {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeFloat",
                                        "F", "F", intBeanClazzName, method.getName(), propertyName);
                            } else if (returnType.equals(int.class)) {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeInt",
                                        "I", "I", intBeanClazzName, method.getName(), propertyName);
                            } else if (returnType.equals(long.class)) {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeLong",
                                        "J", "J", intBeanClazzName, method.getName(), propertyName);
                            } else if (returnType.equals(short.class)) {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeShort",
                                        "S", "S", intBeanClazzName, method.getName(), propertyName);
                            }
                        } else if (Collection.class.isAssignableFrom(returnType)) {
                            Class<?> collectionType = canSerializeGenericCollectionType(method, "json");

                            if (collectionType != null) {
                                createBeanCollectionSerializationCode(visitor, intBeanClazzName, method.getName(),
                                        propertyName, returnType, collectionType);
                            } else {
                                createObjectCollectionSerializationCode(visitor, returnType, intBeanClazzName,
                                        method.getName(), propertyName);
                            }
                        } else if (returnType.equals(Calendar.class)) {
                            createPropertySerializationCode(visitor, intSerializerClazzName, "serializeCalendar",
                                    "Ljava/util/Calendar;", "Ljava/util/Calendar;", intBeanClazzName,
                                    method.getName(), propertyName);
                        } else if (returnType.equals(Date.class)) {
                            createPropertySerializationCode(visitor, intSerializerClazzName, "serializeDate",
                                    "Ljava/util/Date;", "Ljava/util/Date;", intBeanClazzName, method.getName(),
                                    propertyName);
                        } else if (returnType.equals(String.class)) {
                            createPropertySerializationCode(visitor, intSerializerClazzName, "serializeString",
                                    "Ljava/lang/String;", "Ljava/lang/String;", intBeanClazzName, method.getName(),
                                    propertyName);
                        } else {
                            info = returnType.getAnnotation(Serializable.class);
                            canSerialize = info != null && info.serialize() && info.types().indexOf("json") > -1;

                            if (canSerialize) {
                                createBeanSerializationCode(visitor, method.getName(), propertyName, returnType,
                                        intBeanClazzName);
                            } else {
                                createPropertySerializationCode(visitor, intSerializerClazzName, "serializeObject",
                                        "Ljava/lang/Object;", "L" + returnType.getName().replace('.', '/') + ";",
                                        intBeanClazzName, method.getName(), propertyName);
                            }
                        }
                    }
                }

                visitor.visitInsn(Opcodes.RETURN);
                visitor.visitMaxs(8, 7);
                visitor.visitEnd();

                writer.visitEnd();
                byte[] clazzBytes = writer.toByteArray();
                ClassLoader controllerLoader = null;

                if (classLoader.hasLoaded(beanClazz)) {
                    controllerLoader = beanClazz.getClassLoader();
                } else {
                    controllerLoader = this.classLoader;
                }

                Class<?> clazz = Serialization.loadClass(controllerLoader, intSerializerClazzName.replace('/', '.'),
                        clazzBytes);
                serializer = (JsonSerializer<T>) clazz.newInstance();
                String propertyName = Serialization.createPropertyNameFromClass(beanClazz);
                serializer.setName(propertyName);
                serializer.setType(beanClazz);
                serializer.setSerializerFactory(factory);
                serializers.put(beanClazz, serializer);
                return serializer;
            }
        } catch (IllegalAccessException e) {
            throw new SerializerFactoryException("Can't create JSON serializer for " + beanClazz.getName(), e);
        } catch (InstantiationException e) {
            throw new SerializerFactoryException("Can't create JSON serializer for " + beanClazz.getName(), e);
        }
    }

    /**
     * Returns name of property to be serialized from serializable annotation if available. Otherwise
     * the property name is generated from the method name.
     * 
     * @param method
     * @return
     * @throws SerializerFactoryException
     */
    private String getPropertyName(Method method) throws SerializerFactoryException {
        Serializable info = method.getAnnotation(Serializable.class);

        if (info != null && !info.name().equals("")) {
            return info.name();
        }

        return Serialization.createPropertyNameFromMethodName(method.getName());
    }

    /**
     * Creates bytecode for serializing a bean property which returns a collection of opaque objects.
     * 
     * @param visitor method visitor used for creating bytecode
     * @param returnType return type of get method in bean
     * @param intBeanClazzName binary name of bean
     * @param methodName binary name of get method in bean
     */
    private void createObjectCollectionSerializationCode(MethodVisitor visitor, Class<?> returnType,
            String intBeanClazzName, String methodName, String propertyName) {
        visitor.visitTypeInsn(Opcodes.NEW, "com/nginious/http/serialize/JsonObjectCollectionSerializer");
        visitor.visitInsn(Opcodes.DUP);
        visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/nginious/http/serialize/JsonObjectCollectionSerializer",
                "<init>", "()V");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        String intReturnClazzName = returnType.getName().replace('.', '/');
        visitor.visitVarInsn(Opcodes.ALOAD, 3);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, intBeanClazzName, methodName,
                "()L" + intReturnClazzName + ";");
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/JsonObjectCollectionSerializer",
                "serialize", "(Ljava/util/Collection;)Lorg/json/JSONArray;");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitVarInsn(Opcodes.ALOAD, 1);
        visitor.visitLdcInsn(propertyName);
        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "put",
                "(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;");
    }

    /**
     * Creates bytecode for serializing a bean property which returns a collection of beans that are serializable. Bean serializability
     * is determined as described in the class description.
     * 
     * @param visitor method visitor used for creating bytecode
     * @param intBeanClazzName binary class name of bean
     * @param methodName binary name of get method in bean returning collection
     * @param returnType return type of get method in bean
     * @param collectionBeanType class of serializable bean found in collection
     */
    private void createBeanCollectionSerializationCode(MethodVisitor visitor, String intBeanClazzName,
            String methodName, String propertyName, Class<?> returnType, Class<?> collectionBeanType) {
        visitor.visitVarInsn(Opcodes.ALOAD, 0);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/JsonSerializer",
                "getSerializerFactory", "()Lcom/nginious/http/serialize/SerializerFactoryImpl;");
        visitor.visitLdcInsn(collectionBeanType.getName());
        visitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Class", "forName",
                "(Ljava/lang/String;)Ljava/lang/Class;");
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/SerializerFactoryImpl",
                "createJsonSerializer", "(Ljava/lang/Class;)Lcom/nginious/http/serialize/JsonSerializer;");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitTypeInsn(Opcodes.NEW, "com/nginious/http/serialize/JsonBeanCollectionSerializer");
        visitor.visitInsn(Opcodes.DUP);
        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/nginious/http/serialize/JsonBeanCollectionSerializer",
                "<init>", "(Lcom/nginious/http/serialize/JsonSerializer;)V");
        visitor.visitVarInsn(Opcodes.ASTORE, 5);

        visitor.visitVarInsn(Opcodes.ALOAD, 5);
        String intReturnClazzName = returnType.getName().replace('.', '/');
        visitor.visitVarInsn(Opcodes.ALOAD, 3);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, intBeanClazzName, methodName,
                "()L" + intReturnClazzName + ";");
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/JsonBeanCollectionSerializer",
                "serialize", "(Ljava/util/Collection;)Lorg/json/JSONArray;");
        visitor.visitVarInsn(Opcodes.ASTORE, 5);

        visitor.visitVarInsn(Opcodes.ALOAD, 1);
        visitor.visitLdcInsn(propertyName);
        visitor.visitVarInsn(Opcodes.ALOAD, 5);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "put",
                "(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;");
    }

    /**
     * Creates bytecode for serializing a bean property which is in itself a serializable bean as defined in the class description.
     * 
     * @param visitor method visitor used for creating bytecode
     * @param returnMethodName binary name of get method in bean that returns serializable bean
     * @param returnType class of serializable bean
     * @param intBeanClazzName binary class name of bean
     */
    private void createBeanSerializationCode(MethodVisitor visitor, String returnMethodName, String propertyName,
            Class<?> returnType, String intBeanClazzName) {
        String intReturnClazzName = Serialization.createInternalClassName(returnType);
        visitor.visitVarInsn(Opcodes.ALOAD, 0);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/JsonSerializer",
                "getSerializerFactory", "()Lcom/nginious/http/serialize/SerializerFactoryImpl;");
        visitor.visitLdcInsn(returnType.getName());
        visitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Class", "forName",
                "(Ljava/lang/String;)Ljava/lang/Class;");

        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/SerializerFactoryImpl",
                "createJsonSerializer", "(Ljava/lang/Class;)Lcom/nginious/http/serialize/JsonSerializer;");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitVarInsn(Opcodes.ALOAD, 3);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, intBeanClazzName, returnMethodName,
                "()L" + intReturnClazzName + ";");
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/JsonSerializer", "serialize",
                "(Ljava/lang/Object;)Lorg/json/JSONObject;");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitVarInsn(Opcodes.ALOAD, 1);
        visitor.visitLdcInsn(propertyName);
        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/json/JSONObject", "put",
                "(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;");
    }

    /**
     * Creates bytecode for serializing property matching the specified bean method name. The generated bytecode calls the
     * appropriate serialization method in the class {@link JsonSerializer} depending on the specified method type.
     * 
     * @param visitor method visitor for generating bytecode
     * @param clazzName binary name of serializer class being generated
     * @param methodName binary name of method in class {@link JsonSerializer} used for serializing property
     * @param methodType binary type for method in class {@link JsonSerializer} used for serializing property
     * @param beanType binary return type of get method in bean
     * @param beanClazzName binary name of bean class
     * @param beanMethodName binary name of get method in bean for getting method
     */
    private void createPropertySerializationCode(MethodVisitor visitor, String clazzName, String methodName,
            String methodType, String beanType, String beanClazzName, String beanMethodName, String propertyName) {
        visitor.visitVarInsn(Opcodes.ALOAD, 0);
        visitor.visitVarInsn(Opcodes.ALOAD, 1);
        visitor.visitLdcInsn(propertyName);
        visitor.visitVarInsn(Opcodes.ALOAD, 3);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, beanClazzName, beanMethodName, "()" + beanType);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, clazzName, methodName,
                "(Lorg/json/JSONObject;Ljava/lang/String;" + methodType + ")V");
    }

    /**
     * Creates bytecode which implements the {@link JsonSerializer#serializeProperties(org.json.JSONObject, Object)}
     * method for the serializer class being created.
     * 
     * @param writer class byte code writer
     * @param intBeanClazzName binary name of serializer class being generated
     * @return a method visitor for writing bytecode inside the generated method
     */
    private MethodVisitor createSerializeMethod(ClassWriter writer, String intBeanClazzName) {
        String[] exceptions = { "com/nginious/serialize/SerializerException" };
        MethodVisitor visitor = writer.visitMethod(Opcodes.ACC_PUBLIC, "serializeProperties",
                "(Lorg/json/JSONObject;Ljava/lang/Object;)V", null, exceptions);
        visitor.visitCode();

        Label label = new Label();
        visitor.visitVarInsn(Opcodes.ALOAD, 2);
        visitor.visitJumpInsn(Opcodes.IFNONNULL, label);
        visitor.visitInsn(Opcodes.RETURN);
        visitor.visitLabel(label);
        visitor.visitVarInsn(Opcodes.ALOAD, 2);
        visitor.visitTypeInsn(Opcodes.CHECKCAST, intBeanClazzName);
        visitor.visitIntInsn(Opcodes.ASTORE, 3);

        return visitor;
    }
}