All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.qitsoft.qimono.internal.QimonoClassLoader Maven / Gradle / Ivy

Go to download

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;
        }
        
    }


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy