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

Java tutorial

Introduction

Here is the source code for com.nginious.http.serialize.XmlSerializerCreator.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 XML format. The serializer class is created runtime
 * by building the necessary bytecode for the class. The created class is a subclass of {@link XmlSerializer}
 * and overrides the method {@link XmlSerializer#serializeProperties(javax.xml.transform.sax.TransformerHandler, Object)}.
 * </p>
 * 
 * <p>
 * The following outlines the steps used for creating a serializer class
 * <ul>
 * <li>A subclass of {@link XmlSerializer} is created by generating the appropriate bytecode.</li>
 * <li>The serializer class name is the same as the bean class with "XmlSerializer" 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 XmlSerializer} to serialize individual properties. See list below for supported types.</li>
 * </ul>
 * </p>
 * 
 * <p>
 * A bean is regarded as XML 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 XML 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 "xml".
 * </p>
 * 
 * <p>
 * The following property types are supported
 * <ul>
 * <li>boolean - serialized by {@link XmlSerializer#serializeBoolean(javax.xml.transform.sax.TransformerHandler, String, boolean)}</li>
 * <li>double - serialized by {@link XmlSerializer#serializeDouble(javax.xml.transform.sax.TransformerHandler, String, double)}</li>
 * <li>float - serialized by {@link XmlSerializer#serializeFloat(javax.xml.transform.sax.TransformerHandler, String, float)}</li>
 * <li>int - serialized by {@link XmlSerializer#serializeInt(javax.xml.transform.sax.TransformerHandler, String, int)}</li>
 * <li>long - serialized by {@link XmlSerializer#serializeLong(javax.xml.transform.sax.TransformerHandler, String, long)}</li>
 * <li>short - serialized by {@link XmlSerializer#serializeShort(javax.xml.transform.sax.TransformerHandler, String, short)}</li>
 * <li>java.util.Calendar - serialized by {@link XmlSerializer#serializeCalendar(javax.xml.transform.sax.TransformerHandler, String, Calendar)}</li>
 * <li>java.util.Date - serialized by {@link XmlSerializer#serializeDate(javax.xml.transform.sax.TransformerHandler, String, Date)}</li>
 * <li>java.lang.String - serialized by {@link XmlSerializer#serializeString(javax.xml.transform.sax.TransformerHandler, String, String)}</li>
 * <li>java.lang.Object - serialized by {@link XmlSerializer#serializeObject(javax.xml.transform.sax.TransformerHandler, String, Object)}</li>
 * 
 * </ul>
 * 
 * In addition to the above types, if a property is a serializable bean as defined above the 
 * {@link XmlSerializerCreator#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 XmlSerializerCreator extends SerializerCreator<XmlSerializer<?>> {

    private ApplicationClassLoader classLoader;

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

    /**
     * Creates a XML 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> XmlSerializer<T> create(SerializerFactoryImpl factory, Class<T> beanClazz)
            throws SerializerFactoryException {
        XmlSerializer<T> serializer = (XmlSerializer<T>) serializers.get(beanClazz);

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

        try {
            synchronized (this) {
                checkSerializability(beanClazz, "xml");
                String intBeanClazzName = Serialization.createInternalClassName(beanClazz);
                Method[] methods = beanClazz.getMethods();

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

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

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

                // 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("xml") > -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, intBeanClazzName, method.getName(),
                                        propertyName, returnType);
                            }
                        } 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(7, 6);
                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 = (XmlSerializer<T>) clazz.newInstance();
                serializer.setName(Serialization.createPropertyNameFromClass(beanClazz));
                serializer.setType(beanClazz);
                serializer.setSerializerFactory(factory);
                serializers.put(beanClazz, serializer);
                return serializer;
            }
        } catch (IllegalAccessException e) {
            throw new SerializerFactoryException("Can't create XML serializer for " + beanClazz.getName(), e);
        } catch (InstantiationException e) {
            throw new SerializerFactoryException("Can't create XML serializer for " + beanClazz.getName(), e);
        }
    }

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

        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitVarInsn(Opcodes.ALOAD, 1);
        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/XmlObjectCollectionSerializer",
                "serialize", "(Ljavax/xml/transform/sax/TransformerHandler;Ljava/util/Collection;)V");
    }

    /**
     * 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/XmlSerializer",
                "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",
                "createXmlSerializer", "(Ljava/lang/Class;)Lcom/nginious/http/serialize/XmlSerializer;");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitTypeInsn(Opcodes.NEW, "com/nginious/http/serialize/XmlBeanCollectionSerializer");
        visitor.visitInsn(Opcodes.DUP);
        visitor.visitLdcInsn(propertyName);
        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/nginious/http/serialize/XmlBeanCollectionSerializer",
                "<init>", "(Ljava/lang/String;Lcom/nginious/http/serialize/XmlSerializer;)V");
        visitor.visitVarInsn(Opcodes.ASTORE, 5);

        visitor.visitVarInsn(Opcodes.ALOAD, 5);
        visitor.visitVarInsn(Opcodes.ALOAD, 1);

        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/XmlBeanCollectionSerializer",
                "serialize", "(Ljavax/xml/transform/sax/TransformerHandler;Ljava/util/Collection;)V");
    }

    /**
     * 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/XmlSerializer",
                "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",
                "createXmlSerializer", "(Ljava/lang/Class;)Lcom/nginious/http/serialize/XmlSerializer;");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);

        visitor.visitVarInsn(Opcodes.ALOAD, 4);
        visitor.visitVarInsn(Opcodes.ALOAD, 1);
        visitor.visitLdcInsn(convertToXmlName(propertyName));
        visitor.visitVarInsn(Opcodes.ALOAD, 3);
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, intBeanClazzName, returnMethodName,
                "()L" + intReturnClazzName + ";");
        visitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/nginious/http/serialize/XmlSerializer", "serialize",
                "(Ljavax/xml/transform/sax/TransformerHandler;Ljava/lang/String;Ljava/lang/Object;)V");
        visitor.visitVarInsn(Opcodes.ASTORE, 4);
    }

    /**
     * Creates bytecode for serializing property matching the specified bean method name. The generated bytecode calls the
     * appropriate serialization method in the class {@link XmlSerializer} 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 XmlSerializer} used for serializing property
     * @param propertyName name of XML element
     * @param methodType binary type for method in class {@link XmlSerializer} 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,
                "(Ljavax/xml/transform/sax/TransformerHandler;Ljava/lang/String;" + methodType + ")V");
    }

    /**
     * Creates bytecode which implements the {@link XmlSerializer#serializeProperties(javax.xml.transform.sax.TransformerHandler, 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",
                "(Ljavax/xml/transform/sax/TransformerHandler;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, 0);
        visitor.visitVarInsn(Opcodes.ALOAD, 2);
        visitor.visitTypeInsn(Opcodes.CHECKCAST, intBeanClazzName);
        visitor.visitIntInsn(Opcodes.ASTORE, 3);
        return visitor;
    }

    /**
     * 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());
    }

    /**
      * Converts the specified method name to a XML tag name for use in serialized XML.
      * 
      * @param name the name to convert
      * @return the converted name
      */
    protected String convertToXmlName(String name) {
        StringBuffer xmlName = new StringBuffer();

        for (int i = 0; i < name.length(); i++) {
            char ch = name.charAt(i);

            if (Character.isUpperCase(ch)) {
                if (i > 0) {
                    xmlName.append('-');
                }

                xmlName.append(Character.toLowerCase(ch));
            } else {
                xmlName.append(ch);
            }
        }

        return xmlName.toString();
    }
}