View Javadoc
1   /*
2    * Copyright (c) 2006 JMockit developers
3    * This file is subject to the terms of the MIT license (see LICENSE.txt).
4    */
5   package mockit.internal.expectations.mocking;
6   
7   import static java.lang.reflect.Modifier.ABSTRACT;
8   import static java.lang.reflect.Modifier.FINAL;
9   import static java.lang.reflect.Modifier.PRIVATE;
10  import static java.lang.reflect.Modifier.PROTECTED;
11  import static java.lang.reflect.Modifier.PUBLIC;
12  import static java.lang.reflect.Modifier.STATIC;
13  import static java.lang.reflect.Modifier.isNative;
14  
15  import static mockit.asm.jvmConstants.Access.ENUM;
16  import static mockit.asm.jvmConstants.Access.SYNTHETIC;
17  import static mockit.asm.jvmConstants.Opcodes.ACONST_NULL;
18  import static mockit.asm.jvmConstants.Opcodes.DUP;
19  import static mockit.asm.jvmConstants.Opcodes.IF_ACMPEQ;
20  import static mockit.asm.jvmConstants.Opcodes.POP;
21  import static mockit.internal.expectations.MockingFilters.validateAsMockable;
22  import static mockit.internal.util.ObjectMethods.isMethodFromObject;
23  import static mockit.internal.util.Utilities.HOTSPOT_VM;
24  
25  import edu.umd.cs.findbugs.annotations.NonNull;
26  import edu.umd.cs.findbugs.annotations.Nullable;
27  
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  
33  import mockit.asm.classes.ClassInfo;
34  import mockit.asm.classes.ClassReader;
35  import mockit.asm.controlFlow.Label;
36  import mockit.asm.methods.MethodVisitor;
37  import mockit.asm.types.JavaType;
38  import mockit.internal.BaseClassModifier;
39  import mockit.internal.expectations.ExecutionMode;
40  
41  final class MockedClassModifier extends BaseClassModifier {
42      private static final int METHOD_ACCESS_MASK = PRIVATE + SYNTHETIC + ABSTRACT;
43      private static final int PUBLIC_OR_PROTECTED = PUBLIC + PROTECTED;
44      private static final boolean NATIVE_UNSUPPORTED = !HOTSPOT_VM;
45  
46      private static final Map<String, String> FILTERS = new HashMap<>(4);
47      static {
48          FILTERS.put("java/lang/Object", "<init> clone getClass hashCode wait notify notifyAll ");
49          FILTERS.put("java/io/File", "compareTo equals getName getPath hashCode toString ");
50          FILTERS.put("java/util/logging/Logger", "<init> getName ");
51          FILTERS.put("java/util/jar/JarEntry", "<init> ");
52      }
53  
54      @Nullable
55      private final MockedType mockedType;
56      private String className;
57      private String methodSignature;
58      @Nullable
59      private String baseClassNameForCapturedInstanceMethods;
60      @NonNull
61      private ExecutionMode executionMode;
62      private boolean isProxy;
63      @Nullable
64      private String defaultFilters;
65      @Nullable
66      List<String> enumSubclasses;
67  
68      MockedClassModifier(@Nullable ClassLoader classLoader, @NonNull ClassReader classReader,
69              @Nullable MockedType typeMetadata) {
70          super(classReader);
71          mockedType = typeMetadata;
72          setUseClassLoadingBridge(classLoader);
73          executionMode = typeMetadata != null && typeMetadata.injectable ? ExecutionMode.PerInstance
74                  : ExecutionMode.Regular;
75      }
76  
77      void useDynamicMocking() {
78          executionMode = ExecutionMode.Partial;
79      }
80  
81      void setClassNameForCapturedInstanceMethods(@NonNull String internalClassName) {
82          baseClassNameForCapturedInstanceMethods = internalClassName;
83      }
84  
85      @Override
86      public void visit(int version, int access, @NonNull String name, @NonNull ClassInfo additionalInfo) {
87          validateMockingOfJREClass(name);
88  
89          super.visit(version, access, name, additionalInfo);
90          isProxy = "java/lang/reflect/Proxy".equals(additionalInfo.superName);
91  
92          if (isProxy) {
93              className = additionalInfo.interfaces[0];
94          } else {
95              className = name;
96              defaultFilters = FILTERS.get(name);
97          }
98  
99          if (baseClassNameForCapturedInstanceMethods != null) {
100             className = baseClassNameForCapturedInstanceMethods;
101         }
102     }
103 
104     private void validateMockingOfJREClass(@NonNull String internalName) {
105         if (internalName.startsWith("java/")) {
106             validateAsMockable(internalName);
107 
108             if (executionMode == ExecutionMode.Regular && mockedType != null && isFullMockingDisallowed(internalName)) {
109                 String modifyingClassName = internalName.replace('/', '.');
110 
111                 if (modifyingClassName.equals(mockedType.getClassType().getName())) {
112                     throw new IllegalArgumentException("Class " + modifyingClassName
113                             + " cannot be @Mocked fully; instead, use @Injectable or partial mocking");
114                 }
115             }
116         }
117     }
118 
119     private static boolean isFullMockingDisallowed(@NonNull String classDesc) {
120         return classDesc.startsWith("java/io/") && ("java/io/FileOutputStream".equals(classDesc)
121                 || "java/io/FileInputStream".equals(classDesc) || "java/io/FileWriter".equals(classDesc)
122                 || "java/io/PrintWriter java/io/Writer java/io/DataInputStream".contains(classDesc));
123     }
124 
125     @Override
126     public void visitInnerClass(@NonNull String name, @Nullable String outerName, @Nullable String innerName,
127             int access) {
128         cw.visitInnerClass(name, outerName, innerName, access);
129 
130         // The second condition is for classes compiled with Java 8 or older, which had a bug (as an anonymous class can
131         // never be static).
132         if (access == ENUM + FINAL || access == ENUM + STATIC) {
133             if (enumSubclasses == null) {
134                 enumSubclasses = new ArrayList<>();
135             }
136 
137             enumSubclasses.add(name);
138         }
139     }
140 
141     @Nullable
142     @Override
143     public MethodVisitor visitMethod(final int access, @NonNull final String name, @NonNull final String desc,
144             @Nullable final String signature, @Nullable String[] exceptions) {
145         if ((access & METHOD_ACCESS_MASK) != 0) {
146             return unmodifiedBytecode(access, name, desc, signature, exceptions);
147         }
148 
149         methodSignature = signature;
150 
151         if ("<init>".equals(name)) {
152             if (isConstructorNotAllowedByMockingFilters(name)) {
153                 return unmodifiedBytecode(access, name, desc, signature, exceptions);
154             }
155         } else {
156             if (stubOutFinalizeMethod(access, name, desc)) {
157                 return null;
158             }
159 
160             if ("<clinit>".equals(name)) {
161                 return stubOutClassInitializationIfApplicable(access);
162             }
163 
164             if (isMethodNotToBeMocked(access, name, desc) || isMethodNotAllowedByMockingFilters(access, name)) {
165                 return unmodifiedBytecode(access, name, desc, signature, exceptions);
166             }
167         }
168 
169         startModifiedMethodVersion(access, name, desc, signature, exceptions);
170 
171         if (isNative(methodAccess)) {
172             generateEmptyImplementation(methodDesc);
173             return methodAnnotationsVisitor;
174         }
175 
176         return copyOriginalImplementationWithInjectedInterceptionCode();
177     }
178 
179     @NonNull
180     private MethodVisitor unmodifiedBytecode(int access, @NonNull String name, @NonNull String desc,
181             @Nullable String signature, @Nullable String[] exceptions) {
182         return cw.visitMethod(access, name, desc, signature, exceptions);
183     }
184 
185     private boolean isConstructorNotAllowedByMockingFilters(@NonNull String name) {
186         return isProxy || executionMode != ExecutionMode.Regular || isUnmockableInvocation(name);
187     }
188 
189     private boolean isUnmockableInvocation(@NonNull String name) {
190         if (defaultFilters == null) {
191             return false;
192         }
193 
194         int i = defaultFilters.indexOf(name);
195         return i > -1 && defaultFilters.charAt(i + name.length()) == ' ';
196     }
197 
198     private boolean isMethodNotToBeMocked(int access, @NonNull String name, @NonNull String desc) {
199         return isNative(access) && (NATIVE_UNSUPPORTED || (access & PUBLIC_OR_PROTECTED) == 0)
200                 || (isProxy || executionMode == ExecutionMode.Partial) && (isMethodFromObject(name, desc)
201                         || "annotationType".equals(name) && "()Ljava/lang/Class;".equals(desc));
202     }
203 
204     @Nullable
205     private MethodVisitor stubOutClassInitializationIfApplicable(int access) {
206         startModifiedMethodVersion(access, "<clinit>", "()V", null, null);
207 
208         if (mockedType != null && mockedType.isClassInitializationToBeStubbedOut()) {
209             generateEmptyImplementation();
210             return null;
211         }
212 
213         return mw;
214     }
215 
216     private boolean stubOutFinalizeMethod(int access, @NonNull String name, @NonNull String desc) {
217         if ("finalize".equals(name) && "()V".equals(desc)) {
218             startModifiedMethodVersion(access, name, desc, null, null);
219             generateEmptyImplementation();
220             return true;
221         }
222 
223         return false;
224     }
225 
226     private boolean isMethodNotAllowedByMockingFilters(int access, @NonNull String name) {
227         return baseClassNameForCapturedInstanceMethods != null && (access & STATIC) != 0
228                 || executionMode.isMethodToBeIgnored(access) || isUnmockableInvocation(name);
229     }
230 
231     @Override
232     protected void generateInterceptionCode() {
233         if (useClassLoadingBridge) {
234             generateCallToHandlerThroughMockingBridge();
235         } else {
236             generateDirectCallToHandler(className, methodAccess, methodName, methodDesc, methodSignature,
237                     executionMode);
238         }
239 
240         generateDecisionBetweenReturningOrContinuingToRealImplementation();
241     }
242 
243     private void generateCallToHandlerThroughMockingBridge() {
244         generateCodeToObtainInstanceOfClassLoadingBridge(MockedBridge.MB);
245 
246         // First and second "invoke" arguments:
247         boolean isStatic = generateCodeToPassThisOrNullIfStaticMethod();
248         mw.visitInsn(ACONST_NULL);
249 
250         // Create array for call arguments (third "invoke" argument):
251         JavaType[] argTypes = JavaType.getArgumentTypes(methodDesc);
252         generateCodeToCreateArrayOfObject(6 + argTypes.length);
253 
254         int i = 0;
255         generateCodeToFillArrayElement(i, methodAccess);
256         i++;
257         generateCodeToFillArrayElement(i, className);
258         i++;
259         generateCodeToFillArrayElement(i, methodName);
260         i++;
261         generateCodeToFillArrayElement(i, methodDesc);
262         i++;
263         generateCodeToFillArrayElement(i, methodSignature);
264         i++;
265         generateCodeToFillArrayElement(i, executionMode.ordinal());
266         i++;
267 
268         generateCodeToFillArrayWithParameterValues(argTypes, i, isStatic ? 0 : 1);
269         generateCallToInvocationHandler();
270     }
271 
272     private void generateDecisionBetweenReturningOrContinuingToRealImplementation() {
273         Label startOfRealImplementation = new Label();
274         mw.visitInsn(DUP);
275         mw.visitLdcInsn(VOID_TYPE);
276         mw.visitJumpInsn(IF_ACMPEQ, startOfRealImplementation);
277         generateReturnWithObjectAtTopOfTheStack(methodDesc);
278         mw.visitLabel(startOfRealImplementation);
279         mw.visitInsn(POP);
280     }
281 }