/*
* Copyright (c) 2006 Rogério Liesenfeld
* This file is subject to the terms of the MIT license (see LICENSE.txt).
*/
package mockit.internal.mockups;
import java.lang.reflect.*;
import java.util.*;
import javax.annotation.*;
import static java.lang.reflect.Modifier.*;
import mockit.internal.*;
import mockit.internal.reflection.*;
import mockit.internal.state.*;
import mockit.internal.util.*;
import mockit.internal.reflection.GenericTypeReflection.*;
import static mockit.internal.util.ObjectMethods.*;
/**
* A container for the mock methods "collected" from a mockup class, separated in two sets: one with all the mock
* methods, and another with just the subset of static methods.
*/
final class MockMethods
{
@Nonnull final Class<?> realClass;
private final boolean targetTypeIsAClass;
private final boolean reentrantRealClass;
@Nonnull private final List<MockMethod> methods;
@Nullable private MockMethod adviceMethod;
@Nonnull private final GenericTypeReflection typeParametersToTypeArguments;
@Nonnull private String mockClassInternalName;
@Nullable private List<MockState> mockStates;
final class MockMethod
{
private final int access;
@Nonnull final String name;
@Nonnull final String desc;
final boolean isAdvice;
final boolean hasInvocationParameter;
@Nonnull final String mockDescWithoutInvocationParameter;
private boolean hasMatchingRealMethod;
@Nullable private GenericSignature mockSignature;
private int indexForMockState;
private boolean nativeRealMethod;
private MockMethod(int access, @Nonnull String name, @Nonnull String desc)
{
this.access = access;
this.name = name;
this.desc = desc;
if (desc.contains("Lmockit/Invocation;")) {
hasInvocationParameter = true;
mockDescWithoutInvocationParameter = '(' + desc.substring(20);
isAdvice = "$advice".equals(name) && "()Ljava/lang/Object;".equals(mockDescWithoutInvocationParameter);
}
else {
hasInvocationParameter = false;
mockDescWithoutInvocationParameter = desc;
isAdvice = false;
}
hasMatchingRealMethod = false;
indexForMockState = -1;
}
boolean isMatch(int realAccess, @Nonnull String realName, @Nonnull String realDesc, @Nullable String signature)
{
if (name.equals(realName) && hasMatchingParameters(realDesc, signature)) {
hasMatchingRealMethod = true;
nativeRealMethod = isNative(realAccess);
return true;
}
return false;
}
private boolean hasMatchingParameters(@Nonnull String methodDesc, @Nullable String signature)
{
boolean sameParametersIgnoringGenerics = mockDescWithoutInvocationParameter.equals(methodDesc);
if (sameParametersIgnoringGenerics || signature == null) {
return sameParametersIgnoringGenerics;
}
if (mockSignature == null) {
mockSignature = typeParametersToTypeArguments.parseSignature(mockDescWithoutInvocationParameter);
}
return mockSignature.satisfiesGenericSignature(signature);
}
@Nonnull Class<?> getRealClass() { return realClass; }
@Nonnull String getMockNameAndDesc() { return name + desc; }
int getIndexForMockState() { return indexForMockState; }
boolean isStatic() { return Modifier.isStatic(access); }
boolean isPublic() { return Modifier.isPublic(access); }
boolean isForGenericMethod() { return mockSignature != null; }
boolean isForNativeMethod() { return nativeRealMethod; }
boolean requiresMockState() { return hasInvocationParameter || reentrantRealClass; }
boolean canBeReentered() { return targetTypeIsAClass && !nativeRealMethod; }
@Override @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
public boolean equals(Object obj)
{
MockMethod other = (MockMethod) obj;
return realClass == other.getRealClass() && name.equals(other.name) && desc.equals(other.desc);
}
@Override
public int hashCode()
{
return 31 * (31 * realClass.hashCode() + name.hashCode()) + desc.hashCode();
}
}
MockMethods(@Nonnull Class<?> realClass, @Nullable Type targetType)
{
this.realClass = realClass;
if (targetType == null || realClass == targetType) {
targetTypeIsAClass = true;
}
else {
Class<?> targetClass = Utilities.getClassType(targetType);
targetTypeIsAClass = !targetClass.isInterface();
}
reentrantRealClass = targetTypeIsAClass && MockingBridge.instanceOfClassThatParticipatesInClassLoading(realClass);
methods = new ArrayList<MockMethod>();
typeParametersToTypeArguments = new GenericTypeReflection(realClass, targetType);
mockClassInternalName = "";
}
@Nonnull Class<?> getRealClass() { return realClass; }
@Nullable
MockMethod addMethod(boolean fromSuperClass, int access, @Nonnull String name, @Nonnull String desc)
{
if (fromSuperClass && isMethodAlreadyAdded(name, desc)) {
return null;
}
MockMethod mockMethod = new MockMethod(access, name, desc);
if (mockMethod.isAdvice) {
adviceMethod = mockMethod;
}
else {
methods.add(mockMethod);
}
return mockMethod;
}
private boolean isMethodAlreadyAdded(@Nonnull String name, @Nonnull String desc)
{
int p = desc.lastIndexOf(')');
String params = desc.substring(0, p + 1);
for (MockMethod mockMethod : methods) {
if (mockMethod.name.equals(name) && mockMethod.desc.startsWith(params)) {
return true;
}
}
return false;
}
void addMockState(@Nonnull MockState mockState)
{
if (mockStates == null) {
mockStates = new ArrayList<MockState>(4);
}
mockState.mockMethod.indexForMockState = mockStates.size();
mockStates.add(mockState);
}
@Nullable List<MockState> getMockStates() { return mockStates; }
/**
* Finds a mock method with the same signature of a given real method, if previously collected from the mockup class.
* This operation can be performed only once for any given mock method in this container, so that after the last real
* method is processed there should be no mock methods left unused in the container.
*/
@Nullable
MockMethod findMethod(int access, @Nonnull String name, @Nonnull String desc, @Nullable String signature)
{
for (MockMethod mockMethod : methods) {
if (mockMethod.isMatch(access, name, desc, signature)) {
return mockMethod;
}
}
if (
adviceMethod != null && !isNative(access) && !"$init".equals(name) && !"$clinit".equals(name) &&
!isMethodFromObject(name, desc)
) {
return adviceMethod;
}
return null;
}
@Nonnull String getMockClassInternalName() { return mockClassInternalName; }
void setMockClassInternalName(@Nonnull String mockClassInternalName)
{
this.mockClassInternalName = mockClassInternalName.intern();
}
boolean hasUnusedMocks()
{
if (adviceMethod != null) {
return true;
}
for (MockMethod method : methods) {
if (!method.hasMatchingRealMethod) {
return true;
}
}
return false;
}
void registerMockStates(@Nonnull Object mockUp, boolean forStartupMock)
{
if (mockStates != null) {
MockStates allMockStates = TestRun.getMockStates();
if (forStartupMock) {
allMockStates.addStartupMockUpAndItsMockStates(mockUp, mockStates);
}
else {
allMockStates.addMockUpAndItsMockStates(mockUp, mockStates);
}
}
}
}