org.unitils.mock.argumentmatcher.ArgumentMatcherPositionFinder.java Source code

Java tutorial

Introduction

Here is the source code for org.unitils.mock.argumentmatcher.ArgumentMatcherPositionFinder.java

Source

/*
 * Copyright 2008,  Unitils.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.unitils.mock.argumentmatcher;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import static org.objectweb.asm.Type.getMethodDescriptor;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.tree.analysis.*;
import org.unitils.core.UnitilsException;
import org.unitils.mock.annotation.ArgumentMatcher;
import org.unitils.mock.annotation.MatchStatement;
import org.unitils.mock.core.proxy.ProxyInvocation;
import static org.unitils.thirdparty.org.apache.commons.io.IOUtils.closeQuietly;
import static org.unitils.util.ReflectionUtils.getClassWithName;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Utility class for locating argument matchers in method invocations.
 *
 * @author Tim Ducheyne
 * @author Filip Neven
 * @author Kenny Claes
 */
public class ArgumentMatcherPositionFinder {

    /**
     * Locates the argument matchers for the given proxy method invocation.
     *
     * @param proxyInvocation The method invocation, not null
     * @param fromLineNr      The begin line-nr of the invocation
     * @param toLineNr        The end line-nr of the invocation (could be different from the begin line-nr if the invocation is written on more than 1 line)
     * @param index           The index of the matcher on that line, 1 for the first, 2 for the second etc
     * @return The argument indexes, empty if there are no matchers
     */
    public static List<Integer> getArgumentMatcherIndexes(ProxyInvocation proxyInvocation, int fromLineNr,
            int toLineNr, int index) {
        Class<?> testClass = getClassWithName(proxyInvocation.getInvokedAt().getClassName());
        String testMethodName = proxyInvocation.getInvokedAt().getMethodName();
        Method method = proxyInvocation.getMethod();

        return getArgumentMatcherIndexes(testClass, testMethodName, method, fromLineNr, toLineNr, index);
    }

    /**
     * Locates the argument matchers for the method invocation on the given line.
     * An exception is raised when the given method cannot be found.
     *
     * @param clazz         The class containing the method invocation, not null
     * @param methodName    The method containing the method invocation, not null
     * @param invokedMethod The invocation to look for, not null
     * @param fromLineNr    The begin line-nr of the invocation
     * @param toLineNr      The end line-nr of the invocation (could be different from the begin line-nr if the invocation is written on more than 1 line)
     * @param index         The index of the matcher on that line, 1 for the first, 2 for the second etc
     * @return The argument indexes, empty if there are no matchers
     */
    @SuppressWarnings({ "unchecked" })
    public static List<Integer> getArgumentMatcherIndexes(Class<?> clazz, String methodName, Method invokedMethod,
            int fromLineNr, int toLineNr, int index) {
        // read the bytecode of the test class
        ClassNode restClassNode = readClass(clazz);

        // find the correct test method
        List<MethodNode> testMethodNodes = restClassNode.methods;
        for (MethodNode testMethodNode : testMethodNodes) {

            // another method with the same name may exist
            // if no result was found it could be that the line nr was for the other method, so continue with the search
            if (methodName.equals(testMethodNode.name)) {
                List<Integer> result = findArgumentMatcherIndexes(restClassNode, testMethodNode, clazz, methodName,
                        invokedMethod, fromLineNr, toLineNr, index);
                if (result != null) {
                    return result;
                }
            }
        }
        throw new UnitilsException("Unable to find indexes of argument matcher. Method not found: " + methodName);
    }

