org.omnaest.utils.proxy.MethodCallCapturer.java Source code

Java tutorial

Introduction

Here is the source code for org.omnaest.utils.proxy.MethodCallCapturer.java

Source

/*******************************************************************************
 * Copyright 2011 Danny Kunz
 * 
 * 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.omnaest.utils.proxy;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

import net.sf.cglib.proxy.MethodInterceptor;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.omnaest.utils.beans.BeanUtils;
import org.omnaest.utils.beans.result.BeanMethodInformation;
import org.omnaest.utils.proxy.handler.MethodCallCapture;
import org.omnaest.utils.proxy.handler.MethodInvocationHandler;
import org.omnaest.utils.structure.collection.list.ListUtils;
import org.omnaest.utils.structure.element.converter.ElementConverter;

/**
 * A {@link MethodCallCapturer} allows to create stubs for given java types which capture the calls of methods of this stub.
 * 
 * @see MethodName
 * @see BeanProperty
 * @see #methodName
 * @see #beanProperty
 * @see #newInstanceOfCapturedType(Class)
 * @see #newInstanceOfCapturedTypeWhichIsMethodCallCapturerAware(Class)
 * @see #newInstanceOfTransitivlyCapturedType(Class)
 * @see #newInstanceOfTransitivlyCapturedTypeWhichIsMethodCallCapturerAware(Class)
 * @see #getMethodCallCaptureContextList()
 * @see #getCanonicalMethodNameToMethodCallCaptureMap()
 * @see MethodCallCapture
 * @author Omnaest
 */
public class MethodCallCapturer {
    /* ********************************************** Constants ********************************************** */
    public final MethodName methodName = new MethodName(this);
    public final BeanProperty beanProperty = new BeanProperty(this);

    /* ********************************************** Variables ********************************************** */
    protected final Map<Object, List<MethodCallCaptureContext>> stubToMethodCallCaptureContextListMap = Collections
            .synchronizedMap(new IdentityHashMap<Object, List<MethodCallCaptureContext>>());
    protected Object lastActiveRootStub = null;

    /* ********************************************** Classes/Interfaces ********************************************** */

    /**
     * This interface stubs are implementing when they are created by
     * {@link MethodCallCapturer#newInstanceOfCapturedTypeWhichIsMethodCallCapturerAware(Class)}.
     * 
     * @see MethodCallCapturer
     */
    public static interface MethodCallCapturerAware {
        /**
         * Returns the underlying {@link MethodCallCapturer}
         * 
         * @return
         */
        public MethodCallCapturer getMethodCallCapturer();
    }

    /**
     * This interface stubs are always implementing when they are created by
     * {@link MethodCallCapturer#newInstanceOfCapturedType(Class)}.
     * 
     * @author Omnaest
     */
    public static interface TypeCaptureAware {
        /**
         * Returns the captured type
         * 
         * @return
         */
        public Class<?> getCapturedType();
    }

    /**
     * {@link MethodInterceptor} for the {@link MethodCallCapturer}.
     * 
     * @author Omnaest
     */
    protected class MethodCaptureMethodInvocationHandler implements MethodInvocationHandler {
        /* ********************************************** Variables ********************************************** */
        protected CapturedTypeInstanceCreationConfiguration capturedTypeInstanceCreationConfiguration = null;

        /* ********************************************** Methods ********************************************** */

        public MethodCaptureMethodInvocationHandler(
                CapturedTypeInstanceCreationConfiguration capturedTypeInstanceCreationConfiguration) {
            this.capturedTypeInstanceCreationConfiguration = capturedTypeInstanceCreationConfiguration;
        }

