// throws NotFoundException if fails to get names
 // (for example when class of method argument type has not been loaded by class loader)
 private String generateMethodKey(CtMethod method) throws NotFoundException {
   String classQualifiedName = method.getDeclaringClass().getName();
   String methodSimpleName = method.getName();
   List<String> argClassQualifiedNames = getArgClassQualifiedNames(method);
   return TestMethod.generateMethodKey(
       classQualifiedName, methodSimpleName, argClassQualifiedNames);
 }
  @Override
  public byte[] transform(
      ClassLoader loader,
      String className,
      Class<?> classBeingRedefined,
      ProtectionDomain protectionDomain,
      byte[] classfileBuffer)
      throws IllegalClassFormatException {
    // Don't transform system classes. This reason is:
    // - To improve performance
    // - To avoid unexpected behaviors.
    //   For example, if transforms java.lang.invoke.** classes in Java8
    //   even without any ctClass modification, the classes become broken
    //   and Java stream API call fails unexpectedly.
    //   Maybe this is because CtClass instance generated by ClassPool.makeClass method
    //   is cached on global default ClassPool instance.
    if (isJavaSystemClassName(className)) {
      return null;
    }

    // TODO don't need to do anything for java package classes
    ClassPool classPool = ClassPool.getDefault();
    String hookClassName = HookMethodDef.class.getCanonicalName();
    String initializeSrc = hookInitializeSrc();
    boolean transformed = false;
    InputStream stream = null;
    try {
      stream = new ByteArrayInputStream(classfileBuffer);
      CtClass ctClass = null;
      try {
        ctClass = classPool.makeClass(stream, true);
      } catch (RuntimeException e) {
        // makeClass raises RuntimeException when the existing class is frozen.
        // Since frozen classes are maybe system class, just ignore this exception
        return null;
      }

      for (Pair<CtMethod, TestMethod> pair : allSubMethods(srcTree, ctClass)) {
        CtMethod ctSubMethod = pair.getLeft();
        TestMethod subMethod = pair.getRight();
        if (ctSubMethod.isEmpty()) {
          logger.info("skip empty method: " + ctSubMethod.getLongName());
          continue; // cannot hook empty method
        }

        String subClassQualifiedName = subMethod.getTestClass().getQualifiedName();
        String subMethodSimpleName = subMethod.getSimpleName();
        String subMethodArgClassesStr =
            TestMethod.argClassQualifiedNamesToArgClassesStr(
                getArgClassQualifiedNames(ctSubMethod));
        for (int i = 0; i < subMethod.getCodeBody().size(); i++) {
          CodeLine codeLine = subMethod.getCodeBody().get(i);
          if (i + 1 < subMethod.getCodeBody().size()) {
            CodeLine nextCodeLine = subMethod.getCodeBody().get(i + 1);
            assert codeLine.getEndLine() <= nextCodeLine.getStartLine();
            if (codeLine.getEndLine() == nextCodeLine.getStartLine()) {
              // - if multiple statements exist on a line, insert hook only after the last statement
              // - avoid insertion at the middle of the statement.
              //   The problem happens when multi-line statements are like:
              //   method(1);method(
              //           2);
              continue;
            }
          }

          // Hook should be inserted just after the code has finished
          // since the code inserted by the insertAt method is inserted just before the specified
          // line.
          int insertedLine = subMethod.getCodeBody().get(i).getEndLine() + 1;
          int actualInsertedLine = ctSubMethod.insertAt(insertedLine, false, null);
          ctSubMethod.insertAt(
              insertedLine,
              String.format(
                  "%s%s.beforeCodeLineHook(\"%s\",\"%s\",\"%s\",\"%s\",%d, %d);",
                  initializeSrc,
                  hookClassName,
                  subClassQualifiedName,
                  subMethodSimpleName,
                  subMethodSimpleName,
                  subMethodArgClassesStr,
                  codeLine.getStartLine(),
                  actualInsertedLine));
          transformed = true;
        }
      }

      CtClass exceptionType = classPool.get(Throwable.class.getCanonicalName());
      for (Pair<CtMethod, TestMethod> pair : allRootMethods(srcTree, ctClass)) {
        CtMethod ctRootMethod = pair.getLeft();
        TestMethod rootMethod = pair.getRight();
        if (ctRootMethod.isEmpty()) {
          continue; // cannot hook empty method
        }

        String rootClassQualifiedName = rootMethod.getTestClass().getQualifiedName();
        String rootMethodSimpleName = rootMethod.getSimpleName();
        String rootMethodArgClassesStr =
            TestMethod.argClassQualifiedNamesToArgClassesStr(
                getArgClassQualifiedNames(ctRootMethod));
        for (int i = 0; i < rootMethod.getCodeBody().size(); i++) {
          CodeLine codeLine = rootMethod.getCodeBody().get(i);
          if (i + 1 < rootMethod.getCodeBody().size()) {
            CodeLine nextCodeLine = rootMethod.getCodeBody().get(i + 1);
            assert codeLine.getEndLine() <= nextCodeLine.getStartLine();
            if (codeLine.getEndLine() == nextCodeLine.getStartLine()) {
              // - if multiple statements exist on a line, insert hook only after the last statement
              // - avoid insertion at the middle of the statement.
              //   The problem happens when multi-line statements are like:
              //   method(1);method(
              //           2);
              continue;
              // TODO screen capture is not taken correctly for multiple statements in a line
            }
          }

          // Hook should be inserted just after the code has finished
          // since the code inserted by the insertAt method is inserted just before the specified
          // line.
          int insertedLine = rootMethod.getCodeBody().get(i).getEndLine() + 1;
          int actualInsertedLine = ctRootMethod.insertAt(insertedLine, false, null);
          ctRootMethod.insertAt(
              insertedLine,
              String.format(
                  "%s%s.beforeCodeLineHook(\"%s\",\"%s\",\"%s\",\"%s\",%d,%d);",
                  initializeSrc,
                  hookClassName,
                  rootClassQualifiedName,
                  rootMethodSimpleName,
                  rootMethodSimpleName,
                  rootMethodArgClassesStr,
                  codeLine.getStartLine(),
                  actualInsertedLine));
        }

        ctRootMethod.insertBefore(
            String.format(
                "%s%s.beforeMethodHook(\"%s\",\"%s\",\"%s\");",
                initializeSrc,
                hookClassName,
                rootClassQualifiedName,
                rootMethodSimpleName,
                rootMethodSimpleName));
        ctRootMethod.addCatch(
            String.format(
                "{ %s%s.methodErrorHook(\"%s\",\"%s\",$e); throw $e; }",
                initializeSrc, hookClassName, rootClassQualifiedName, rootMethodSimpleName),
            exceptionType);
        ctRootMethod.insertAfter(
            String.format(
                "%s%s.afterMethodHook(\"%s\",\"%s\");",
                initializeSrc, hookClassName, rootClassQualifiedName, rootMethodSimpleName),
            true);
        transformed = true;
      }

      // don't transform not changed ctClass
      // (to improve performance and avoid unexpected error)
      if (transformed) {
        logger.info("transform " + className);
        return ctClass.toBytecode();
      } else {
        return null;
      }
    } catch (CannotCompileException e) {
      // print error since exception in transform method is just ignored
      System.err.println("exception on " + className);
      e.printStackTrace();
      throw new IllegalClassFormatException(e.getLocalizedMessage());
    } catch (Exception e) {
      // print error since exception in transform method is just ignored
      System.err.println("exception on " + className);
      e.printStackTrace();
      throw new RuntimeException(e);
    } finally {
      IOUtils.closeQuietly(stream);
    }
  }