/* * Copyright (c) 2006 Rogério Liesenfeld * This file is subject to the terms of the MIT license (see LICENSE.txt). */ package mockit.internal.expectations.transformation; import java.lang.instrument.*; import java.security.*; import java.util.*; import javax.annotation.*; import static java.lang.reflect.Modifier.*; import mockit.external.asm.*; import mockit.internal.*; import mockit.internal.startup.*; import mockit.internal.util.*; import static mockit.external.asm.ClassReader.*; public final class ExpectationsTransformer implements ClassFileTransformer { @Nonnull private final List<String> baseSubclasses; public ExpectationsTransformer(@Nonnull Instrumentation instrumentation) { baseSubclasses = new ArrayList<String>(); baseSubclasses.add("mockit/Expectations"); baseSubclasses.add("mockit/StrictExpectations"); baseSubclasses.add("mockit/Verifications"); baseSubclasses.add("mockit/FullVerifications"); baseSubclasses.add("mockit/VerificationsInOrder"); baseSubclasses.add("mockit/FullVerificationsInOrder"); Class<?>[] alreadyLoaded = instrumentation.getAllLoadedClasses(); findAndModifyOtherBaseSubclasses(alreadyLoaded); modifyFinalSubclasses(alreadyLoaded); } private void findAndModifyOtherBaseSubclasses(@Nonnull Class<?>[] alreadyLoaded) { for (Class<?> aClass : alreadyLoaded) { if ( aClass.getClassLoader() != null && !isFinalClass(aClass) && isExpectationsOrVerificationsSubclassFromUserCode(aClass) ) { modifyInvocationsSubclass(aClass, false); } } } private static boolean isFinalClass(@Nonnull Class<?> aClass) { return isFinal(aClass.getModifiers()) || ClassNaming.isAnonymousClass(aClass); } private static boolean isExpectationsOrVerificationsSubclassFromUserCode(@Nonnull Class<?> aClass) { if (isExpectationsOrVerificationsAPIClass(aClass)) { return false; } Class<?> superclass = aClass.getSuperclass(); while (superclass != null && superclass != Object.class && superclass.getClassLoader() != null) { if (isExpectationsOrVerificationsAPIClass(superclass)) { return true; } superclass = superclass.getSuperclass(); } return false; } private static boolean isExpectationsOrVerificationsAPIClass(@Nonnull Class<?> aClass) { return ("mockit.Expectations mockit.StrictExpectations mockit.Verifications mockit.FullVerifications " + "mockit.VerificationsInOrder mockit.FullVerificationsInOrder").contains(aClass.getName()); } private void modifyFinalSubclasses(@Nonnull Class<?>[] alreadyLoaded) { for (Class<?> aClass : alreadyLoaded) { if ( aClass.getClassLoader() != null && isFinalClass(aClass) && isExpectationsOrVerificationsSubclassFromUserCode(aClass) ) { modifyInvocationsSubclass(aClass, true); } } } private void modifyInvocationsSubclass(@Nonnull Class<?> aClass, boolean isFinalClass) { ClassReader cr = ClassFile.createClassFileReader(aClass); byte[] modifiedClassfile = modifyInvocationsSubclass(cr, aClass.getClassLoader(), isFinalClass); if (modifiedClassfile != null) { Startup.redefineMethods(aClass, modifiedClassfile); } } @Nullable private byte[] modifyInvocationsSubclass(@Nonnull ClassReader cr, ClassLoader loader, boolean finalClass) { EndOfBlockModifier modifier = new EndOfBlockModifier(cr, loader, finalClass); try { cr.accept(modifier, SKIP_FRAMES); return modifier.toByteArray(); } catch (VisitInterruptedException ignore) {} catch (Throwable e) { e.printStackTrace(); } return null; } @Nullable @Override public byte[] transform( @Nullable ClassLoader loader, @Nonnull String className, @Nullable Class<?> classBeingRedefined, @Nullable ProtectionDomain protectionDomain, @Nonnull byte[] classfileBuffer) { if (classBeingRedefined == null && protectionDomain != null) { ClassReader cr = new ClassReader(classfileBuffer); String superClassName = cr.getSuperName(); if ( !baseSubclasses.contains(superClassName) && !superClassName.endsWith("Expectations") && !superClassName.endsWith("Verifications") ) { return null; } boolean finalClass = ClassNaming.isAnonymousClass(className); return modifyInvocationsSubclass(cr, loader, finalClass); } return null; } private final class EndOfBlockModifier extends ClassVisitor { @Nonnull private final ClassWriter cw; @Nullable private final ClassLoader loader; private boolean isFinalClass; @Nonnull private String classDesc; EndOfBlockModifier(@Nonnull ClassReader cr, @Nullable ClassLoader loader, boolean isFinalClass) { super(new ClassWriter(cr)); assert cv != null; cw = (ClassWriter) cv; this.loader = loader; this.isFinalClass = isFinalClass; classDesc = ""; } @Override public void visit( int version, int access, @Nonnull String name, @Nullable String signature, @Nullable String superName, @Nullable String[] interfaces) { if (isFinal(access)) { isFinalClass = true; } if (isClassWhichShouldBeModified(name, superName)) { cw.visit(version, access, name, signature, superName, interfaces); classDesc = name; } else { throw VisitInterruptedException.INSTANCE; } } private boolean isClassWhichShouldBeModified(@Nonnull String name, @Nullable String superName) { if (baseSubclasses.contains(name)) { return false; } int i = baseSubclasses.indexOf(superName); boolean superClassIsKnownInvocationsSubclass = i >= 0; if (isFinalClass) { if (superClassIsKnownInvocationsSubclass) { return true; } SuperClassAnalyser superClassAnalyser = new SuperClassAnalyser(loader); if (superClassAnalyser.classExtendsInvocationsClass(superName)) { return true; } } else if (superClassIsKnownInvocationsSubclass) { baseSubclasses.add(name); return true; } else { SuperClassAnalyser superClassAnalyser = new SuperClassAnalyser(loader); if (superClassAnalyser.classExtendsInvocationsClass(superName)) { baseSubclasses.add(name); return true; } } return false; } @Override public MethodVisitor visitMethod( int access, @Nonnull String name, @Nonnull String desc, @Nullable String signature, @Nullable String[] exceptions) { MethodWriter mw = cw.visitMethod(access, name, desc, signature, exceptions); boolean callEndInvocations = isFinalClass && "<init>".equals(name); return new InvocationBlockModifier(mw, classDesc, callEndInvocations); } } private final class SuperClassAnalyser extends ClassVisitor { @Nullable private final ClassLoader loader; private boolean classExtendsBaseSubclass; private SuperClassAnalyser(@Nullable ClassLoader loader) { this.loader = loader; } boolean classExtendsInvocationsClass(@Nullable String classOfInterest) { if (classOfInterest == null || "java/lang/Object".equals(classOfInterest)) { return false; } ClassReader cr = ClassFile.createClassFileReader(loader, classOfInterest); try { cr.accept(this, SKIP_DEBUG + SKIP_FRAMES); } catch (VisitInterruptedException ignore) {} return classExtendsBaseSubclass; } @Override public void visit( int version, int access, @Nonnull String name, @Nullable String signature, @Nullable String superName, @Nullable String[] interfaces) { classExtendsBaseSubclass = baseSubclasses.contains(superName); if (!classExtendsBaseSubclass && !"java/lang/Object".equals(superName)) { classExtendsInvocationsClass(superName); } throw VisitInterruptedException.INSTANCE; } } }