com.qitsoft.qimono.internal.QimonoClassLoader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of qimono Show documentation
Show all versions of qimono Show documentation
The framework library base on Mockito used to stub methods with private visibility, final methods or static methods.
Also this library could be used to assign/retrieve values from private fields and to substitute date and time in tests.
The newest version!
/*
* Copyright (C) 2011 QitSoft Inc.
*
* This file is part of Qimono.
*
* Qimono is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*/
package com.qitsoft.qimono.internal;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.mockito.Mockito;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* The classloader of Qimono library where all of class instrumentation is performed.
* This class loader before loading the classes checks if the class should be instrumented
* and makes the required transformations. When the tests are run all of classes
* referenced by the test and the test itself is instrumented.
*
* The classloader do not instrument classes from java.*, sun.*, org.junit.* and org.hamcrest.*
* packages. The rest of classes are instrumented. Also additionally you can add the classes or packages
* to exclude from instrumenting by using {@link #addExcludeClass(java.lang.Class)},
* {@link #addExcludeClass(java.lang.String) } or {@link #addExcludePackage(java.lang.String) } methods.
*
* @author Serghei Soloviov
*/
public class QimonoClassLoader extends ClassLoader {
private List modifiers = new ArrayList();
private Set excludeClasses = new HashSet();
private Set includeClasses = new HashSet();
private List excludePackages = new ArrayList();
private List loadedClassListeners = new ArrayList();
private List syntheticClassModifiers = new ArrayList();
private Map syntheticRegistry = new Hashtable();
private Map unmockedSyntheticRegistry = new Hashtable();
private Class mockitoClass;
private Method spyMethod;
private Method resetMethod;
/**
* The constructor which accepts the parent classloader. If the parent class loaded
* is null it uses as parent classloader the classloader used to load this class.
*
* @param parentClassLoader the parent class loader.
*/
public QimonoClassLoader(ClassLoader parentClassLoader) {
super(parentClassLoader==null?QimonoClassLoader.class.getClassLoader():parentClassLoader);
}
/** The default constructor used to instantiate the classloader with the
* parent one as the class loader used to load this class.
*/
public QimonoClassLoader() {
super(QimonoClassLoader.class.getClassLoader());
}
/**
* The method used to add the {@link ClassModifier} object used to modify
* classes loaded by this classloader. If the modifier was added after some of
* classes were loaded those classes will not be reloaded and instrumented using
* the modifier.
*
* @param modifier the modifier used to modify (instrument) the loaded classes.
*/
public void addClassModifier(ClassModifier modifier) {
modifiers.add(modifier);
}
/**
* Returns the path to the class. It replaces the dots in full class name (with packages)
* with slashes and appends the ".class". This method is used to load the
* content of the class to be loaded for further instrumentation.
*
* @param name the full name of class to load.
* @return the string representing the path to the class resource.
*/
protected String getClassResourceName(String name) {
return name.replaceAll("\\.", "/") + ".class";
}
/**
* Checks is the class should be loaded by the parent classloader. This method
* returns true for classes from java.*, sun.*, org.junit.* or org.hamcrest.* packages.
* Also the classes from com.qitsoft.qimono.internal.* package and {@link QimonoJUnitRunner}
* are considered to be foreign (loaded by the parent classloader). The foreign class
* also is considered if it contains in the excluded package added by {@link #addExcludePackage(java.lang.String) }
* or class that matches the classes added by {@link #addExcludeClass(java.lang.Class) } or
* {@link #addExcludeClass(java.lang.String) } methods or class included with
* {@link #addIncludeClass(java.lang.Class) }.
* Firstly the method checks if the class is in default excluded packages (java.*, sun.* etc)
* and then it checks if the class was excluded using the above methods. So even if the
* class was marked to be included and the class is from java.* package, the class
* will be loaded by the parent class loader.
*
* @param name the name of class to check it it should be loaded by the parent class loader (is foreign).
* @return true if the class is foreign, otherwise returns false.
*
* @see #isFiltered(java.lang.String)
*/
protected boolean isForeignClass(String name) {
return name.startsWith("java.")
|| name.startsWith("sun.")
|| name.startsWith("org.junit.")
|| name.startsWith("org.hamcrest.")
|| name.startsWith("com.qitsoft.qimono.internal.")
|| name.startsWith("com.qitsoft.qimono.runners.QimonoJUnitRunner")
|| isFiltered(name);
}
/**
* Checks if the class to load is synthetic. The synthetic class is the class
* created runtime during loading of the real class. This classes are made by
* modifier added with {@link #addSyntheticClassModifier(com.qitsoft.qimono.internal.ClassModifier) }.
* The synthetic class always are in qimono.synthetic.* package.
*
* @param name the full name of class to check if it is synthetic.
* @return true if the class is synthetic.
*/
protected boolean isSyntheticClass(String name) {
return name.startsWith("qimono.synthetic.");
}
/**
* {@inheritDoc }
*/
@Override
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class clazz = findLoadedClass(name);
if (clazz == null) {
if (isForeignClass(name)) {
clazz = getParent().loadClass(name);
} else if (clazz == null) {
clazz = findClass(name);
}
}
if (clazz == null) {
if (!isSyntheticClass(name)) {
throw new ClassNotFoundException("Cannot find the class "+name);
} else {
return null;
}
}
if (resolve) {
resolveClass(clazz);
}
createAndRegisterSyntheticClass(name, clazz);
for(LoadedClassListener listener : loadedClassListeners) {
listener.loadedClass(clazz, this);
}
return clazz;
}
/**
* {@inheritDoc }
*/
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = null;
if (isSyntheticClass(name)) {
data = createSyntheticClass(name);
if (data == null) {
return null;
}
} else {
data = loadClassData(name);
if (!name.startsWith("org.mockito.")
&& !name.startsWith("org.objenesis.")) {
data = modifyClassData(data);
}
}
return defineClass(name, data, 0, data.length);
} catch (IOException ex) {
throw new ClassNotFoundException("Cannot find the class " + name, ex);
}
}
/**
* Returns the {@link org.mockito.Mockito} class. If the class were already loaded
* it returns the stored one.
*
* @return the Mockito class.
*/
protected Class getMockitoClass() {
if (mockitoClass == null) {
try {
mockitoClass = loadClass("org.mockito.Mockito");
} catch (ClassNotFoundException ex) {
return null;
}
}
return mockitoClass;
}
/**
* Returns the {@link org.mockito.Mockito#spy(java.lang.Object) } method.
*
* @return the {@link java.lang.reflect.Method} object of {@link org.mockito.Mockito#spy(java.lang.Object) } method.
*/
protected Method getSpyMethod() {
if (spyMethod == null) {
try {
spyMethod = getMockitoClass().getMethod("spy", Object.class);
} catch (NoSuchMethodException ex) {
return null;
} catch (SecurityException ex) {
return null;
}
}
return spyMethod;
}
/**
* Returns the {@link org.mockito.Mockito#reset(T[]) } method.
*
* @return the {@link java.lang.reflect.Method} object of {@link org.mockito.Mockito#reset(T[]) } method.
*/
protected Method getResetMethod() {
if (resetMethod == null) {
try {
resetMethod = getMockitoClass().getMethod("reset", Object[].class);
} catch (NoSuchMethodException ex) {
return null;
} catch (SecurityException ex) {
return null;
}
}
return resetMethod;
}
/**
* Performs object transformations according with {@link org.mockito.Mockito#spy(java.lang.Object) } method.
*
* @param obj the object to spy with Mockito.
* @return The spied object.
*
* @see org.mockito.Mockito#spy(java.lang.Object)
*/
protected Object spyObject(Object obj) {
try {
return getSpyMethod().invoke(getMockitoClass(), obj);
} catch (IllegalAccessException ex) {
return null;
} catch (IllegalArgumentException ex) {
return null;
} catch (InvocationTargetException ex) {
return null;
}
}
/**
* Performs reset of the object accordingly with {@link org.mockito.Mockito#reset(T[]) } method.
*
* @param obj the object to reset.
*
* @see org.mockito.Mockito#reset(T[])
*/
protected void resetObject(Object obj) {
try {
getResetMethod().invoke(getMockitoClass(), new Object[]{new Object[]{obj}});
} catch (IllegalAccessException ex) {
return;
} catch (IllegalArgumentException ex) {
return;
} catch (InvocationTargetException ex) {
return;
}
}
/**
* Checks if the class is a target for creating the synthetic class. The classes
* considered to have a synthetic class are those which aren't foreign, already synthetic
* or are in the org.mockito.* and org.objenesis.* packages.
* If there are no synthetic modifiers added to the class loader this method
* consider that every class cannot have a synthetic class.
*
* @param name the full name of class to check for possibility to have a synthetic class.
* @return true if the class could have a synthetic class. otherwise false.
*/
private boolean isSyntheticTargetClass(String name) {
return !syntheticClassModifiers.isEmpty()
&& !isForeignClass(name)
&& !isSyntheticClass(name)
&& !name.startsWith("org.mockito.")
&& !name.startsWith("org.objenesis.");
}
/**
* Instruments the class using class modifiers added before.
*
* @param data the raw content of the class.
* @return the modified content of class which further should be loaded by this classloader.
*/
private byte[] modifyClassData(byte[] data) {
if (modifiers.isEmpty()) {
return data;
}
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
for(int i=modifiers.size()-2;i>=0;i--) {
modifiers.get(i).setNextVisitor(modifiers.get(i+1));
modifiers.get(i).setClassLoader(this);
}
modifiers.get(modifiers.size()-1).setNextVisitor(classWriter);
modifiers.get(modifiers.size()-1).setClassLoader(this);
ClassReader classReader = new ClassReader(data);
classReader.accept(modifiers.get(0), 0);
return classWriter.toByteArray();
}
/**
* Performs loading of the raw content of the class. This class asks the resource
* with name obtained with {@link #getClassResourceName(java.lang.String) } from the
* parent constructor. If the class could not be found throws {@link java.io.IOException}
*
* @param name the full name of class to load its content.
* @return the raw content of the class requested.
* @throws IOException when the class cannot be located by the parent loader or the class
* cannot be loaded.
*/
private byte[] loadClassData(String name) throws IOException {
String path = getClassResourceName(name);
InputStream in = getResourceAsStream(path);
if (in == null) {
throw new IOException("Cannot locate class "+name);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int ch;
while( (ch = in.read()) >= 0 ) {
out.write(ch);
}
byte[] result = out.toByteArray();
out.close();
in.close();
return result;
}
/**
* Adds a class which should not be instrumented during load and should be loaded
* by the parent class loader.
*
* @param clazz the class to exclude from loading by this class loader.
*/
public void addExcludeClass(Class clazz) {
excludeClasses.add(clazz.getName());
}
/**
* Adds the class which should not be instrumented during load and should be loaded
* by the parent class loader.
*
* @param className the full name of class to exclude from loading by this class loader.
*/
public void addExcludeClass(String className) {
excludeClasses.add(className);
}
/**
* Adds the classes from the package which should be excluded from loading
* by this class loader and should not be instrumented.
*
* @param name the full name of package with classes which should be loaded by
* the parent class loader.
*/
public void addExcludePackage(String name) {
excludePackages.add(name+".");
}
/**
* Checks if the class is excluded from loading by this class loader. This
* method checks only by the rules added by {@link #addExcludeClass(java.lang.Class) },
* {@link #addExcludeClass(java.lang.String) }, {@link #addExcludePackage(java.lang.String) } or
* {@link #addIncludeClass(java.lang.Class) }.
* Firstly it checks if the class is marked to be included, then is checks if the class is
* excluded by {@link #addExcludeClass(java.lang.Class) } or {@link #addExcludeClass(java.lang.String) }
* and then if the class is in the excluded package.
*
* @param name the full name of class to check.
* @return true if the class is excluded from loading by this class loader.
*/
private boolean isFiltered(String name) {
if (includeClasses.contains(name)) {
return false;
}
if (excludeClasses.contains(name)) {
return true;
}
for(String pkg : excludePackages) {
if (name.startsWith(pkg)) {
return true;
}
}
return false;
}
/**
* The class marked to be included to be loaded and instrumented by this class loader.
* The classes added with this method have precedence before the classes marked to excluded.
*
* @param clazz the class to be included for load by the class loader.
*/
public void addIncludeClass(Class clazz) {
includeClasses.add(clazz.getName());
}
/**
* Returns the list of all class modifiers added before.
*
* @return the list with class modifiers. If there are no class modifiers added
* it will return an empty list.
*/
public List getClassModifiers() {
return modifiers;
}
/**
* Adds a listener when the class was loaded. There could be multiple listeners.
*
* @param loadedClassListener the listener which reacts when the class were loaded.
*/
public void addLoadedClassListener(LoadedClassListener loadedClassListener) {
loadedClassListeners.add(loadedClassListener);
}
/**
* Returns the list of listeners added by {@link #addLoadedClassListener(com.qitsoft.qimono.internal.LoadedClassListener) }
*
* @return the list with {@link LoadedClassListener} instances.
*/
public List getLoadedClassListeners() {
return loadedClassListeners;
}
/**
* Adds a modifier for creating synthetic class. This modifier
* added the required members to the class. It cannot be used to create the
* default constructor because the constructor is created before the
* modifier is invoked, it can only to modify it.
*
* @param modifier the {@link ClassModifier} for generation of syntehtic classes.
*/
public void addSyntheticClassModifier(ClassModifier modifier) {
syntheticClassModifiers.add(modifier);
}
/**
* Returns the list of syntehtic class modifiers.
*
* @return the list of {@link ClassModifier} used in creating synthetic classes.
*/
public List getSyntheticClassModifiers() {
return syntheticClassModifiers;
}
/**
* Creates the synthetic class of the class to be loaded. If the class should not
* have a synthetic class (sess {@link #isSyntheticTargetClass(java.lang.String)})
* {@literal null} is returned.
*
* @param name the full name of class to be as a target for synthetic class.
* @return the content of new synthetic class which further should be loaded by the class loader.
*/
private byte[] createSyntheticClass(String name) {
if (syntheticClassModifiers.isEmpty()) {
return null;
}
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
for(int i=syntheticClassModifiers.size()-2;i>=0;i--) {
syntheticClassModifiers.get(i).setNextVisitor(syntheticClassModifiers.get(i+1));
syntheticClassModifiers.get(i).setClassLoader(this);
}
WroteClassIndicatorAdapter wroteClassIndicatorAdapter = new WroteClassIndicatorAdapter(classWriter);
syntheticClassModifiers.get(syntheticClassModifiers.size()-1).setNextVisitor(wroteClassIndicatorAdapter);
syntheticClassModifiers.get(syntheticClassModifiers.size()-1).setClassLoader(this);
createSimpleSyntehticClass(name, syntheticClassModifiers.get(0));
if (wroteClassIndicatorAdapter.isWroteClass()) {
return classWriter.toByteArray();
} else {
return null;
}
}
/**
* Creates the synthetic class for the class. The generated synthetic class also
* is instantiated and registered in the internal registry for further mocking it.
*
* @param name the name of target for synthetic class
* @param clazz the loaded target class for synthetic class.
*/
private void createAndRegisterSyntheticClass(String name, Class clazz) {
if (isSyntheticTargetClass(name)) {
try {
Class syntheticClass = loadClass("qimono.synthetic."+name);
if (syntheticClass != null) {
Object syntheticObject = syntheticClass.newInstance();
unmockedSyntheticRegistry.put(clazz, syntheticObject);
}
} catch (Exception ex) {
Logger.getLogger(QimonoClassLoader.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* Creates base of synthetic class. It creates only the constructor for further
* modifications by the synthetic class modifiers.
*
* @param name the name of synthetic class.
* @param visitor the next visitor used to modify the synthetic class.
*/
private void createSimpleSyntehticClass(String name, ClassVisitor visitor) {
visitor.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, name.replaceAll("\\.", "/"), null, "java/lang/Object", null);
MethodVisitor initVisitor = visitor.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null);
if (initVisitor != null) {
initVisitor.visitCode();
initVisitor.visitVarInsn(Opcodes.ALOAD, 0);
initVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V");
initVisitor.visitInsn(Opcodes.RETURN);
initVisitor.visitMaxs(0, 0);
initVisitor.visitEnd();
}
visitor.visitEnd();
}
/**
* Returns the mock of the loaded synthetic class. This mock is created
* using {@link org.mockito.Mockito#spy(java.lang.Object) } method.
* If the class were not loaded and registered the method will return null.
*
* @param clazz the synthetic class whose spy should be obtained.
* @return the spy of synthetic class.
*/
public synchronized Object getSyntheticMock(Class clazz) {
Object obj = syntheticRegistry.get(clazz);
if (obj != null) {
return obj;
}
obj = unmockedSyntheticRegistry.get(clazz);
if (obj == null) {
return null;
}
obj = spyObject(obj);
if (obj == null) return null;
syntheticRegistry.put(clazz, obj);
unmockedSyntheticRegistry.remove(clazz);
return obj;
}
/**
* Resets the synthetic object's spies.
*/
public void resetSyntheticMocks() {
for(Object obj : syntheticRegistry.values()) {
resetObject(obj);
}
}
/**
* The class modifier used to generate synthetic classes and detects if the
* class was modified.
*/
private class WroteClassIndicatorAdapter extends ClassAdapter {
private boolean wroteClass = false;
/**
* Base constructor used to instantiate a modifier with the next modifier in the chain.
* @param cv the parent (next) class visitor in the chain.
*/
public WroteClassIndicatorAdapter(ClassVisitor cv) {
super(cv);
}
/**
* {@inheritDoc }
*/
@Override
public void visit(int i, int i1, String string, String string1, String string2, String[] strings) {
super.visit(i, i1, string, string1, string2, strings);
wroteClass = true;
}
/**
* Returns if the class were wrote (modified) by the next modifiers.
* @return true is the class were modified.
*/
public boolean isWroteClass() {
return wroteClass;
}
}
}