        @Override
        public Object handle(MethodCallCapture methodCallCapture) {
            //
            Object retval = null;

            //
            final String methodName = methodCallCapture.getMethod().getName();
            if (this.capturedTypeInstanceCreationConfiguration.isMethodCallCapturerAware()
                    && "getMethodCallCapturer".equals(methodName)) {
                //
                retval = MethodCallCapturer.this;
            } else if ("getCapturedType".equals(methodName)) {
                //
                retval = this.capturedTypeInstanceCreationConfiguration.getType();
            } else if ("toString".equals(methodName)) {
                //
                retval = "Capturer proxy for "
                        + String.valueOf(this.capturedTypeInstanceCreationConfiguration.getType());
            } else {
                Method method = methodCallCapture.getMethod();
                if (method != null) {
                    //
                    MethodCallCaptureContext methodCallCaptureContext = new MethodCallCaptureContext(
                            methodCallCapture,
                            this.capturedTypeInstanceCreationConfiguration.getPreviousMethodCallCaptureContext());
                    if (this.capturedTypeInstanceCreationConfiguration.isCreatingTransitiveStubs()) {
                        Class<?> returnType = method.getReturnType();
                        if (returnType != null && Object.class.isAssignableFrom(returnType)) {
                            //
                            CapturedTypeInstanceCreationConfiguration capturedTypeInstanceCreationConfiguration = new CapturedTypeInstanceCreationConfiguration(
                                    returnType, this.capturedTypeInstanceCreationConfiguration.getInterfaces(),
                                    this.capturedTypeInstanceCreationConfiguration.isCreatingTransitiveStubs());
                            capturedTypeInstanceCreationConfiguration
                                    .setPreviousMethodCallCaptureContext(methodCallCaptureContext);

                            //
                            retval = MethodCallCapturer.this
                                    .newInstanceOfCapturedType(capturedTypeInstanceCreationConfiguration);

                            // 
                            methodCallCaptureContext.setReturnedStub(retval);

                        }
                    }

                    //
                    MethodCallCapturer.this.addMethodCallCapture(methodCallCaptureContext);
                }
            }

            // 
            return retval;
        }
    }

    /**
     * Configuration object when a {@link MethodCallCapturer} creates a new stub instance.
     * 
     * @see MethodCallCapturer#newInstanceOfCapturedType(CapturedTypeInstanceCreationConfiguration)
     * @author Omnaest
     */
    protected static class CapturedTypeInstanceCreationConfiguration {
        /* ********************************************** Variables ********************************************** */
        protected Class<?> type = null;
        protected Class<?>[] interfaces = null;
        protected boolean isCreatingTransitiveStubs = false;
        protected MethodCallCaptureContext previousMethodCallCaptureContext = null;

        /* ********************************************** Methods ********************************************** */

        public CapturedTypeInstanceCreationConfiguration(Class<?> type, Class<?>[] interfaces,
                boolean isCreatingTransitiveStubs) {
            super();
            this.type = type;
            this.interfaces = interfaces;
            this.isCreatingTransitiveStubs = isCreatingTransitiveStubs;
        }

        /**
         * @return
         */
        public Class<?> getType() {
            return this.type;
        }

        /**
         * @param type
         */
        public void setType(Class<?> type) {
            this.type = type;
        }

        /**
         * @return
         */
        public Class<?>[] getInterfaces() {
            return this.interfaces;
        }

        /**
         * @param interfaces
         */
        public void setInterfaces(Class<?>[] interfaces) {
            this.interfaces = interfaces;
        }

        /**
         * @return
         */
        public boolean isCreatingTransitiveStubs() {
            return this.isCreatingTransitiveStubs;
        }

        /**
         * @param isCreatingTransitiveStubs
         */
        public void setCreatingTransitiveStubs(boolean isCreatingTransitiveStubs) {
            this.isCreatingTransitiveStubs = isCreatingTransitiveStubs;
        }

        /**
         * Returns true, if the stub implements the {@link MethodCallCapturerAware} interface.
         * 
         * @return
         */
        protected boolean isMethodCallCapturerAware() {
            return this.interfaces != null && ArrayUtils.contains(this.interfaces, MethodCallCapturerAware.class);
        }

        protected MethodCallCaptureContext getPreviousMethodCallCaptureContext() {
            return this.previousMethodCallCaptureContext;
        }

        protected void setPreviousMethodCallCaptureContext(
                MethodCallCaptureContext previousMethodCallCaptureContext) {
            this.previousMethodCallCaptureContext = previousMethodCallCaptureContext;
        }

    }

