/*
* Copyright (c) 2006 Rogério Liesenfeld
* This file is subject to the terms of the MIT license (see LICENSE.txt).
*/
package mockit.internal.state;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.*;
import javax.annotation.*;
import static java.lang.reflect.Modifier.*;
import mockit.internal.*;
import mockit.internal.capturing.*;
import mockit.internal.expectations.mocking.*;
import mockit.internal.startup.*;
import mockit.internal.util.*;
import static mockit.internal.util.GeneratedClasses.*;
import static mockit.internal.util.Utilities.*;
/**
* Holds data about redefined real classes and their corresponding mock classes (if any), and provides methods to
* add/remove such state both from this instance and from other state holders with associated data.
*/
public final class MockFixture
{
/**
* Similar to {@code redefinedClasses}, but for classes modified by a {@code ClassFileTransformer} such as the
* {@code CaptureTransformer}, and containing the pre-transform bytecode instead of the modified one.
*/
@Nonnull private final Map<ClassIdentification, byte[]> transformedClasses;
/**
* Real classes currently redefined in the running JVM and their current (modified) bytecodes.
* <p/>
* The keys in the map allow each redefined real class to be later restored to a previous definition.
* <p/>
* The modified bytecode arrays in the map allow a new redefinition to be made on top of the current redefinition
* (in the case of the Mockups API), or to restore the class to a previous definition (provided the map is copied
* between redefinitions of the same class).
*/
@Nonnull private final Map<Class<?>, byte[]> redefinedClasses;
/**
* Subset of all currently redefined classes which contain one or more native methods.
* <p/>
* This is needed because in order to restore such methods it is necessary (for some classes) to re-register them
* with the JVM.
*
* @see #reregisterNativeMethodsForRestoredClass(Class)
*/
@Nonnull private final Set<String> redefinedClassesWithNativeMethods;
/**
* Maps redefined real classes to the internal name of the corresponding mock classes, when it's the case.
* <p/>
* This allows any global state associated to a mock class to be discarded when the corresponding real class is
* later restored to its original definition.
*/
@Nonnull private final Map<Class<?>, String> realClassesToMockClasses;
@Nonnull private final List<Class<?>> mockedClasses;
@Nonnull private final Map<Type, InstanceFactory> mockedTypesAndInstances;
@Nonnull private final List<CaptureTransformer<?>> captureTransformers;
public MockFixture()
{
transformedClasses = new HashMap<ClassIdentification, byte[]>(2);
redefinedClasses = new IdentityHashMap<Class<?>, byte[]>(8);
redefinedClassesWithNativeMethods = new HashSet<String>();
realClassesToMockClasses = new IdentityHashMap<Class<?>, String>(8);
mockedClasses = new ArrayList<Class<?>>();
mockedTypesAndInstances = new IdentityHashMap<Type, InstanceFactory>();
captureTransformers = new ArrayList<CaptureTransformer<?>>();
}
// Methods to add/remove transformed/redefined classes /////////////////////////////////////////////////////////////
public void addTransformedClass(@Nonnull ClassIdentification classId, @Nonnull byte[] pretransformClassfile)
{
transformedClasses.put(classId, pretransformClassfile);
}
public void addRedefinedClass(
@Nullable String mockClassInternalName, @Nonnull Class<?> redefinedClass, @Nonnull byte[] modifiedClassfile)
{
if (mockClassInternalName != null) {
String previousNames = realClassesToMockClasses.put(redefinedClass, mockClassInternalName);
if (previousNames != null) {
realClassesToMockClasses.put(redefinedClass, previousNames + ' ' + mockClassInternalName);
}
}
addRedefinedClass(redefinedClass, modifiedClassfile);
}
public void addRedefinedClass(@Nonnull Class<?> redefinedClass, @Nonnull byte[] modifiedClassfile)
{
redefinedClasses.put(redefinedClass, modifiedClassfile);
}
public void registerMockedClass(@Nonnull Class<?> mockedType)
{
if (!isMockedClass(mockedType)) {
if (Proxy.isProxyClass(mockedType)) {
mockedType = mockedType.getInterfaces()[0];
}
mockedClasses.add(mockedType);
}
}
public boolean isStillMocked(@Nullable Object instance, @Nonnull String classDesc)
{
Class<?> targetClass;
if (instance == null) {
targetClass = ClassLoad.loadByInternalName(classDesc);
return isClassAssignableTo(mockedClasses, targetClass);
}
targetClass = instance.getClass();
return mockedTypesAndInstances.containsKey(targetClass) || isInstanceOfMockedClass(instance);
}
public boolean isMockedClass(@Nonnull Class<?> targetClass)
{
int n = mockedClasses.size();
for (int i = 0; i < n; i++) {
Class<?> mockedClass = mockedClasses.get(i);
if (mockedClass == targetClass) {
return true;
}
}
return false;
}
public boolean isInstanceOfMockedClass(@Nonnull Object mockedInstance)
{
Class<?> mockedClass = mockedInstance.getClass();
return findClassAssignableFrom(mockedClasses, mockedClass) != null;
}
public void registerInstanceFactoryForMockedType(
@Nonnull Class<?> mockedType, @Nonnull InstanceFactory mockedInstanceFactory)
{
registerMockedClass(mockedType);
mockedTypesAndInstances.put(mockedType, mockedInstanceFactory);
}
@Nullable
public InstanceFactory findInstanceFactory(@Nonnull Type mockedType)
{
InstanceFactory instanceFactory = mockedTypesAndInstances.get(mockedType);
if (instanceFactory != null) {
return instanceFactory;
}
Class<?> mockedClass = getClassType(mockedType);
//noinspection ReuseOfLocalVariable
instanceFactory = mockedTypesAndInstances.get(mockedClass);
if (instanceFactory != null) {
return instanceFactory;
}
boolean abstractType = mockedClass.isInterface() || isAbstract(mockedClass.getModifiers());
for (Entry<Type, InstanceFactory> entry : mockedTypesAndInstances.entrySet()) {
Type registeredMockedType = entry.getKey();
Class<?> registeredMockedClass = getClassType(registeredMockedType);
if (abstractType) {
registeredMockedClass = getMockedClassOrInterfaceType(registeredMockedClass);
}
if (mockedClass.isAssignableFrom(registeredMockedClass)) {
instanceFactory = entry.getValue();
break;
}
}
return instanceFactory;
}
private void restoreDefinition(@Nonnull Class<?> redefinedClass)
{
if (redefinedClassesWithNativeMethods.contains(redefinedClass.getName())) {
reregisterNativeMethodsForRestoredClass(redefinedClass);
}
removeMockedClass(redefinedClass);
}
private void removeMockedClass(@Nonnull Class<?> mockedClass)
{
mockedTypesAndInstances.remove(mockedClass);
mockedClasses.remove(mockedClass);
}
private void discardStateForCorrespondingMockClassIfAny(@Nonnull Class<?> redefinedClass)
{
String mockClassesInternalNames = realClassesToMockClasses.remove(redefinedClass);
TestRun.getMockStates().removeClassState(redefinedClass, mockClassesInternalNames);
}
void restoreTransformedClasses(@Nonnull Set<ClassIdentification> previousTransformedClasses)
{
if (!transformedClasses.isEmpty()) {
Set<ClassIdentification> classesToRestore;
if (previousTransformedClasses.isEmpty()) {
classesToRestore = transformedClasses.keySet();
}
else {
classesToRestore = getTransformedClasses();
classesToRestore.removeAll(previousTransformedClasses);
}
if (!classesToRestore.isEmpty()) {
restoreAndRemoveTransformedClasses(classesToRestore);
}
}
}
private void restoreAndRemoveTransformedClasses(@Nonnull Set<ClassIdentification> classesToRestore)
{
RedefinitionEngine redefinitionEngine = new RedefinitionEngine();
for (ClassIdentification transformedClassId : classesToRestore) {
byte[] definitionToRestore = transformedClasses.get(transformedClassId);
redefinitionEngine.restoreToDefinition(transformedClassId.getLoadedClass(), definitionToRestore);
}
transformedClasses.keySet().removeAll(classesToRestore);
}
void restoreRedefinedClasses(@Nonnull Map<?, byte[]> previousDefinitions)
{
if (redefinedClasses.isEmpty()) {
return;
}
RedefinitionEngine redefinitionEngine = new RedefinitionEngine();
Iterator<Entry<Class<?>, byte[]>> itr = redefinedClasses.entrySet().iterator();
while (itr.hasNext()) {
Entry<Class<?>, byte[]> entry = itr.next();
Class<?> redefinedClass = entry.getKey();
byte[] currentDefinition = entry.getValue();
byte[] previousDefinition = previousDefinitions.get(redefinedClass);
//noinspection ArrayEquality
if (currentDefinition != previousDefinition) {
redefinitionEngine.restoreDefinition(redefinedClass, previousDefinition);
if (previousDefinition == null) {
restoreDefinition(redefinedClass);
discardStateForCorrespondingMockClassIfAny(redefinedClass);
itr.remove();
}
else {
entry.setValue(previousDefinition);
}
}
}
}
void removeMockedClasses(@Nonnull List<Class<?>> previousMockedClasses)
{
int currentMockedClassCount = mockedClasses.size();
if (currentMockedClassCount > 0) {
int previousMockedClassCount = previousMockedClasses.size();
if (previousMockedClassCount == 0) {
mockedClasses.clear();
mockedTypesAndInstances.clear();
}
else if (previousMockedClassCount < currentMockedClassCount) {
mockedClasses.retainAll(previousMockedClasses);
mockedTypesAndInstances.keySet().retainAll(previousMockedClasses);
}
}
}
// Methods that deal with redefined native methods /////////////////////////////////////////////////////////////////
public void addRedefinedClassWithNativeMethods(@Nonnull String redefinedClassInternalName)
{
redefinedClassesWithNativeMethods.add(redefinedClassInternalName.replace('/', '.'));
}
private static void reregisterNativeMethodsForRestoredClass(@Nonnull Class<?> realClass)
{
Method registerNatives = null;
try {
registerNatives = realClass.getDeclaredMethod("registerNatives");
}
catch (NoSuchMethodException ignore) {
try { registerNatives = realClass.getDeclaredMethod("initIDs"); }
catch (NoSuchMethodException ignored) {} // OK
}
if (registerNatives != null) {
try {
registerNatives.setAccessible(true);
registerNatives.invoke(null);
}
catch (IllegalAccessException ignore) {} // won't happen
catch (InvocationTargetException ignore) {} // shouldn't happen either
}
// OK, although another solution will be required for this particular class if it requires
// natives to be explicitly registered again (not all do, such as java.lang.Float).
}
// Getter methods for the maps and collections of transformed/redefined/mocked classes /////////////////////////////
@Nonnull
Set<ClassIdentification> getTransformedClasses()
{
return transformedClasses.isEmpty() ?
Collections.<ClassIdentification>emptySet() :
new HashSet<ClassIdentification>(transformedClasses.keySet());
}
@Nonnull
Map<Class<?>, byte[]> getRedefinedClasses()
{
return redefinedClasses.isEmpty() ?
Collections.<Class<?>, byte[]>emptyMap() :
new HashMap<Class<?>, byte[]>(redefinedClasses);
}
@Nullable
public byte[] getRedefinedClassfile(@Nonnull Class<?> redefinedClass)
{
return redefinedClasses.get(redefinedClass);
}
public boolean containsRedefinedClass(@Nonnull Class<?> redefinedClass)
{
return redefinedClasses.containsKey(redefinedClass);
}
@Nonnull
public List<Class<?>> getMockedClasses()
{
return mockedClasses.isEmpty() ? Collections.<Class<?>>emptyList() : new ArrayList<Class<?>>(mockedClasses);
}
// Methods dealing with capture transformers ///////////////////////////////////////////////////////////////////////
public void addCaptureTransformer(@Nonnull CaptureTransformer<?> transformer)
{
captureTransformers.add(transformer);
}
public int getCaptureTransformerCount() { return captureTransformers.size(); }
public void removeCaptureTransformers(int previousTransformerCount)
{
int currentTransformerCount = captureTransformers.size();
for (int i = currentTransformerCount - 1; i >= previousTransformerCount; i--) {
CaptureTransformer<?> transformer = captureTransformers.get(i);
transformer.deactivate();
Startup.instrumentation().removeTransformer(transformer);
captureTransformers.remove(i);
}
}
@Nullable
public CaptureOfNewInstances findCaptureOfImplementations(@Nonnull Class<?> capturedType)
{
for (CaptureTransformer<?> captureTransformer : captureTransformers) {
CaptureOfNewInstances capture = captureTransformer.getCaptureOfImplementationsIfApplicable(capturedType);
if (capture != null) {
return capture;
}
}
return null;
}
public boolean isCaptured(@Nonnull Object mock)
{
Class<?> mockedClass = getMockedClass(mock);
CaptureOfNewInstances capture = findCaptureOfImplementations(mockedClass);
return capture != null;
}
public boolean areCapturedClasses(@Nonnull Class<?> mockedClass1, @Nonnull Class<?> mockedClass2)
{
for (CaptureTransformer<?> captureTransformer : captureTransformers) {
if (captureTransformer.areCapturedClasses(mockedClass1, mockedClass2)) {
return true;
}
}
return false;
}
}