Back

package com.futureshocked.classloader;

import com.futureshocked.debug.StringUtil;

import org.objectweb.asm.*;

import java.util.List;
import java.util.HashMap;
import java.util.Iterator;

/**
 * An <tt>org.objectweb.asm.ClassAdapter</tt> that will modify classes as
 *   needed. As a class is loaded the appropriate visit*() methods are called
 *   in this class.<br><br>
 *
 * By default, an {@link InterceptedMethod} object that requests a static
 *   debugging call will call <tt>StaticDebugger.printDebugInformation()</tt>.
 *
 * <br><br>See also: Visitor design pattern.
 */
public class DebuggerClassVisitor extends ClassAdapter
    implements Opcodes {
  /**
   * The name of the class currently being visited.
   */
  protected String className;

  /**
   * A {@link ClassTransforms} that holds {@link InterceptedMethod} objects.
   */
  protected ClassTransforms ct;

  /**
   * <tt>true</tt> if debugging information should be displayed.
   */
  protected boolean verbose;

  /**
   * The message to be printed when the recursion limit is exceeded.
   */
  protected String message = "Recursion limit exceeded for : ";

  /**
   * The name of the class that contains a static method to be called for
   *   debugging purposes.
   */
  protected String staticDebugClass = "com/futureshocked/debug/StaticDebugger";

  /**
   * The name of the static method to be called for debugging purposes.
   */
  protected String staticDebugMethod = "printDebugInformation";

  /**
   * The description of the static method to be called for debugging. Should
   *   be in the appropriate ASM format - see {@link ApplicationDebugger} for
   *   an example.
   */
  protected String staticDebugDescription = "(Ljava/lang/Object;I)V";

  /**
   * Constructor that sets the appropriate instance variables.
   *
   * @param cv A <tt>ClassVisitor</tt> or <tt>ClassWriter</tt> that has already
   *   been created.
   * @param className The name of the class being visited.
   * @param ct Container for {@link InterceptedMethod} objects.
   * @param verbose <tt>true</tt> if debugging info should be displayed.
   */
  public DebuggerClassVisitor(ClassVisitor cv, String className,
      ClassTransforms ct, boolean verbose) {
    super(cv);
    this.className = className;
    this.ct = ct;
    this.verbose = verbose;
  }

  /**
   * <tt>visit()</tt> is called when a class is first created. All extra
   *   fields related to recursion limited are created here.
   *
   * @param version see ASM documentation.
   * @param access Access flags - see ASM documentation.
   * @param name The name of the class.
   * @param signature The class signature - see ASM documentation.
   * @param superName The name of the parent class - see ASM documentation.
   * @param interfaces Interfaces implemented by this object.
   */
  public void visit(int version, int access, String name, String signature,
      String superName, String[] interfaces) {
    super.visit(version, access, name, signature, superName, interfaces);

    // All overloaded methods with the same name share the same recursion
    //   counter - this HashMap keeps the fields from being created twice.
    HashMap addedFields = new HashMap();
    List methods = ct.getMethodsByClass(className);

    if (methods != null) {
      Iterator i = methods.iterator();
      while (i.hasNext()) {
        InterceptedMethod im = (InterceptedMethod) i.next();
        if (im.needsRecursionLimiter()) {
          String fieldName = getFieldNameByMethod(im.getMethodName());

          // If the field hasn't already been created, create it
          if (!addedFields.containsKey(fieldName)) {
            if (verbose)
              System.out.println("RecursionLimiterClassVisitor: " +
                  "creating field " + StringUtil.getShortClassName(className) +
                  "." + fieldName);

            super.visitField(ACC_PRIVATE, fieldName, "I", null, null);
            addedFields.put(fieldName, im);
          }
        }
      }
    }
  }

  /**
   * Called for every method in a class. If this is called for a method
   *   we wish to intercept, this method will add the appropriate bytecode.
   *
   * @param access Access flags for the method - see ASM documentation.
   * @param name The name of the method.
   * @param desc The description of the method - see {@link ApplicationDebugger}
   *   or ASM documentation.
   * @param signature Method signature - see ASM documentation.
   * @param exceptions Exceptions thrown by this method - see ASM documentation.
   * @return <tt>MethodVisitor</tt> object
   */
  public MethodVisitor visitMethod(int access, String name, String desc,
      String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
        exceptions);

    InterceptedMethod im = ct.getInterceptedMethod(className, name, desc);
    if (im != null) {
      if (im.needsRecursionLimiter())
        mv = addRecursionLimiter(mv, im, getReturnTypeFromDescription(desc));

      if (im.needsStaticDebuggerCall())
        mv = addStaticDebuggerCall(mv, im);
    }

    return mv;
  }

  /**
   * Adds the bytecode to call the static debugger.
   *
   * @param mv The current <tt>MethodVisitor</tt>
   * @param im The {@link InterceptedMethod} object that represents the method
   *   that will be modified.
   * @return <tt>MethodVisitor</tt> object passed in
   */
  public MethodVisitor addStaticDebuggerCall(MethodVisitor mv,
      InterceptedMethod im) {
    // corresponds to: SomeClass.staticMethod(this, int debugDepth);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitIntInsn(BIPUSH, im.getStaticDebuggerCallDepth());
    mv.visitMethodInsn(INVOKESTATIC, staticDebugClass, staticDebugMethod,
        staticDebugDescription);
    return mv;
  }

  /**
   * Adds a recursion limit to a recursive limit - keeps recursive methods that
   *   are buggy from causing a stack overflow.
   *
   * @param mv The current <tt>MethodVisitor</tt>
   * @param im The {@link InterceptedMethod} object that represents the method
   *   that will be modified.
   * @param returnType The return type of the method being modified - this is
   *   the same type as in ASM description, not full class names such as String.
   * @return The current <tt>MethodVisitor</tt>
   */
  public MethodVisitor addRecursionLimiter(MethodVisitor mv,
      InterceptedMethod im, String returnType) {
    String field = getFieldNameByMethod(im.getMethodName());
    // need to turn com.some.package.Class into com/some/pacakge/Class
    String clazz = im.getClassName().replace('.', '/');

    // this will execute '(field)++'
    mv.visitVarInsn(ALOAD, 0);
    mv.visitInsn(DUP);
    mv.visitFieldInsn(GETFIELD, clazz, field, "I");
    mv.visitInsn(ICONST_1);
    mv.visitInsn(IADD);
    mv.visitFieldInsn(PUTFIELD, clazz, field, "I");

    // this will compare field to maxRecursionCount, and jump to below if it's
    //  less than the max.
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETFIELD, clazz, field, "I");
    mv.visitIntInsn(BIPUSH, im.getMaxRecursionCount());
    Label l0 = new Label();
    mv.visitJumpInsn(IF_ICMPLE, l0);

    // if message is not null print out an error to System.out
    if (message != null) {
      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
          "Ljava/io/PrintStream;");
      mv.visitLdcInsn(message + im.getMethodName());
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
          "(Ljava/lang/String;)V");
    }

    // Executes '(field)--', since the code added to the end of the method will
    //   not be reached.
    mv.visitVarInsn(ALOAD, 0);
    mv.visitInsn(DUP);
    mv.visitFieldInsn(GETFIELD, clazz, field, "I");
    mv.visitInsn(ICONST_1);
    mv.visitInsn(ISUB);
    mv.visitFieldInsn(PUTFIELD, clazz, field, "I");

    // Adds a return instruction of the appropriate type. Currently only types
    //   void, int, and Object are supported.
    if (returnType.equals("V")) {
      mv.visitInsn(RETURN);
    }
    else if (returnType.equals("I")) {
      // return 0 for return type of int
      mv.visitInsn(ICONST_0);
      mv.visitInsn(IRETURN);
    }
    else {
      // return null for Object return types
      mv.visitInsn(ACONST_NULL);
      mv.visitInsn(ARETURN);
    }
    // if recursion count is under the max, the jump from above lands here
    mv.visitLabel(l0);
    mv = new RecursionLimiterMethodAdapter(mv, clazz, field);
    return mv;
  }

  /**
   * Returns the name of the field that holds the current recursion count for
   *   the method.
   *
   * @param method The method that needs to know it's recursion count.
   * @return A <tt>String</tt> that holds the field name.
   */
  private String getFieldNameByMethod(String method) {
    return "__" + method + "RecursionCount";
  }

  /**
   * Parses the return type from the method description.
   *
   * @param desc The method description - see {@link ApplicationDebugger}.
   * @return A <tt>String</tt> containing the type.
   */
  private String getReturnTypeFromDescription(String desc) {
    String type = null;
    if (desc != null) {
      type = desc.substring(desc.indexOf(')') + 1);
    }
    return type;
  }

  /**
   * Inner class that adds recursion limiting instructions to the end of
   *   a method. More specifically, it will execute '(field)--' before every
   *   RETURN opcode in a method.
   *
   * Since this will examine every opcode in a class file, it is only
   *   invoked on methods which are known to require modification before
   *   RETURN opcodes.
   */
  class RecursionLimiterMethodAdapter extends MethodAdapter implements Opcodes {
    /**
     * The name of the class being visited.
     */
    protected String className;

    /**
     * The name of the field to decrement.
     */
    protected String field;

    /**
     * Constructor which sets the appropriate instance variables.
     *
     * @param mv <tt>MethodVisitor</tt> for the current method.
     * @param className The name of the class.
     * @param field The name of the field to decrement.
     */
    public RecursionLimiterMethodAdapter(MethodVisitor mv, String className,
        String field) {
      super(mv);
      this.className = className;
      this.field = field;
    }

    /**
     * Called for every opcode in a method - if the opcode is a RETURN opcode
     *   then the field decrementing opcodes will be added.
     *
     * @param opcode
     */
    public void visitInsn(int opcode) {
      // Since opcodes are integers this covers the range of all possible
      //   return opcodes - cleaner looking than a big OR statement, but it does
      //   require an explanation. See org.objectweb.asm.Opcodes for more
      //   information.
      if ((opcode >= IRETURN) && (opcode <= RETURN)) {
        mv.visitVarInsn(ALOAD, 0);
        mv.visitInsn(DUP);
        mv.visitFieldInsn(GETFIELD, className, field, "I");
        mv.visitInsn(ICONST_1);
        mv.visitInsn(ISUB);
        mv.visitFieldInsn(PUTFIELD, className, field, "I");
      }
      super.visitInsn(opcode);
    }
  }
}

Top