com.centimia.orm.jaqu.ext.asm.JaquClassAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.centimia.orm.jaqu.ext.asm.JaquClassAdapter.java

Source

/*
 * Copyright (c) 2007-2010 Centimia Ltd.
 * All rights reserved.  Unpublished -- rights reserved
 *
 * Use of a copyright notice is precautionary only, and does
 * not imply publication or disclosure.
 *  
 * Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group, Centimia Inc.
 */

/*
 * Update Log
 * 
 *  Date         User            Comment
 * ------         -------            --------
 * 01/02/2010      Shai Bentin          create
 */
package com.centimia.orm.jaqu.ext.asm;

import java.util.HashSet;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import com.centimia.orm.jaqu.annotation.Entity;

/**
 * asm adapter which adds post compile data to the classes annotated with {@link Entity}
 * 
 * @author Shai Bentin
 *
 */
public class JaquClassAdapter extends ClassVisitor implements Opcodes {

    private static final String $ORIG = "$orig_";
    private String className;
    private HashSet<String> relationFields = new HashSet<>();
    private HashSet<String> lazyLoadFields = new HashSet<>();
    private boolean isEntityAnnotationPresent = false;
    private boolean isMappedSupperClass = false;

    public JaquClassAdapter(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    /* (non-Javadoc)
     * @see org.objectweb.asm.ClassAdapter#visit(int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName,
            String[] interfaces) {
        this.className = name;
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    /* (non-Javadoc)
     * @see org.objectweb.asm.ClassAdapter#visitAnnotation(java.lang.String, boolean)
     */
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if (desc != null && desc.indexOf("com/centimia/orm/jaqu/annotation/Entity") != -1) {
            this.isEntityAnnotationPresent = true;
        }
        if (desc != null && desc.indexOf("com/centimia/orm/jaqu/annotation/MappedSuperclass") != -1) {
            this.isMappedSupperClass = true;
        }
        return super.visitAnnotation(desc, visible);
    }

    /* (non-Javadoc)
     * @see org.objectweb.asm.ClassAdapter#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[])
     * 
     * this method visitor changes the name of the relation getter to $orig_[originalName]
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        // 1. if name is in the list of o2m and return type is collection instrument add call to db.getRelationFromDb or db.getRelationArrayFromDb, only if value of field is null;
        if ((isEntityAnnotationPresent || isMappedSupperClass) && name.startsWith("get")) {
            // this is a getter check if it is a relation getter
            if (relationFields.contains(name.substring(3).toLowerCase())) {
                // this is a relationship.
                String newName = $ORIG + name;
                String fieldName = camelCase(name);

                generateNewMethodBody(access, desc, signature, exceptions, name, newName, fieldName);

                return super.visitMethod(access, newName, desc, signature, exceptions);
            } else if (lazyLoadFields.contains(name.substring(3).toLowerCase())) {
                // this is a O2O relationship which should be lazy loaded
                String newName = $ORIG + name;
                String fieldName = camelCase(name);

                generateLazyRelation(access, desc, exceptions, name, newName, fieldName);
                return super.visitMethod(access, newName, desc, signature, exceptions);
            } else
                return super.visitMethod(access, name, desc, signature, exceptions);
        } else if (isEntityAnnotationPresent || isMappedSupperClass)
            return super.visitMethod(access, name, desc, signature, exceptions);

        else
            return null;
    }

    /**
     * @param name
     * @return
     */
    private String camelCase(String name) {
        char[] realName = name.substring(3).toCharArray();
        realName[0] = Character.toLowerCase(realName[0]);
        return new String(realName);
    }

    /* (non-Javadoc)
     * @see org.objectweb.asm.ClassAdapter#visitField(int, java.lang.String, java.lang.String, java.lang.String, java.lang.Object)
     */
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (isEntityAnnotationPresent || isMappedSupperClass) {
            // collect the fields that are relation by rule. (Collection type fields....)
            if (desc.indexOf("java/util/List") != -1 || desc.indexOf("java/util/Set") != -1
                    || desc.indexOf("java/util/Collection") != -1)
                relationFields.add(name.toLowerCase());
        }
        return new JaquFieldVisitor(Opcodes.ASM5, super.visitField(access, name, desc, signature, value),
                name.toLowerCase(), relationFields, lazyLoadFields);
    }

    /* (non-Javadoc)
     * @see org.objectweb.asm.ClassAdapter#visitEnd()
     */
    @Override
    public void visitEnd() {
        if (isEntityAnnotationPresent) {
            FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_TRANSIENT, "db", "Lcom/centimia/orm/jaqu/Db;", null,
                    null);
            fv.visitEnd();

            fv = cv.visitField(ACC_PUBLIC, "isLazy", "Z", null, null);
            // add the jaquIgnore annotaion to the lazy field because we need to carry this field around the network but not persist it
            AnnotationVisitor av = fv.visitAnnotation("Lcom/centimia/orm/jaqu/annotation/JaquIgnore;", true);
            av.visitEnd();
            fv.visitEnd();
        }

        super.visitEnd();
    }

    public final boolean isEntityAnnotationPresent() {
        return this.isEntityAnnotationPresent;
    }

    public final boolean isMappedSuperClass() {
        return this.isMappedSupperClass;
    }

    /**
     * Generates the new method body, copy the old to a new method and connect them.
     * 
     * the structure of the new method is:<br>
     * <br><b><div style="background:lightgray">
     * <pre>
     * public [CollectionType] [getterName]() {
     *    if ([fieldName] == null){
     *       try {
     *          if (null == db || db.isClosed())
     *             throw new RuntimeException("Cannot initialize a 'Relation' outside an open session!!!. Try initializing the field directly within the class.");
     *          Method method = db.getClass().getDeclaredMethod("getRelationFromDb", String.class, Object.class, Class.class);
     *          method.setAccessible(true);
     *          children = (Collection<TestTable>)method.invoke(db, [fieldName], this, TestTable.class);
     *          method.setAccessible(false);
     *       }
     *       catch (Exception e) {
     *          if (e instanceof RuntimeException)
     *             throw (RuntimeException)e;
     *          throw new RuntimeException(e.getMessage(), e);
     *       }
     *    }
     * return $orig_[getterName]();
     * }
     * </pre>
     * </div>
     * 
     * @param access
     * @param desc
     * @param signature
     * @param exceptions
     * @param name
     * @param newName
     * @param fieldName
     */
    private void generateNewMethodBody(int access, String desc, String signature, String[] exceptions, String name,
            String newName, String fieldName) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        String fieldSignature = signature.substring(signature.indexOf(')') + 1, signature.lastIndexOf('<')) + ";";
        String type = signature.substring(signature.indexOf('<') + 1, signature.indexOf('>'));
        String cast = desc.substring(desc.indexOf("java/"), desc.indexOf(';'));

        mv.visitCode();
        Label l0 = new Label();
        Label l1 = new Label();
        Label l2 = new Label();
        mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, fieldName, fieldSignature);
        Label l3 = new Label();
        mv.visitJumpInsn(IFNONNULL, l3);
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        Label l4 = new Label();
        mv.visitJumpInsn(IFNULL, l4);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/Db", "isClosed", "()Z", false);
        Label l5 = new Label();
        mv.visitJumpInsn(IFEQ, l5);
        mv.visitLabel(l4);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
        mv.visitInsn(DUP);
        mv.visitLdcInsn(
                "Cannot initialize a 'Relation' outside an open session!!!. Try initializing the field directly within the class.");
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V", false);
        mv.visitInsn(ATHROW);
        mv.visitLabel(l5);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitLdcInsn("getRelationFromDb");
        mv.visitInsn(ICONST_3);
        mv.visitTypeInsn(ANEWARRAY, "java/lang/Class");
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_0);
        mv.visitLdcInsn(Type.getType("Ljava/lang/String;"));
        mv.visitInsn(AASTORE);
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_1);
        mv.visitLdcInsn(Type.getType("Ljava/lang/Object;"));
        mv.visitInsn(AASTORE);
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_2);
        mv.visitLdcInsn(Type.getType("Ljava/lang/Class;"));
        mv.visitInsn(AASTORE);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getDeclaredMethod",
                "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false);
        mv.visitVarInsn(ASTORE, 1);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitInsn(ICONST_1);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/Method", "setAccessible", "(Z)V", false);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitInsn(ICONST_3);
        mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_0);
        mv.visitLdcInsn(fieldName);
        mv.visitInsn(AASTORE);
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_1);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitInsn(AASTORE);
        mv.visitInsn(DUP);
        mv.visitInsn(ICONST_2);
        mv.visitLdcInsn(Type.getType(type));
        mv.visitInsn(AASTORE);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/Method", "invoke",
                "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", false);
        mv.visitTypeInsn(CHECKCAST, cast);
        mv.visitFieldInsn(PUTFIELD, className, fieldName, fieldSignature);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitInsn(ICONST_0);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/Method", "setAccessible", "(Z)V", false);
        mv.visitLabel(l1);
        mv.visitJumpInsn(GOTO, l3);
        mv.visitLabel(l2);
        mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] { "java/lang/Exception" });
        mv.visitVarInsn(ASTORE, 1);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(INSTANCEOF, "java/lang/RuntimeException");
        Label l6 = new Label();
        mv.visitJumpInsn(IFEQ, l6);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, "java/lang/RuntimeException");
        mv.visitInsn(ATHROW);
        mv.visitLabel(l6);
        mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { "java/lang/Exception" }, 0, null);
        mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
        mv.visitInsn(DUP);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "getMessage", "()Ljava/lang/String;", false);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>",
                "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);
        mv.visitInsn(ATHROW);
        mv.visitLabel(l3);
        mv.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, className, newName, desc, false);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(7, 2);
        mv.visitEnd();
    }

    /**
     * Generates the new method body, copy the old to a new method and connect them.
     * the structure of the new method is:<br>
     * <br><b><div style="background:lightgray">
     * <pre>
     * public [entityType] [getterName]() {
     *   if ([field] == null) {
     *      try {
     *         if (null == db)
     *            throw new RuntimeException("Cannot initialize 'Relation' outside an open session!!!. Try initializing field directly within the class.");
     *         
     *         [parentType] parent = this.getClass().newInstance();
     *         [entityType] desc = TestB.class.newInstance();
     *         
     *         // get the primary key
     *         Object pk = db.getPrimaryKey(this);
     *         
     *         // get the object
     *         [entityValue] = db.from(desc).innerJoin(parent).on(parent.[entityValue]).is(desc).where(db.getPrimaryKey(parent)).is(pk).selectFirst();
     *      }
     *      catch (Exception e) {
     *         if (e instanceof RuntimeException)
     *            throw (RuntimeException)e;
     *         throw new RuntimeException(e.getMessage(), e);
     *      }
     *   }
     *   return $orig_[getterName]();
     *  }
     * </pre>
     * </div>
     * 
     * @param access
     * @param desc
     * @param exceptions
     * @param name - current method name
     * @param newName - new method name (the orig...)
     * @param fieldName
     */
    public void generateLazyRelation(int access, String desc, String[] exceptions, String name, String newName,
            String fieldName) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, null, exceptions);
        String fieldSignature = desc.substring(desc.indexOf(')') + 1);
        String fieldClassName = desc.substring(desc.indexOf(')') + 2, desc.length() - 1);

        mv.visitCode();
        Label l0 = new Label();
        Label l1 = new Label();
        Label l2 = new Label();
        mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, fieldName, fieldSignature);
        Label l3 = new Label();
        mv.visitJumpInsn(IFNULL, l3);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, fieldName, fieldSignature);
        mv.visitFieldInsn(GETFIELD, fieldClassName, "isLazy", "Z");
        mv.visitJumpInsn(IFEQ, l3);
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        Label l4 = new Label();
        mv.visitJumpInsn(IFNULL, l4);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/Db", "isClosed", "()Z", false);
        Label l5 = new Label();
        mv.visitJumpInsn(IFEQ, l5);
        mv.visitLabel(l4);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
        mv.visitInsn(DUP);
        mv.visitLdcInsn(
                "Cannot initialize 'Relation' outside an open session!!!. Try initializing field directly within the class.");
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/String;)V", false);
        mv.visitInsn(ATHROW);
        mv.visitLabel(l5);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "newInstance", "()Ljava/lang/Object;", false);
        mv.visitTypeInsn(CHECKCAST, className);
        mv.visitVarInsn(ASTORE, 1);
        mv.visitLdcInsn(Type.getType(fieldSignature));
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "newInstance", "()Ljava/lang/Object;", false);
        mv.visitTypeInsn(CHECKCAST, fieldClassName);
        mv.visitVarInsn(ASTORE, 2);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/Db", "getPrimaryKey",
                "(Ljava/lang/Object;)Ljava/lang/Object;", false);
        mv.visitVarInsn(ASTORE, 3);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitVarInsn(ALOAD, 2);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/Db", "from",
                "(Ljava/lang/Object;)Lcom/centimia/orm/jaqu/QueryInterface;", false);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEINTERFACE, "com/centimia/orm/jaqu/QueryInterface", "innerJoin",
                "(Ljava/lang/Object;)Lcom/centimia/orm/jaqu/QueryJoin;", true);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitFieldInsn(GETFIELD, className, fieldName, fieldSignature);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/QueryJoin", "on",
                "(Ljava/lang/Object;)Lcom/centimia/orm/jaqu/QueryJoinCondition;", false);
        mv.visitVarInsn(ALOAD, 2);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/QueryJoinCondition", "is",
                "(Ljava/lang/Object;)Lcom/centimia/orm/jaqu/QueryJoinWhere;", false);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, "db", "Lcom/centimia/orm/jaqu/Db;");
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/Db", "getPrimaryKey",
                "(Ljava/lang/Object;)Ljava/lang/Object;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/QueryJoinWhere", "where",
                "(Ljava/lang/Object;)Lcom/centimia/orm/jaqu/QueryCondition;", false);
        mv.visitVarInsn(ALOAD, 3);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/QueryCondition", "is",
                "(Ljava/lang/Object;)Lcom/centimia/orm/jaqu/QueryWhere;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "com/centimia/orm/jaqu/QueryWhere", "selectFirst", "()Ljava/lang/Object;",
                false);
        mv.visitTypeInsn(CHECKCAST, fieldClassName);
        mv.visitFieldInsn(PUTFIELD, className, fieldName, fieldSignature);
        mv.visitLabel(l1);
        mv.visitJumpInsn(GOTO, l3);
        mv.visitLabel(l2);
        mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] { "java/lang/Exception" });
        mv.visitVarInsn(ASTORE, 1);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(INSTANCEOF, "java/lang/RuntimeException");
        Label l6 = new Label();
        mv.visitJumpInsn(IFEQ, l6);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitTypeInsn(CHECKCAST, "java/lang/RuntimeException");
        mv.visitInsn(ATHROW);
        mv.visitLabel(l6);
        mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { "java/lang/Exception" }, 0, null);
        mv.visitTypeInsn(NEW, "java/lang/RuntimeException");
        mv.visitInsn(DUP);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "getMessage", "()Ljava/lang/String;", false);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>",
                "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);
        mv.visitInsn(ATHROW);
        mv.visitLabel(l3);
        mv.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKEVIRTUAL, className, newName, desc, false);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(4, 4);
        mv.visitEnd();
    }

    /**
     * Returns true when the adapter has dealt with a JaQu annotated class and altered it.
     * @return boolean
     */
    public boolean isJaquAnnotated() {
        return isEntityAnnotationPresent || isMappedSupperClass;
    }
}