    /**
     * Uses ASM to read the byte code of the given class. This will access the class file and create some sort
     * of DOM tree for the structure of the bytecode.
     *
     * @param clazz The class to read, not null
     * @return The structure of the class, not null
     */
    protected static ClassNode readClass(Class<?> clazz) {
        InputStream inputStream = null;
        try {
            inputStream = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".class");

            ClassReader classReader = new ClassReader(inputStream);
            ClassNode classNode = new ClassNode();
            classReader.accept(classNode, 0);
            return classNode;

        } catch (Exception e) {
            throw new UnitilsException("Unable to read class file for " + clazz, e);
        } finally {
            closeQuietly(inputStream);
        }
    }

    /**
     * Locates the argument matchers for the method invocation on the given line.
     *
     * @param classNode             The class containing the method invocation, not null
     * @param methodNode            The method containing the method invocation, not null
     * @param interpretedClass      The current class, not null
     * @param interpretedMethodName The current method name, not null
     * @param invokedMethod         The invocation to look for, not null
     * @param fromLineNr            The begin line-nr of the invocation
     * @param toLineNr              The end line-nr of the invocation (could be different from the begin line-nr if the invocation is written on more than 1 line)
     * @param index                 The index of the matcher on that line, 1 for the first, 2 for the second etc
     * @return The argument indexes, null if method was not found, empty if method found but there are no matchers
     */
    protected static List<Integer> findArgumentMatcherIndexes(ClassNode classNode, MethodNode methodNode,
            Class<?> interpretedClass, String interpretedMethodName, Method invokedMethod, int fromLineNr,
            int toLineNr, int index) {
        String invokedMethodName = invokedMethod.getName();
        String invokedMethodDescriptor = getMethodDescriptor(invokedMethod);
        try {
            // analyze the instructions in the method
            MethodInterpreter methodInterpreter = new MethodInterpreter(interpretedClass, interpretedMethodName,
                    invokedMethodName, invokedMethodDescriptor, fromLineNr, toLineNr, index);
            Analyzer analyzer = new MethodAnalyzer(methodNode, methodInterpreter);
            analyzer.analyze(classNode.name, methodNode);
            // retrieve the found matcher indexes, if any
            return methodInterpreter.getResultArgumentMatcherIndexes();

        } catch (AnalyzerException e) {
            if (e.getCause() instanceof UnitilsException) {
                throw (UnitilsException) e.getCause();
            }
            throw new UnitilsException(
                    "Unable to find argument matchers for method invocation. Method name: " + invokedMethodName
                            + ", method description; " + invokedMethodDescriptor + ", line nr; " + fromLineNr,
                    e);
        }
    }

    /**
     * Analyzer that passes the line nrs to the given interpreter.
     * By default an analyzer filters out the line number instructions. This analyzer intercepts these instructions and
     * sets the current line nr on the interpreter.
     */
    protected static class MethodAnalyzer extends Analyzer {

        /* The method to analyze */
        protected MethodNode methodNode;

        /* The interpreter to use during the analysis */
        protected MethodInterpreter methodInterpreter;

        /**
         * Creates an analyzer.
         *
         * @param methodNode        The method to analyze, not null
         * @param methodInterpreter The interpreter to use during the analysis, not null
         */
        public MethodAnalyzer(MethodNode methodNode, MethodInterpreter methodInterpreter) {
            super(methodInterpreter);
            this.methodNode = methodNode;
            this.methodInterpreter = methodInterpreter;
        }

        /**
         * Overridden to handle the line number instructions.
         *
         * @param instructionIndex     The current index
         * @param nextInstructionIndex The next index
         */
        protected void newControlFlowEdge(int instructionIndex, int nextInstructionIndex) {
            AbstractInsnNode insnNode = methodNode.instructions.get(instructionIndex);
            if (insnNode instanceof LineNumberNode) {
                LineNumberNode lineNumberNode = (LineNumberNode) insnNode;
                methodInterpreter.setCurrentLineNr(lineNumberNode.line);
            }
        }
    }

    /**
     * Interpreter that implements the argument matcher finder behavior.
     * The analyzer simulates the processing of instructions by the VM and calls methods on this class to determine the
     * result of the processing of an instruction. During this processing, the analyzer simulates the maintenance of
     * the operand stack. For example:
     * <p/>
     * Suppose you have following statement: 1 + 2
     * The analyzer will first simulate the instruction to load constant 1 on the operand stack,
     * then it does the same for constant 2, finally it simulates the sum instruction on both operands, removes both
     * operands from the operand stack and puts the result back on the stack.
     * <p/>
     * All these instructions will pass through this interpreter to determine the result values to put on the
     * operand stack.
     * <p/>
     * This interpreter works as follows to find the argument matchers: if a method call instruction is found that is
     * an argument matcher we return an ArugmentMatcherValue. For other instructions an ArugmentMatcherValue is returned if
     * one of its operands was a an ArugmentMatcherValue. When the actual invoked method is found, we then just
     * have to look at the operands: if one of the operands is an ArugmentMatcherValue, we've found the index of the argument matcher.
     * <p/>
     * For example:<br>
     * mock.methodCall(0, gt(2))   would give<br>
     * 1) load 0<br>
     * .... stack ( NotAnArgumentMatcherValue )<br>
     * 2) load 2<br>
     * .... stack ( NotAnArgumentMatcherValue, NotAnArgumentMatcherValue)<br>
     * 3) invoke argment matcher (pops last operand)<br>
     * .... stack ( NotAnArgumentMatcherValue, ArgumentMatcherValue)<br>
     * 4) invoke mock method using last 2 operands => we've found an argument matcher as second operand
     */
    protected static class MethodInterpreter extends BasicInterpreter {

        protected Class<?> interpretedClass;

        protected String interpretedMethodName;

        /* The name of the method to look for */
        protected String invokedMethodName;

        /* The signature of the method to look for */
        protected String invokedMethodDescriptor;

        /* The line nrs between which the invocation can be found */
        protected int fromLineNr, toLineNr;

        protected int index;

        /* The line that is currently being analyzed */
        protected int currentLineNr = 0;

        protected int currentIndex = 1;

        protected Method currentMatcherMethod;

        protected Set<MethodInsnNode> handledMethodInsnNodes = new HashSet<MethodInsnNode>();

        /* The resulting indexes or null if method was not found */
        protected List<Integer> resultArgumentMatcherIndexes;

        /**
         * Creates an interpreter.
         *
         * @param interpretedClass        The current class, not null
         * @param interpretedMethodName   The current method name, not null
         * @param invokedMethodName       The method to look for, not null
         * @param invokedMethodDescriptor The signature of the method to look for, not null
         * @param fromLineNr              The begin line-nr of the invocation
         * @param toLineNr                The end line-nr of the invocation (could be different from the begin line-nr if the invocation is written on more than 1 line)
         * @param index                   The index of the matcher on that line, 1 for the first, 2 for the second etc
         */
        public MethodInterpreter(Class<?> interpretedClass, String interpretedMethodName, String invokedMethodName,
                String invokedMethodDescriptor, int fromLineNr, int toLineNr, int index) {
            this.interpretedClass = interpretedClass;
            this.interpretedMethodName = interpretedMethodName;
            this.invokedMethodName = invokedMethodName;
            this.invokedMethodDescriptor = invokedMethodDescriptor;
            this.fromLineNr = fromLineNr;
            this.toLineNr = toLineNr;
            this.index = index;
        }

        /**
         * Gets the result after the analysis was performed.
         *
         * @return The argument indexes, null if method was not found, empty if method found but there are no matchers
         */
        public List<Integer> getResultArgumentMatcherIndexes() {
            return resultArgumentMatcherIndexes;
        }

        /**
         * Sets the line nr that is being analyzed.
         *
         * @param currentLineNr The line nr
         */
        public void setCurrentLineNr(int currentLineNr) {
            this.currentLineNr = currentLineNr;
        }

        @Override
        public BasicValue copyOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException {
            BasicValue resultValue = super.copyOperation(insn, value);
            return getValue(resultValue, value);
        }

        @Override
        public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException {
            BasicValue resultValue = super.unaryOperation(insn, value);
            return getValue(resultValue, value);
        }

        @Override
        public BasicValue binaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2)
                throws AnalyzerException {
            BasicValue resultValue = super.binaryOperation(insn, value1, value2);
            return getValue(resultValue, value1, value2);
        }

        @Override
        public BasicValue ternaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2,
                BasicValue value3) throws AnalyzerException {
            BasicValue resultValue = super.ternaryOperation(insn, value1, value2, value3);
            return getValue(resultValue, value1, value2, value3);
        }

        /**
         * Handles an instruction of a method call.
         *
         * @param instructionNode The instruction
         * @param values          The operands
         * @return The merged values or an ArugmentMatcherValue if an argument matcher method was found
         */
        @Override
        @SuppressWarnings({ "unchecked" })
        public BasicValue naryOperation(AbstractInsnNode instructionNode, List values) throws AnalyzerException {
            BasicValue resultValue = super.naryOperation(instructionNode, values);

            if (!(instructionNode instanceof MethodInsnNode)) {
                return getValue(resultValue, values);
            }

            // check whether we are interested in the instruction
            MethodInsnNode methodInsnNode = (MethodInsnNode) instructionNode;
            if (instructionOutOfRange() || instructionAlreadyHandled(methodInsnNode)) {
                return getValue(resultValue, values);
            }

            if (isInvokedMethod(methodInsnNode)) {
                if (currentIndex++ != index) {
                    return getValue(resultValue, values);
                }
                if (resultArgumentMatcherIndexes != null) {
                    throwUnitilsException(
                            "Method invocation occurs more than once within the same clause. Method name: "
                                    + invokedMethodName);
                }
                // we've found the method, now check which operands are argument matchers
                resultArgumentMatcherIndexes = getArgumentMatcherIndexes(methodInsnNode, values);
                currentMatcherMethod = null;
                return createArgumentMatcherValue(resultValue);
            }

            Method method = getMethod(methodInsnNode);
            if (method != null) {
                // check whether the method is a match statement
                if (isMatcherMethod(method)) {
                    currentMatcherMethod = method;
                }
                // check whether the method is an argument matcher (i.e. has the @ArgumentMatcher annotation)
                if (isArgumentMatcherMethod(method)) {
                    if (currentMatcherMethod == null) {
                        throwUnitilsException(
                                "An argument matcher cannot be used outside the context of a match statement.");
                    }
                    // we've found an argument matcher
                    return createArgumentMatcherValue(resultValue);
                }
            }

            // nothing special found
            return getValue(resultValue, values);
        }

        protected boolean instructionOutOfRange() {
            return currentLineNr < fromLineNr || toLineNr < currentLineNr;
        }

        protected boolean instructionAlreadyHandled(MethodInsnNode methodInsnNode) {
            return !handledMethodInsnNodes.add(methodInsnNode);
        }

        protected boolean isInvokedMethod(MethodInsnNode methodInsnNode) {
            return invokedMethodName.equals(methodInsnNode.name)
                    && invokedMethodDescriptor.equals(methodInsnNode.desc);
        }

        protected List<Integer> getArgumentMatcherIndexes(MethodInsnNode methodInsnNode, List values) {
            List<Integer> result = new ArrayList<Integer>();

            // for non-static invocations the first operand is always 'this'
            boolean isStatic = methodInsnNode.getOpcode() == INVOKESTATIC;
            for (int i = 0; i < values.size(); i++) {
                if (values.get(i) instanceof ArgumentMatcherValue) {
                    result.add(isStatic ? i : i - 1);
                }
            }
            return result;
        }

        protected boolean isMatcherMethod(Method method) {
            return method.getAnnotation(MatchStatement.class) != null;
        }

        protected boolean isArgumentMatcherMethod(Method method) {
            return method.getAnnotation(ArgumentMatcher.class) != null;
        }

        /**
         * Throws a {@link org.unitils.core.UnitilsException} with the given error message. The stacktrace is modified, to make
         * it point to the line of code that was analyzed by this class.
         *
         * @param errorMessage The error message
         */
        protected void throwUnitilsException(String errorMessage) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(errorMessage);
            stringBuilder.append("\n at ");
            stringBuilder.append(new StackTraceElement(interpretedClass.getName(), interpretedMethodName,
                    interpretedClass.getName(), currentLineNr).toString());
            stringBuilder.append("\n");
            throw new UnitilsException(stringBuilder.toString());
        }

        /**
         * Merges two values.
         *
         * @param value1 The first value
         * @param value2 The second value
         * @return The merged value
         */
        @Override
        public BasicValue merge(BasicValue value1, BasicValue value2) {
            BasicValue resultValue = super.merge(value1, value2);
            if (value1 instanceof ArgumentMatcherValue || value2 instanceof ArgumentMatcherValue) {
                return createArgumentMatcherValue(resultValue);
            }
            return resultValue;
        }

        /**
         * Finds a method using the ASM method node
         *
         * @param methodNode The ASM method node, not null
         * @return The method, null if not found
         */
        protected Method getMethod(MethodInsnNode methodNode) {
            String internalClassName = methodNode.owner;
            String className = internalClassName.replace('/', '.');
            String methodName = methodNode.name;
            String methodDescriptor = methodNode.desc;

            Class<?> clazz = getClassWithName(className);
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if (methodName.equals(method.getName()) && methodDescriptor.equals(getMethodDescriptor(method))) {
                    return method;
                }
            }
            return null;
        }

        /**
         * @param resultValue The result value
         * @param values      The values that can be ArgumentMatcherValues
         * @return The result value, or a ArgumentMatcherValue of the same type if one of the values is an ArgumentMatcherValue
         */
        protected BasicValue getValue(BasicValue resultValue, BasicValue... values) {
            if (values != null) {
                for (BasicValue value : values) {
                    if (value instanceof ArgumentMatcherValue) {
                        return createArgumentMatcherValue(resultValue);
                    }
                }
            }
            return resultValue;
        }

        /**
         * @param resultValue The result value
         * @param values      The values that can be ArgumentMatcherValues
         * @return The result value, or a ArgumentMatcherValue of the same type if one of the values is an ArgumentMatcherValue
         */
        protected BasicValue getValue(BasicValue resultValue, List<BasicValue> values) {
            int nrOfArgumentMatcherValues = getNrOfArgumentMacherValues(values);

            if (nrOfArgumentMatcherValues > 1) {
                throwUnitilsException("An argument matcher cannot be used in an expression.");
            }
            if (nrOfArgumentMatcherValues == 1) {
                return createArgumentMatcherValue(resultValue);
            }
            return resultValue;
        }

        /**
         * @param values The values that can be ArgumentMatcherValues
         * @return The nr of values that are an ArgumentMatcherValue
         */
        protected int getNrOfArgumentMacherValues(List<BasicValue> values) {
            if (values == null) {
                return 0;
            }

            int count = 0;
            for (BasicValue value : values) {
                if (value instanceof ArgumentMatcherValue) {
                    count++;
                }
            }
            return count;
        }

        /**
         * @param resultValue The result value
         * @return An ArgumentMatcherValue of the same type
         */
        protected ArgumentMatcherValue createArgumentMatcherValue(BasicValue resultValue) {
            if (resultValue == null) {
                return new ArgumentMatcherValue(null);
            }
            Type type = ((BasicValue) resultValue).getType();
            return new ArgumentMatcherValue(type);
        }
    }

    /**
     * A value representing a found argument matcher invocation
     */
    protected static class ArgumentMatcherValue extends BasicValue {

        public ArgumentMatcherValue(Type type) {
            super(type);
        }
    }

}