/* * Copyright 2016 the original author or authors. * * 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 org.powermock.core.transformers.impl; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewConstructor; import javassist.Modifier; import javassist.NotFoundException; import javassist.bytecode.AccessFlag; import javassist.bytecode.AttributeInfo; import javassist.bytecode.ClassFile; import javassist.bytecode.CodeAttribute; import javassist.bytecode.DuplicateMemberException; import javassist.bytecode.FieldInfo; import javassist.bytecode.InnerClassesAttribute; import javassist.expr.ConstructorCall; import javassist.expr.ExprEditor; import javassist.expr.FieldAccess; import javassist.expr.MethodCall; import javassist.expr.NewExpr; import org.powermock.core.IndicateReloadClass; import org.powermock.core.MockGateway; import org.powermock.core.transformers.MockTransformer; import org.powermock.core.transformers.TransformStrategy; import static org.powermock.core.transformers.TransformStrategy.CLASSLOADER; import static org.powermock.core.transformers.TransformStrategy.INST_REDEFINE; import static org.powermock.core.transformers.TransformStrategy.INST_TRANSFORM; public abstract class AbstractMainMockTransformer implements MockTransformer { private static final String VOID = ""; private static final int METHOD_CODE_LENGTH_LIMIT = 65536; protected final TransformStrategy strategy; public AbstractMainMockTransformer(TransformStrategy strategy) {this.strategy = strategy;} public CtClass transform(final CtClass clazz) throws Exception { if (clazz.isFrozen()) { clazz.defrost(); } return transformMockClass(clazz); } protected abstract CtClass transformMockClass(CtClass clazz) throws CannotCompileException, NotFoundException; protected String allowMockingOfPackagePrivateClasses(final CtClass clazz) { final String name = clazz.getName(); if (strategy != INST_REDEFINE) { try { final int modifiers = clazz.getModifiers(); if (Modifier.isPackage(modifiers)) { if (!name.startsWith("java.") && !(clazz.isInterface() && clazz.getDeclaringClass() != null)) { clazz.setModifiers(Modifier.setPublic(modifiers)); } } } catch (NotFoundException e) { // OK, continue } } return name; } protected void suppressStaticInitializerIfRequested(final CtClass clazz, final String name) throws CannotCompileException { if (strategy == CLASSLOADER) { if (MockGateway.staticConstructorCall(name) != MockGateway.PROCEED) { CtConstructor classInitializer = clazz.makeClassInitializer(); classInitializer.setBody("{}"); } } } protected void removeFinalModifierFromClass(final CtClass clazz) { if (strategy != INST_REDEFINE) { if (Modifier.isFinal(clazz.getModifiers())) { clazz.setModifiers(clazz.getModifiers() ^ Modifier.FINAL); } ClassFile classFile = clazz.getClassFile2(); AttributeInfo attribute = classFile.getAttribute(InnerClassesAttribute.tag); if (attribute != null && attribute instanceof InnerClassesAttribute) { InnerClassesAttribute ica = (InnerClassesAttribute) attribute; String name = classFile.getName(); int n = ica.tableLength(); for (int i = 0; i < n; ++i) { if (name.equals(ica.innerClass(i))) { int accessFlags = ica.accessFlags(i); if (Modifier.isFinal(accessFlags)) { ica.setAccessFlags(i, accessFlags ^ Modifier.FINAL); } } } } } } protected void allowMockingOfStaticAndFinalAndNativeMethods(final CtClass clazz) throws NotFoundException, CannotCompileException { if (strategy != INST_TRANSFORM) { for (CtMethod m : clazz.getDeclaredMethods()) { modifyMethod(m); } } } protected void removeFinalModifierFromAllStaticFinalFields(final CtClass clazz) { if (strategy != INST_REDEFINE) { for (CtField f : clazz.getDeclaredFields()) { final int modifiers = f.getModifiers(); if (Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers)) { f.setModifiers(modifiers ^ Modifier.FINAL); } } } } protected void setAllConstructorsToPublic(final CtClass clazz) { if (strategy == CLASSLOADER) { for (CtConstructor c : clazz.getDeclaredConstructors()) { final int modifiers = c.getModifiers(); if (!Modifier.isPublic(modifiers)) { c.setModifiers(Modifier.setPublic(modifiers)); } } } } /** * According to JVM specification method size must be lower than 65536 bytes. * When that limit is exceeded class loader will fail to load the class. * Since instrumentation can increase method size significantly it must be * ensured that JVM limit is not exceeded. * <p/> * When the limit is exceeded method's body is replaced by exception throw. * Method is then instrumented again to allow mocking and suppression. * * @see <a href="http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.3">JVM specification</a> */ protected CtClass ensureJvmMethodSizeLimit(CtClass clazz) throws CannotCompileException, NotFoundException { for (CtMethod method : clazz.getDeclaredMethods()) { if (isMethodSizeExceeded(method)) { String code = "{throw new IllegalAccessException(\"" + "Method was too large and after instrumentation exceeded JVM limit. " + "PowerMock modified the method to allow JVM to load the class. " + "You can use PowerMock API to suppress or mock this method behaviour." + "\");}"; method.setBody(code); modifyMethod(method); } } return clazz; } private boolean isMethodSizeExceeded(CtMethod method) { CodeAttribute codeAttribute = method.getMethodInfo().getCodeAttribute(); return codeAttribute != null && codeAttribute.getCodeLength() >= METHOD_CODE_LENGTH_LIMIT; } private void modifyMethod(final CtMethod method) throws NotFoundException, CannotCompileException { if (!shouldSkipMethod(method)) { // Lookup the method return type final CtClass returnTypeAsCtClass = method.getReturnType(); final String returnTypeAsString = getReturnTypeAsString(method); if (Modifier.isNative(method.getModifiers())) { modifyNativeMethod(method, returnTypeAsCtClass, returnTypeAsString); } else { modifyMethod(method, returnTypeAsCtClass, returnTypeAsString); } } } private boolean shouldSkipMethod(CtMethod method) { return isAccessFlagSynthetic(method) || Modifier.isAbstract(method.getModifiers()); } private boolean isAccessFlagSynthetic(CtMethod method) { int accessFlags = method.getMethodInfo2().getAccessFlags(); return ((accessFlags & AccessFlag.SYNTHETIC) != 0) && !isBridgeMethod(method); } private void modifyMethod(CtMethod method, CtClass returnTypeAsCtClass, String returnTypeAsString) throws CannotCompileException { final String returnValue = getCorrectReturnValueType(returnTypeAsCtClass); String classOrInstance = classOrInstance(method); String code = "Object value = " + MockGateway.class.getName() + ".methodCall(" + classOrInstance + ", \"" + method.getName() + "\", $args, $sig, \"" + returnTypeAsString + "\");" + "if (value != " + MockGateway.class.getName() + ".PROCEED) " + "return " + returnValue + "; "; method.insertBefore("{ " + code + "}"); } private boolean isBridgeMethod(CtMethod method) {return (method.getMethodInfo2().getAccessFlags() & AccessFlag.BRIDGE) != 0;} private String classOrInstance(CtMethod method) { String classOrInstance = "this"; if (Modifier.isStatic(method.getModifiers())) { classOrInstance = "$class"; } return classOrInstance; } private void modifyNativeMethod(CtMethod method, CtClass returnTypeAsCtClass, String returnTypeAsString) throws CannotCompileException { String methodName = method.getName(); String returnValue = "($r)value"; if (returnTypeAsCtClass.equals(CtClass.voidType)) { returnValue = VOID; } String classOrInstance = classOrInstance(method); method.setModifiers(method.getModifiers() - Modifier.NATIVE); String code = "Object value = " + MockGateway.class.getName() + ".methodCall(" + classOrInstance + ", \"" + method.getName() + "\", $args, $sig, \"" + returnTypeAsString + "\");" + "if (value != " + MockGateway.class.getName() + ".PROCEED) " + "return " + returnValue + "; " + "throw new java.lang.UnsupportedOperationException(\"" + methodName + " is native\");"; method.setBody("{" + code + "}"); } private String getReturnTypeAsString(final CtMethod method) throws NotFoundException { CtClass returnType = method.getReturnType(); String returnTypeAsString = VOID; if (!returnType.equals(CtClass.voidType)) { returnTypeAsString = returnType.getName(); } return returnTypeAsString; } /** * @return The correct return type, i.e. takes care of casting the a wrapper * type to primitive type if needed. */ private String getCorrectReturnValueType(final CtClass returnTypeAsCtClass) { final String returnTypeAsString = returnTypeAsCtClass.getName(); final String returnValue; if (returnTypeAsCtClass.equals(CtClass.voidType)) { returnValue = VOID; } else if (returnTypeAsCtClass.isPrimitive()) { if (returnTypeAsString.equals("char")) { returnValue = "((java.lang.Character)value).charValue()"; } else if (returnTypeAsString.equals("boolean")) { returnValue = "((java.lang.Boolean)value).booleanValue()"; } else { returnValue = "((java.lang.Number)value)." + returnTypeAsString + "Value()"; } } else { returnValue = "(" + returnTypeAsString + ")value"; } return returnValue; } private boolean isNotSyntheticField(FieldInfo fieldInfo) { return (fieldInfo.getAccessFlags() & AccessFlag.SYNTHETIC) == 0; } protected final class PowerMockExpressionEditor extends ExprEditor { private final CtClass clazz; protected PowerMockExpressionEditor(CtClass clazz) { this.clazz = clazz; } @Override public void edit(FieldAccess f) throws CannotCompileException { if (f.isReader()) { CtClass returnTypeAsCtClass; FieldInfo fieldInfo; try { CtField field = f.getField(); returnTypeAsCtClass = field.getType(); fieldInfo = field.getFieldInfo2(); } catch (NotFoundException e) { /* * If multiple java agents are active (in INST_REDEFINE mode), the types implicitly loaded by javassist from disk * might differ from the types available in memory. Thus, this error might occur. * * It may also happen if PowerMock is modifying an SPI where the SPI require some classes to be available in the classpath * at runtime but they are not! This is valid in some cases such as slf4j. */ return; } if (isNotSyntheticField(fieldInfo)) { String code = "{Object value = " + MockGateway.class.getName() + ".fieldCall(" + "$0,$class,\"" + f.getFieldName() + "\",$type);" + "if(value == " + MockGateway.class.getName() + ".PROCEED) {" + " $_ = $proceed($$);" + "} else {" + " $_ = " + getCorrectReturnValueType(returnTypeAsCtClass) + ";" + "}}"; f.replace(code); } } } @Override public void edit(MethodCall m) throws CannotCompileException { try { final CtMethod method = m.getMethod(); final CtClass declaringClass = method.getDeclaringClass(); if (declaringClass != null) { if (shouldTreatAsSystemClassCall(declaringClass)) { StringBuilder code = new StringBuilder(); code.append("{Object classOrInstance = null; if($0!=null){classOrInstance = $0;} else { classOrInstance = $class;}"); code.append("Object value = ") .append(MockGateway.class.getName()) .append(".methodCall(") .append("classOrInstance,\"") .append(m.getMethodName()) .append("\",$args, $sig,\"") .append(getReturnTypeAsString(method)) .append("\");"); code.append("if(value == ").append(MockGateway.class.getName()).append(".PROCEED) {"); code.append(" $_ = $proceed($$);"); code.append("} else {"); final String correctReturnValueType = getCorrectReturnValueType(method.getReturnType()); if (!VOID.equals(correctReturnValueType)) { code.append(" $_ = ").append(correctReturnValueType).append(";"); } code.append("}}"); m.replace(code.toString()); } } } catch (NotFoundException e) { /* * If multiple java agents are active (in INST_REDEFINE mode), the types implicitly loaded by javassist from disk * might differ from the types available in memory. Thus, this error might occur. * * It may also happen if PowerMock is modifying an SPI where the SPI require some classes to be available in the classpath * at runtime but they are not! This is valid in some cases such as slf4j. */ } } private boolean shouldTreatAsSystemClassCall(CtClass declaringClass) { final String className = declaringClass.getName(); return className.startsWith("java."); } @Override public void edit(ConstructorCall c) throws CannotCompileException { /* * Note that constructor call only intercepts calls to super or this * from an instantiated class. This means that A a = new A(); will * NOT trigger a ConstructorCall for the default constructor in A. * If A where to extend B and A's constructor only delegates to * super(), the default constructor of B would trigger a * ConstructorCall. This means that we need to handle * "suppressConstructorCode" both here and in NewExpr. */ if (strategy != INST_REDEFINE && !c.getClassName().startsWith("java.lang")) { final CtClass superclass; try { superclass = clazz.getSuperclass(); } catch (NotFoundException e) { throw new RuntimeException(e); } /* * Create a default constructor in the super class if it doesn't * exist. This is needed because if the code in the current * constructor should be suppressed (which we don't know at this * moment of time) the parent class must have a default * constructor that we can delegate to. */ addNewDeferConstructor(clazz); final StringBuilder code = new StringBuilder(); code.append("{Object value =") .append(MockGateway.class.getName()) .append(".constructorCall($class, $args, $sig);"); code.append("if (value != ").append(MockGateway.class.getName()).append(".PROCEED){"); /* * TODO Suppress and lazy inject field (when this feature is ready). */ if (superclass.getName().equals(Object.class.getName())) { code.append(" super();"); } else { code.append(" super((").append(IndicateReloadClass.class.getName()).append(") null);"); } code.append("} else {"); code.append(" $proceed($$);"); code.append("}}"); c.replace(code.toString()); } } /** * Create a defer constructor in the class which will be called when the * constructor is suppressed. * * @param clazz The class whose super constructor will get a new defer * constructor if it doesn't already have one. * @throws CannotCompileException If an unexpected compilation error occurs. */ private void addNewDeferConstructor(final CtClass clazz) throws CannotCompileException { final CtClass superClass; try { superClass = clazz.getSuperclass(); } catch (NotFoundException e1) { throw new IllegalArgumentException("Internal error: Failed to get superclass for " + clazz.getName() + " when about to create a new default constructor."); } ClassPool classPool = clazz.getClassPool(); /* * To make a unique defer constructor we create a new constructor * with one argument (IndicateReloadClass). So we get this class a * Javassist class below. */ final CtClass constructorType; try { constructorType = classPool.get(IndicateReloadClass.class.getName()); } catch (NotFoundException e) { throw new IllegalArgumentException("Internal error: failed to get the " + IndicateReloadClass.class.getName() + " when added defer constructor."); } clazz.defrost(); if (superClass.getName().equals(Object.class.getName())) { try { clazz.addConstructor(CtNewConstructor.make(new CtClass[]{constructorType}, new CtClass[0], "{super();}", clazz)); } catch (DuplicateMemberException e) { // OK, the constructor has already been added. } } else { addNewDeferConstructor(superClass); try { clazz.addConstructor(CtNewConstructor.make(new CtClass[]{constructorType}, new CtClass[0], "{super($$);}", clazz)); } catch (DuplicateMemberException e) { // OK, the constructor has already been added. } } } @Override public void edit(NewExpr e) throws CannotCompileException { String code = "Object instance =" + MockGateway.class.getName() + ".newInstanceCall($type,$args,$sig);" + "if(instance != " + MockGateway.class.getName() + ".PROCEED) {" + " if(instance instanceof java.lang.reflect.Constructor) {" + " $_ = ($r) sun.reflect.ReflectionFactory.getReflectionFactory().newConstructorForSerialization($type, java.lang.Object.class.getDeclaredConstructor(null)).newInstance(null);" + " } else {" + " $_ = ($r) instance;" + " }" + "} else {" + " $_ = $proceed($$);" + "}"; // TODO Change to objenisis instead e.replace(code); } } }