    /**
     * Container class for the absolute canonical method name when a {@link MethodCallCapture} instance has been captured.
     * 
     * @see MethodCallCapturer
     * @author Omnaest
     */
    public static class MethodCallCaptureContext {
        /* ********************************************** Variables ********************************************** */
        protected MethodCallCapture methodCallCapture = null;
        protected MethodCallCaptureContext previousMethodCallCaptureContext = null;
        protected Object returnedStub = null;

        /* ********************************************** Methods ********************************************** */

        public MethodCallCaptureContext(MethodCallCapture methodCallCapture,
                MethodCallCaptureContext previousMethodCallCaptureContext) {
            super();
            this.methodCallCapture = methodCallCapture;
            this.previousMethodCallCaptureContext = previousMethodCallCaptureContext;
        }

        public MethodCallCapture getMethodCallCapture() {
            return this.methodCallCapture;
        }

        /**
         * Returns the canonical list of {@link MethodCallCapture}s from the given root stub as base.
         * 
         * @param rootStub
         * @return
         */
        public List<MethodCallCapture> determineCanonicalMethodCallCaptures(Object rootStub) {
            //
            List<MethodCallCapture> retlist = new ArrayList<MethodCallCapture>();

            //
            if (this.previousMethodCallCaptureContext == null) {
                //
                retlist.add(this.methodCallCapture);
            } else if (this.getStub() == rootStub) {
            } else {
                //
                retlist.addAll(
                        this.previousMethodCallCaptureContext.determineCanonicalMethodCallCaptures(rootStub));
                retlist.add(this.methodCallCapture);
            }

            //
            return retlist;
        }

        /**
         * Returns the canonical method name relative to the given root stub object
         * 
         * @return
         */
        public String determineCanonicalMethodName(Object rootStub) {
            //
            String retval = null;

            //
            List<MethodCallCapture> canonicalMethodCallCaptureList = this
                    .determineCanonicalMethodCallCaptures(rootStub);
            for (MethodCallCapture methodCallCapture : canonicalMethodCallCaptureList) {
                //
                String methodName = methodCallCapture.getMethodName();

                //
                retval = retval == null ? methodName : retval + "." + methodName;
            }

            //
            return retval;
        }

        /**
         * Returns the canonical property name relative to the given root stub object
         * 
         * @return
         */
        public String determineCanonicalPropertyName(Object rootStub) {
            //
            String retval = null;

            //
            List<MethodCallCapture> canonicalMethodCallCaptureList = this
                    .determineCanonicalMethodCallCaptures(rootStub);
            for (MethodCallCapture methodCallCapture : canonicalMethodCallCaptureList) {
                //
                Method method = methodCallCapture.getMethod();
                if (method != null) {
                    //        
                    BeanMethodInformation beanMethodInformation = BeanUtils.beanMethodInformation(method);
                    if (beanMethodInformation != null) {
                        //
                        String referencedFieldName = beanMethodInformation.getPropertyName();

                        //
                        retval = retval == null ? referencedFieldName : retval + "." + referencedFieldName;
                    }
                }
            }

            //
            return retval;
        }

        /**
         * Returns the stub for which this {@link MethodCallCaptureContext} was created for one of its methods called.
         * 
         * @return
         */
        public Object getStub() {
            return this.methodCallCapture != null ? this.methodCallCapture.getObject() : null;
        }

        /**
         * Returns the stub objects which have been invoked to produce this {@link MethodCallCaptureContext}.
         * 
         * @return
         */
        public List<Object> getStubList() {
            //
            List<Object> retlist = new ArrayList<Object>();

            //
            if (this.previousMethodCallCaptureContext != null) {
                retlist.addAll(this.previousMethodCallCaptureContext.getStubList());
            }

            //
            retlist.add(this.getStub());

            //
            return retlist;
        }

        /**
         * Gets the stub object returned by the captured method call.
         * 
         * @return
         */
        protected Object getReturnedStub() {
            return this.returnedStub;
        }

        protected void setReturnedStub(Object returnedStub) {
            this.returnedStub = returnedStub;
        }

        public MethodCallCaptureContext getPreviousMethodCallCaptureContext() {
            return this.previousMethodCallCaptureContext;
        }

    }

    /**
     * ReplacementResult of a replay action.
     * 
     * @see MethodCallCapturer#replay(Object)
     * @author Omnaest
     */
    public static class ReplayResult {
        /* ********************************************** Variables ********************************************** */
        protected Exception exception = null;
        protected boolean isReplaySuccessful = false;

        /* ********************************************** Methods ********************************************** */

        /**
         * Returns true if the replay action was successful without an exception been thrown.
         * 
         * @return
         */
        public boolean isReplaySuccessful() {
            return this.isReplaySuccessful;
        }

        /**
         * Returns the {@link Exception} if one was thrown during the replay.
         * 
         * @return
         */
        public Exception getException() {
            return this.exception;
        }

        protected void setException(Exception exception) {
            this.exception = exception;
        }

        protected void setReplaySuccessful(boolean isReplaySuccessful) {
            this.isReplaySuccessful = isReplaySuccessful;
        }

    }

    /* ********************************************** Methods ********************************************** */

    /**
     * @see MethodCallCapturer
     */
    public MethodCallCapturer() {
        super();
    }

    /**
     * Creates a new stub instance for the given class or interface which records all method invocations to this
     * {@link MethodCallCapturer}.
     * 
     * @see MethodCallCapturer#getMethodCallCaptureContextList()
     * @see MethodCallCapturer#getCanonicalMethodNameToMethodCallCaptureMap()
     * @see #methodNameOf(Object)
     */
    public <E> E newInstanceOfCapturedType(Class<? extends E> clazz) {
        //
        return this.<E>newInstanceOfCapturedType(new CapturedTypeInstanceCreationConfiguration(clazz, null, false));
    }

    /**
     * Creates a new instance like {@link #newInstanceOfCapturedType(Class)} but makes the created stub implementing the
     * {@link MethodCallCapturerAware} interface which allows to get the underlying {@link MethodCallCapturer} through the generated
     * stub.
     * 
     * @see #newInstanceOfCapturedType(Class)
     * @param <E>
     * @param clazz
     * @return
     */
    public <E> E newInstanceOfCapturedTypeWhichIsMethodCallCapturerAware(Class<? extends E> clazz) {
        //
        CapturedTypeInstanceCreationConfiguration capturedTypeInstanceCreationConfiguration = new CapturedTypeInstanceCreationConfiguration(
                clazz, new Class[] { MethodCallCapturerAware.class }, false);
        return this.<E>newInstanceOfCapturedType(capturedTypeInstanceCreationConfiguration);
    }

    /**
     * Does create stubs whose objects returned by method invocations are stubs themselves. This allows to capture canonical method
     * names.
     * 
     * @see #newInstanceOfCapturedType(Class)
     * @see #methodNameOf(Object)
     * @param <E>
     * @param clazz
     * @return
     */
    public <E> E newInstanceOfTransitivlyCapturedType(Class<? extends E> clazz) {
        return this.<E>newInstanceOfCapturedType(new CapturedTypeInstanceCreationConfiguration(clazz, null, true));
    }

    /**
     * Creates a stub which implements the {@link MethodCallCapturerAware} interface and which generates method returned objects
     * which are stubs, too.
     * 
     * @see #newInstanceOfCapturedType(Class)
     * @see #newInstanceOfCapturedTypeWhichIsMethodCallCapturerAware(Class)
     * @see #newInstanceOfTransitivlyCapturedType(Class)
     * @param <E>
     * @param clazz
     * @return
     */
    public <E> E newInstanceOfTransitivlyCapturedTypeWhichIsMethodCallCapturerAware(Class<? extends E> clazz) {
        //
        CapturedTypeInstanceCreationConfiguration capturedTypeInstanceCreationConfiguration = new CapturedTypeInstanceCreationConfiguration(
                clazz, new Class[] { MethodCallCapturerAware.class }, true);
        return this.<E>newInstanceOfCapturedType(capturedTypeInstanceCreationConfiguration);
    }

    /**
     * @see #newInstanceOfCapturedType(Class)
     * @see #newInstanceOfCapturedTypeWhichIsMethodCallCapturerAware(Class)
     * @see #newInstanceOfTransitivlyCapturedType(Class)
     * @see #newInstanceOfTransitivlyCapturedTypeWhichIsMethodCallCapturerAware(Class)
     * @param <E>
     * @param capturedTypeInstanceCreationConfiguration
     * @return
     */
    @SuppressWarnings("unchecked")
    protected <E> E newInstanceOfCapturedType(
            CapturedTypeInstanceCreationConfiguration capturedTypeInstanceCreationConfiguration) {
        //
        final MethodCaptureMethodInvocationHandler methodCaptureMethodInvocationHandler = new MethodCaptureMethodInvocationHandler(
                capturedTypeInstanceCreationConfiguration);
        final E stubInstance = StubCreator.<E>newStubInstance(
                (Class<? extends E>) capturedTypeInstanceCreationConfiguration.getType(),
                ArrayUtils.add(capturedTypeInstanceCreationConfiguration.getInterfaces(), TypeCaptureAware.class),
                methodCaptureMethodInvocationHandler);

        //
        if (capturedTypeInstanceCreationConfiguration.getPreviousMethodCallCaptureContext() == null) {
            this.lastActiveRootStub = stubInstance;
        }

        //
        return stubInstance;
    }

    /**
     * Gets an available list for the given proxy object or creates a new one.
     * 
     * @param stub
     * @return
     */
    protected List<MethodCallCaptureContext> getOrCreateMethodCallCaptureContextListForStub(Object stub) {
        //
        List<MethodCallCaptureContext> retlist = null;

        //
        if (!this.stubToMethodCallCaptureContextListMap.containsKey(stub)) {
            this.stubToMethodCallCaptureContextListMap.put(stub,
                    Collections.synchronizedList(new ArrayList<MethodCallCaptureContext>()));
        }

        //
        retlist = this.stubToMethodCallCaptureContextListMap.get(stub);

        //
        return retlist;
    }

    /**
     * Returns a new list instance of all {@link MethodCallCaptureContext} instances.
     * 
     * @return
     */
    public List<MethodCallCaptureContext> getMethodCallCaptureContextList() {
        return ListUtils.mergeAll(this.stubToMethodCallCaptureContextListMap.values());
    }

    /**
     * Returns a new list instance of all {@link MethodCallCaptureContext} instances for a given stub instance.
     * 
     * @param stub
     * @return
     */
    public List<MethodCallCaptureContext> getMethodCallCaptureContextList(Object stub) {
        return new ArrayList<MethodCallCaptureContext>(this.getOrCreateMethodCallCaptureContextListForStub(stub));
    }

    /**
     * Returns a new list instance of all {@link MethodCallCaptureContext} instances for the last active stub instance. All
     * {@link MethodCallCaptureContext} instances coming before an {@link MethodCallCaptureContext} which is based on a nested call
     * within the previous context will be merged into the nested call {@link MethodCallCaptureContext}.
     * 
     * @return
     */
    public List<MethodCallCaptureContext> getMethodCallCaptureContextWithMergedHierarchyList() {
        return this.getMethodCallCaptureContextWithMergedHierarchyList(this.lastActiveRootStub);
    }

    /**
     * Returns a new list instance of all {@link MethodCallCaptureContext} instances for a given stub instance. All
     * {@link MethodCallCaptureContext} instances coming before an {@link MethodCallCaptureContext} which is based on a nested call
     * within the previous context will be merged into the nested call {@link MethodCallCaptureContext}.
     * 
     * @param stub
     * @return
     */
    public List<MethodCallCaptureContext> getMethodCallCaptureContextWithMergedHierarchyList(Object stub) {
        //
        List<MethodCallCaptureContext> retlist = this.getMethodCallCaptureContextList(stub);

        //
        this.mergeHierarchicalMethodCallCaptureContextList(retlist);

        //
        return retlist;
    }

    /**
     * Returns the last {@link MethodCallCaptureContext} which has been reported to this {@link MethodCallCapturer}.
     * 
     * @return
     */
    public MethodCallCaptureContext getLastMethodCallContext() {
        //
        List<MethodCallCaptureContext> methodCallCaptureContextList = this.getMethodCallCaptureContextList();
        int methodCallCaptureListSize = methodCallCaptureContextList.size();
        return methodCallCaptureListSize > 0 ? methodCallCaptureContextList.get(methodCallCaptureListSize - 1)
                : null;
    }

    /**
     * Resets the {@link MethodCallCapturer}.
     * 
     * @return this
     */
    public MethodCallCapturer reset() {
        //
        this.stubToMethodCallCaptureContextListMap.clear();

        //
        return this;
    }

    /**
     * Clears the captured method calls for a special stub object.
     * 
     * @param stub
     * @return this
     */
    public MethodCallCapturer reset(Object stub) {
        //
        this.getOrCreateMethodCallCaptureContextListForStub(stub).clear();

        //
        return this;
    }

    /**
     * Returns a new list of all captured canonical property names for the last active stub object in order of their invocation.
     * (First invocation is the first entry)
     * 
     * @see #getCapturedCanonicalMethodNameList()
     * @return
     */
    public List<String> getCapturedCanonicalPropertyNameList() {
        return this.getCapturedCanonicalPropertyNameList(this.lastActiveRootStub);
    }

    /**
     * Returns a new list of all captured canonical property names for a given stub object in order of their invocation. (First
     * invocation is the first entry)
     * 
     * @see #getCapturedCanonicalMethodNameList()
     * @param stub
     * @return
     */
    public List<String> getCapturedCanonicalPropertyNameList(final Object stub) {
        //
        List<String> canonicalPropertyNameList = null;

        //    
        ElementConverter<MethodCallCaptureContext, String> elementTransformer = new ElementConverter<MethodCallCapturer.MethodCallCaptureContext, String>() {
            @Override
            public String convert(MethodCallCaptureContext methodCallCaptureContext) {
                return methodCallCaptureContext.determineCanonicalPropertyName(stub);
            }
        };
        canonicalPropertyNameList = ListUtils.convert(this.getOrCreateMethodCallCaptureContextListForStub(stub),
                elementTransformer);

        //
        return canonicalPropertyNameList;
    }

    /**
     * Returns a new list of all captured canonical method names for a given stub object in order of their invocation. (First
     * invocation is the first entry)
     * 
     * @see #getCapturedCanonicalMethodNameList()
     * @param stub
     * @return
     */
    public List<String> getCapturedCanonicalMethodNameList(final Object stub) {
        //
        List<String> canonicalMethodNameList = null;

        //    
        ElementConverter<MethodCallCaptureContext, String> elementTransformer = new ElementConverter<MethodCallCapturer.MethodCallCaptureContext, String>() {
            @Override
            public String convert(MethodCallCaptureContext methodCallCaptureContext) {
                return methodCallCaptureContext.determineCanonicalMethodName(stub);
            }
        };
        canonicalMethodNameList = ListUtils.convert(this.getOrCreateMethodCallCaptureContextListForStub(stub),
                elementTransformer);

        //
        return canonicalMethodNameList;
    }

    /**
     * Returns a new list of all captured canonical method names for the last active stub object in order of their invocation.
     * (First invocation is the first entry)
     * 
     * @see #getCapturedCanonicalMethodNameListWithMergedHierarchyCalls(Object)
     * @return
     */
    public List<String> getCapturedCanonicalMethodNameList() {
        return this.getCapturedCanonicalMethodNameList(this.lastActiveRootStub);
    }

    /**
     * Returns a {@link List} of the canonical method names captured by the last active stub.
     * 
     * @see #getCapturedCanonicalMethodNameListWithMergedHierarchyCalls(Object)
     * @return
     */
    public List<String> getCapturedCanonicalMethodNameListWithMergedHierarchyCalls() {
        return this.getCapturedCanonicalMethodNameListWithMergedHierarchyCalls(this.lastActiveRootStub);
    }

    /**
     * Returns a {@link List} of the canonical property names captured by the last active stub.
     * 
     * @see #getCapturedCanonicalPropertyNameListWithMergedHierarchyCalls(Object)
     * @return
     */
    public List<String> getCapturedCanonicalPropertyNameListWithMergedHierarchyCalls() {
        return this.getCapturedCanonicalPropertyNameListWithMergedHierarchyCalls(this.lastActiveRootStub);
    }

    /**
     * Returns a {@link List} of the canonical method names captured by the given stub but with all hierarchical calls like
     * <code>testInterface.doTestSubInterface().doCalculateSomething()</code> which result in
     * <ul>
     * <li>testInterface</li>
     * <li>testInterface.fieldObject</li>
     * <li>testInterface.fieldObject.fieldString</li>
     * </ul>
     * merged into a single representation like:
     * <ul>
     * <li>testInterface.fieldObject.fieldString</li>
     * </ul>
     * 
     * @see #getCapturedCanonicalPropertyNameListWithMergedHierarchyCalls()
     * @see #getCapturedCanonicalMethodNameListWithMergedHierarchyCalls(Object)
     * @param stub
     * @return
     */
    public List<String> getCapturedCanonicalPropertyNameListWithMergedHierarchyCalls(Object stub) {
        //
        List<String> retlist = new ArrayList<String>();

        //
        List<String> capturedCanonicalPropertyNameList = this.getCapturedCanonicalPropertyNameList(stub);
        if (capturedCanonicalPropertyNameList != null) {
            //
            this.mergeHierarchicalNameList(capturedCanonicalPropertyNameList);

            //
            retlist.addAll(capturedCanonicalPropertyNameList);
        }

        //
        return retlist;
    }

    /**
     * Returns a {@link List} of the canonical method names captured by the given stub but with all hierarchical calls like
     * <code>testInterface.doTestSubInterface().doCalculateSomething()</code> which result in
     * <ul>
     * <li>testInterface</li>
     * <li>testInterface.doTestSubInterface</li>
     * <li>testInterface.doTestSubInterface.doCalculateSomething</li>
     * </ul>
     * merged into a single representation like:
     * <ul>
     * <li>testInterface.doTestSubInterface.doCalculateSomething</li>
     * </ul>
     * 
     * @see #getCapturedCanonicalMethodNameList()
     * @see #getCapturedCanonicalMethodNameListWithMergedHierarchyCalls()
     * @param stub
     * @return
     */
    public List<String> getCapturedCanonicalMethodNameListWithMergedHierarchyCalls(Object stub) {
        //
        List<String> retlist = new ArrayList<String>();

        //
        List<String> capturedCanonicalMethodNameList = this.getCapturedCanonicalMethodNameList(stub);
        if (capturedCanonicalMethodNameList != null) {
            //
            this.mergeHierarchicalNameList(capturedCanonicalMethodNameList);

            //
            retlist.addAll(capturedCanonicalMethodNameList);
        }

        //
        return retlist;
    }

    /**
     * Merges a list of hierarchical names. E.g. <code>testInterface.doTestSubInterface().doCalculateSomething()</code> which result
     * in
     * <ul>
     * <li>testInterface</li>
     * <li>testInterface.doTestSubInterface</li>
     * <li>testInterface.doTestSubInterface.doCalculateSomething</li>
     * </ul>
     * merged into a single representation like:
     * <ul>
     * <li>testInterface.doTestSubInterface.doCalculateSomething</li>
     * </ul>
     * 
     * @param hierarchicalNameList
     */
    protected void mergeHierarchicalNameList(List<String> hierarchicalNameList) {

        //
        List<String> hierarchicalNameListReversed = new ArrayList<String>(hierarchicalNameList);
        Collections.reverse(hierarchicalNameListReversed);

        //
        hierarchicalNameList.clear();

        //
        String methodNameLast = null;
        for (String methodName : hierarchicalNameListReversed) {
            //
            if (methodNameLast == null || !StringUtils.startsWith(methodNameLast, methodName + ".")) {
                hierarchicalNameList.add(methodName);
            }

            //
            methodNameLast = methodName;
        }

        //
        Collections.reverse(hierarchicalNameList);
    }

    /**
     * Merges hierarchies of {@link MethodCallCaptureContext}s into the most nested {@link MethodCallCaptureContext} for each group
     * of {@link MethodCallCaptureContext}s
     * 
     * @param hierarchicalMethodCallCaptureContextList
     */
    protected void mergeHierarchicalMethodCallCaptureContextList(
            List<MethodCallCaptureContext> hierarchicalMethodCallCaptureContextList) {

        //
        List<MethodCallCaptureContext> hierarchicalMethodCallCaptureContextReversedList = new ArrayList<MethodCallCaptureContext>(
                hierarchicalMethodCallCaptureContextList);
        Collections.reverse(hierarchicalMethodCallCaptureContextReversedList);

        //
        hierarchicalMethodCallCaptureContextList.clear();

        //
        MethodCallCaptureContext methodCallCaptureContextLast = null;
        for (MethodCallCaptureContext methodCallCaptureContext : hierarchicalMethodCallCaptureContextReversedList) {
            //
            MethodCallCaptureContext previousMethodCallCaptureContextFromLast = methodCallCaptureContextLast != null
                    ? methodCallCaptureContextLast.getPreviousMethodCallCaptureContext()
                    : null;

            //
            if (methodCallCaptureContextLast == null
                    || !methodCallCaptureContext.equals(previousMethodCallCaptureContextFromLast)) {
                hierarchicalMethodCallCaptureContextList.add(methodCallCaptureContext);
            }

            //
            methodCallCaptureContextLast = methodCallCaptureContext;
        }

        //
        Collections.reverse(hierarchicalMethodCallCaptureContextList);
    }

    /**
     * Replays the method invocations done to the last active stub for the given object like the object would have been invoked in
     * the first place.
     * 
     * @see #replay(Object, Object)
     * @param object
     */
    public <E> ReplayResult replay(E object) {
        return this.replay(this.lastActiveRootStub, object);
    }

    /**
     * Replays the method invocations done to the given stub for the given object like the object would have been invoked in the
     * first place.
     * 
     * @see #replay(Object)
     * @param stub
     * @param object
     * @return {@link ReplayResult}
     */
    public <E> ReplayResult replay(E stub, E object) {
        //
        ReplayResult result = new ReplayResult();

        //
        if (stub != null && object != null) {
            //
            Map<Object, Object> stubToObjectMap = new IdentityHashMap<Object, Object>();
            stubToObjectMap.put(stub, object);

            //
            try {
                List<MethodCallCaptureContext> methodCallCaptureContextList = this
                        .getMethodCallCaptureContextList(stub);
                for (MethodCallCaptureContext methodCallCaptureContext : methodCallCaptureContextList) {
                    if (methodCallCaptureContext != null) {
                        //
                        MethodCallCapture methodCallCapture = methodCallCaptureContext.getMethodCallCapture();
                        if (methodCallCapture != null) {
                            //
                            Object invocationStub = methodCallCapture.getObject();
                            Object invocationObject = stubToObjectMap.get(invocationStub);

                            //
                            if (invocationObject != null) {
                                Method method = methodCallCapture.getMethod();
                                Object[] args = methodCallCapture.getArguments();
                                if (method != null) {
                                    //
                                    Object methodInvocationResult = method.invoke(invocationObject, args);

                                    //
                                    Object returnedStub = methodCallCaptureContext.getReturnedStub();
                                    if (returnedStub != null && methodInvocationResult != null) {
                                        stubToObjectMap.put(returnedStub, methodInvocationResult);
                                    }
                                }
                            }
                        }
                    }
                }

                //
                result.setReplaySuccessful(true);
            } catch (Exception exception) {
                result.setException(exception);
            }
        }

        //
        return result;
    }

    /**
     * Adds a {@link MethodCallCapture} instance to the report.
     * 
     * @param canonicalMethodName
     * @param methodCallCapture
     * @param rootStub
     * @param returnedStub
     */
    private void addMethodCallCapture(MethodCallCaptureContext methodCallCaptureContext) {
        //
        List<Object> stubList = methodCallCaptureContext.getStubList();
        for (Object stub : stubList) {
            this.getOrCreateMethodCallCaptureContextListForStub(stub).add(methodCallCaptureContext);
        }

        //
        this.lastActiveRootStub = stubList.get(0);
    }
}