org.xchain.framework.lifecycle.Execution.java Source code

Java tutorial

Introduction

Here is the source code for org.xchain.framework.lifecycle.Execution.java

Source

/**
 *    Copyright 2011 meltmedia
 *
 *    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.xchain.framework.lifecycle;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.Pointer;

import org.xchain.EngineeredCommand;
import org.xchain.Locatable;
import org.xchain.Registerable;
import org.xchain.Command;
import org.xchain.Filter;
import org.xchain.framework.jxpath.Scope;
import org.xchain.framework.jxpath.ScopedJXPathContextImpl;
import org.xchain.framework.util.ThreadLocalStack;

import org.xml.sax.Locator;
import org.xml.sax.helpers.LocatorImpl;

/**
 * The Execution class manages the context and execution stacks.
 *
 * @author Christian Trimble
 * @author Devon Tackett
 * @author Josh Kennedy
 */
public final class Execution {
    private static Logger log = LoggerFactory.getLogger(Execution.class);

    /** A thread local stack of the commands being executed. */
    private static ThreadLocalStack<ExecutionContext> executionContextStack = new ThreadLocalStack<ExecutionContext>();

    /** A thread local stack of the commands currently suspended. */
    private static ThreadLocalStack<ExecutionContext> suspendedExecutionContextStack = new ThreadLocalStack<ExecutionContext>();

    /** A thread local of the current execution level context. */
    private static ThreadLocal<JXPathContext> executionContextTl = new ThreadLocal<JXPathContext>();

    /** A thread local stack of execution trace elements. */
    private static ThreadLocalStack<ExecutionTraceElement> executionTraceStack = new ThreadLocalStack<ExecutionTraceElement>();

    /** A thread local stack of suspended execution trace elements. */
    private static ThreadLocalStack<ExecutionTraceElement> suspendedExecutionTraceStack = new ThreadLocalStack<ExecutionTraceElement>();

    /** A thread local stack of chain level contexts. */
    private static ThreadLocalStack<JXPathContext> chainContextStack = new ThreadLocalStack<JXPathContext>();

    /** A thread local stack of suspended chain level contexts. */
    private static ThreadLocalStack<JXPathContext> suspendedChainContextStack = new ThreadLocalStack<JXPathContext>();

    /** A stack of prefixes defined for the life of a given command. */
    private static ThreadLocal<LinkedList<PrefixMappingContext>> prefixMappingStack = new ThreadLocal<LinkedList<PrefixMappingContext>>() {
        protected LinkedList<PrefixMappingContext> initialValue() {
            return new LinkedList<PrefixMappingContext>();
        }
    };

    /**
     * Start an execution stack on the current thread.
     * 
     * @param globalContext The global JXPathContext for this execution.
     */
    public static void startExecution(JXPathContext globalContext) {
        // Wrap the incoming context in a global context
        executionContextTl
                .set(new ScopedJXPathContextImpl(globalContext, globalContext.getContextBean(), Scope.execution));
        executionContextStack.push(new GlobalExecutionContext());
    }

    /**
     * End all execution for this thread.  All contexts will be cleared.
     * 
     * @throws ExecutionException If an exception was thrown during execution it will be wrapped in an ExecutionException.
     */
    public static void endExecution() throws ExecutionException {
        ExecutionException executionException = null;
        // Check if an exception was encountered during execution.
        if (executionContextStack.peek().exceptionContext != null) {
            // An exception was found.  Create a new ExecutionException with a trace to the source of the exception.
            executionException = new ExecutionException("An exception was thrown during the execution.",
                    executionContextStack.peek().exceptionContext.trace,
                    executionContextStack.peek().exceptionContext.exception);
        }
        // Clear all stacks.
        executionContextStack.clear();
        suspendedExecutionContextStack.clear();
        executionTraceStack.clear();
        suspendedExecutionTraceStack.clear();
        chainContextStack.clear();
        suspendedChainContextStack.clear();

        // End the execution context.
        endContext(executionContextTl.get());
        executionContextTl.set(null);
        if (executionException != null) {
            // Throw the ExecutionException if one was created.
            throw executionException;
        }
    }

    /**
     * Returns a string representation of the current execution.
     */
    private static String stateString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Execution Context Stacks:").append(executionContextStack.size()).append("/")
                .append(suspendedExecutionContextStack.size());
        builder.append(" Execution Trace Stacks:").append(executionTraceStack.size()).append("/")
                .append(suspendedExecutionTraceStack.size());
        builder.append(" Local Context Stacks:").append(chainContextStack.size()).append("/")
                .append(suspendedChainContextStack.size());
        builder.append(" Global Context:").append(executionContextTl.get() != null);
        return builder.toString();
    }

    private static String detailedStateString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Execution Context Stack:\n");
        for (ExecutionContext executionContext : executionContextStack.toList()) {
            builder.append("  ").append(executionContext.toString()).append("\n");
        }
        builder.append("Suspended Execution Context Stack:\n");
        for (ExecutionContext executionContext : suspendedExecutionContextStack.toList()) {
            builder.append("  ").append(executionContext.toString()).append("\n");
        }
        return builder.toString();
    }

    /**
     * Returns true if a command is currently executing.
     */
    public static boolean inExecution() {
        return executionContextStack.size() > 0;
    }

    /**
     * <p>Signals that the execute method of a command has been called.  If this command is registered with a catalog, then
     * a new local context is created based on the current context that was passed in.</p>
     *
     * <p>NOTE: This command is used by the engineering framework and should not be called directly by
     * command implementations.</p>
     *
     * @param command the command that is being executed.
     * @param context the current local context or the global context.
     * @return the current local context.  If the command is registered with a catalog, then the context returned is
     * the new local context for this command.  Otherwise, the context returned will be the same as the context passed in.
     */
    public static JXPathContext startCommandExecute(Command command, JXPathContext context) {
        JXPathContext localContext = null;

        // push this command onto the stack.
        executionContextStack.push(new CommandExecutionContext(command));

        if (isLocalContextBoundary(command)) {
            // create the new local context.
            localContext = new ScopedJXPathContextImpl(executionContextTl.get(), context.getContextBean(),
                    context.getContextPointer(), Scope.chain);

            // push the local context.
            chainContextStack.push(localContext);

            startExecutionTrace(command);
        } else if (!(context instanceof ScopedJXPathContextImpl)) {
            throw new IllegalStateException("Initial call to command that is not registered with a catalog.");
        }

        updateExecutionTraceLocation();

        // get the local context.
        context = getCurrentContext();
        if (command instanceof EngineeredCommand) {
            // Define prefix mappings for engineered commands.
            definePrefixMappings(context, (EngineeredCommand) command);
        }

        // return the context.
        return context;
    }

    /**
     * Returns true if the command represents a local context boundary, false otherwise.
     *
     * @return true if the command represents a local context boundary, false otherwise.
     */
    private static boolean isLocalContextBoundary(Command command) {
        if (!(command instanceof Registerable)) {
            return false;
        }

        return ((Registerable) command).isRegistered();
    }

    /**
     * <p>Signals that we are creating a local context for context bean
     * that has the same variable and function scope as the last context
     * bean.</p>
     * 
     * <p>The user can safely call this method.</p>
     * 
     * @param context the current local context.
     * @param contextPointer A Pointer to the new context bean.
     * 
     * @return the new local context that shares variables and namespace context with the previous local context,
     * but it has a new context bean.
     */
    public static JXPathContext startContextPointer(JXPathContext context, Pointer contextPointer) {
        // make sure that this is the current local context.
        testCurrentLocalContext(context);

        // create the relative context.
        JXPathContext localContext = context.getRelativeContext(contextPointer);

        // push the context onto the stack.
        chainContextStack.push(localContext);

        // return the new local context.
        return localContext;
    }

    /**
     * <p>Executed at the end of a command's execute method if it is not a filter or at the end of a
     * command's postProcess method if it is a filter.  This method will change the current local
     * context if the current command is registered with a catalog.</p>
     *
     * <p>NOTE: This command is used by the engineering framework and should not be called directly by
     * command implementations.</p>
     *
     * @param context the current local context.
     * @return the previous local context on the context stack.
     */
    public static JXPathContext endCommandExecute(Command command, JXPathContext context) {
        if (command instanceof EngineeredCommand) {
            // Undefine prefix mappings for engineered commands.
            undefinePrefixMappings(context, (EngineeredCommand) command);
        }

        // make sure that we are managing the stacks correctly.
        testCurrentLocalContext(context);
        testCurrentCommand(command);

        boolean isFilter = command instanceof Filter;

        // if we are in a filter, the suspend everything.
        if (isFilter) {
            suspendedExecutionContextStack.push(executionContextStack.pop());

            // if this is a local context boundary, then suspend the context.
            if (isLocalContextBoundary(command)) {
                suspendedChainContextStack.push(chainContextStack.pop());
                suspendExecutionTrace();
            }
        }
        // otherwise, this is a just a command, so pop it's state off of the stacks.
        else {
            // remove the command from the stack.
            executionContextStack.pop();

            // if this was a context boundary, then update the execution trace and the local context stack.
            if (isLocalContextBoundary(command)) {
                endContext(chainContextStack.pop());
                stopExecutionTrace();
            }
        }

        // update the current location of the execution trace.
        updateExecutionTraceLocation();

        // return the current context.
        return getCurrentContext();
    }

    /**
     * <p>End the current context pointer.</p>
     *
     * @param context the local context to stop.
     * @return the new local context.
     */
    public static JXPathContext stopContextPointer(JXPathContext context) {
        testCurrentLocalContext(context);
        endContext(chainContextStack.pop());
        return getCurrentContext();
    }

    /**
     * <p>Marks the end of a commands post process method.</p>
     * <p>NOTE: This command is used by the engineering framework and should not be called directly by
     * command implementations.</p>
     *
     * @param context the current local context.
     * @return the previous local context on the context stack.
     */
    public static JXPathContext endCommandPostProcess(Command command, JXPathContext context) {
        if (command instanceof EngineeredCommand) {
            // Undefine prefix mappings for engineered commands.
            undefinePrefixMappings(context, (EngineeredCommand) command);
        }

        // make sure that we are managing the stacks correctly.
        testCurrentLocalContext(context);
        testCurrentCommand(command);

        // move the command to the top of the suspended command stack.
        executionContextStack.pop();

        // otherwise, this is a just a command, so pop it's state off of the stacks.
        if (isLocalContextBoundary(command)) {
            endContext(chainContextStack.pop());
            stopExecutionTrace();
        }

        // update the current location of the execution trace.
        updateExecutionTraceLocation();

        // return the current context.
        return getCurrentContext();
    }

    /**
     * <p>Suspends a local context for a context pointer.</p>
     *
     * @param context the current local context.
     * @return the new local context.
     */
    public static JXPathContext suspendContextPointer(JXPathContext context) {
        testCurrentLocalContext(context);
        suspendedChainContextStack.push(chainContextStack.pop());
        return getCurrentContext();
    }

    /**
     * Resumes the top item on the suspended context stack.
     *
     * <p>NOTE: This command is used by the engineering framework and should not be called directly by
     * command implementations.</p>
     *
     * @param context the current local context.
     * @return the context that was resumed.
     */
    public static JXPathContext startCommandPostProcess(Command command, JXPathContext context) {
        try {
            // make sure that we are managing the stacks correctly.
            testCurrentLocalContext(context);

            // move the top of the suspended command stack to the top of the command stack.
            executionContextStack.push(suspendedExecutionContextStack.pop());
            testCurrentCommand(command);

            // move the top of the 
            if (isLocalContextBoundary(command)) {
                chainContextStack.push(suspendedChainContextStack.pop());
                resumeExecutionTrace();
            }

            // update the current location of the execution trace.
            updateExecutionTraceLocation();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }

        // Get the current context.
        context = getCurrentContext();
        if (command instanceof EngineeredCommand) {
            // Define prefix mappings for engineered commands.
            definePrefixMappings(context, (EngineeredCommand) command);
        }

        // return the current context.
        return context;
    }

    /**
     * Add all custom prefixes for the given command to the context.  PrefixMappings are stored in a stack.  The last prefix mapping that
     * is defined must be the first one undefined.  Any prefixes that conflict with the command's prefixes will have their original
     * values saved and will be restored once the prefix mapping is undefined for the given command.
     * 
     * @param context The context being used.
     * @param command The command whose custom prefixes are to be used.
     */
    public static void definePrefixMappings(JXPathContext context, EngineeredCommand command) {
        PrefixMappingContext prefixContext = null;
        // get the first prefix mapping.
        if (prefixMappingStack.get().size() > 0
                && prefixMappingStack.get().getFirst().isContextFor(context, command)) {
            prefixContext = prefixMappingStack.get().getFirst();
            prefixContext.setCallCount(prefixContext.getCallCount() + 1);
        } else {
            // create a new prefix context.
            prefixContext = new PrefixMappingContext(context, command);

            // place the prefix on the top of the prefix context stack.
            prefixMappingStack.get().addFirst(prefixContext);

            // iterate over the mappings defined in the command, storing the old values.
            for (Map.Entry<String, String> entry : command.getPrefixMap().entrySet()) {
                prefixContext.getOriginalPrefixMap().put(entry.getKey(), context.getNamespaceURI(entry.getKey()));
            }

            // set the new values into the context.
            for (Map.Entry<String, String> entry : command.getPrefixMap().entrySet()) {
                context.registerNamespace(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * Remove all custom prefixes for the given command from the context.  Prefix mappings are stored in a stack.  The
     * latest prefix mapping that has been defined must be the first one undefined.  When a prefix mapping is undefined
     * any prefixes that conflicted with the command's prefixes will have their original values restored.
     * 
     * @param context The context being used.
     * @param command The command whose custom prefixes are to be undefined.
     */
    public static void undefinePrefixMappings(JXPathContext context, EngineeredCommand command) {
        // get the first item from the stack.
        PrefixMappingContext prefixContext = prefixMappingStack.get().getFirst();

        if (!prefixContext.isContextFor(context, command)) {
            throw new RuntimeException("CommandUtil.undefinePrefixMapping called on wrong context and command.");
        }

        // if this is a duplicate mapping call, then just decrement the call count.
        if (prefixContext.getCallCount() > 0) {
            prefixContext.setCallCount(prefixContext.getCallCount() - 1);
        }

        // otherwise, we need to remove the mappings and remove the context.
        else {

            // set the original values into the context.
            for (Map.Entry<String, String> entry : prefixContext.getOriginalPrefixMap().entrySet()) {
                context.registerNamespace(entry.getKey(), entry.getValue());
            }

            // remove the prefix mapping from the context stack.
            prefixMappingStack.get().removeFirst();

            // if the stack is empty, then remove the stack for this thread.
            if (prefixMappingStack.get().size() == 0) {
                prefixMappingStack.remove();
            }
        }
    }

    /**
     * <p>This method is used to resume a suspended context pointer.</p>
     *
     * @param context the current context.
     * @return the context that was resumed.
     */
    public static JXPathContext resumeContextPointer(JXPathContext context) {
        testCurrentLocalContext(context);
        chainContextStack.push(suspendedChainContextStack.pop());
        return chainContextStack.peek();
    }

    /**
     * <p>Pushes an execution trace onto the top of the execution trace stack.</p>
     *
     * @param command the command to start an execution trace for.
     */
    private static void startExecutionTrace(Command command) {
        String systemId = null;
        QName qName = null;

        if (command instanceof Registerable) {
            Registerable registerable = (Registerable) command;
            if (registerable.isRegistered()) {
                systemId = registerable.getSystemId();
                qName = registerable.getQName();
            }
        }

        // add a new locator for this systemId and qName to the top of the stack.
        executionTraceStack.push(new ExecutionTraceElement(systemId, qName, null));
    }

    /**
     * <p>Removes the top execution trace from the execution trace stack.</p>
     */
    private static void stopExecutionTrace() {
        executionTraceStack.pop();
    }

    /**
     * <p>Moves the execution trace on the top of the execution trace stack to the top of the suspended execution
     * trace stack.</p>
     */
    private static void suspendExecutionTrace() {
        suspendedExecutionTraceStack.push(executionTraceStack.pop());
    }

    /**
     * <p>Moves the execution trace on the top of the suspended exection trace stack to the top of the execution trace stack.</p>
     */
    private static void resumeExecutionTrace() {
        executionTraceStack.push(suspendedExecutionTraceStack.pop());
    }

    /**
     * Updates the current location of the top entry of the execution trace stack.
     */
    private static void updateExecutionTraceLocation() {
        if (!executionTraceStack.isEmpty()) {
            Command command = ((CommandExecutionContext) executionContextStack.peek()).command;
            Locator locator = null;

            if (command instanceof Locatable) {
                locator = ((Locatable) command).getLocator();
            } else {
                LocatorImpl locatorImpl = new LocatorImpl();
                locatorImpl.setLineNumber(0);
                locatorImpl.setColumnNumber(0);
                locatorImpl.setSystemId("UNKNOWN_LOCATION");
                locator = locatorImpl;
            }

            executionTraceStack.peek().setLocator(locator);
        }
    }

    private static JXPathContext getCurrentContext() {
        JXPathContext currentContext = null;
        if (chainContextStack.isEmpty()) {
            currentContext = executionContextTl.get();
        } else {
            currentContext = chainContextStack.peek();
        }

        return currentContext;
    }

    /**
     * Called by engineered commands to notify that an exception is propagating from an execute(JXPathContext) method.
     */
    public static void exceptionThrown(Command command, Exception exception) {
        // make sure that we are in the current command.
        testCurrentCommand(command);

        // get the parent context.
        ExecutionContext parentExecutionContext = executionContextStack.peek(1);
        ExecutionContext executionContext = executionContextStack.peek();
        ExceptionContext exceptionContext = executionContext.exceptionContext;

        // if the exception thrown is the same exception on the current node, then move it to the parent.
        if (exceptionContext != null && exceptionContext.exception == exception) {
            parentExecutionContext.exceptionContext = exceptionContext;
        }

        // if the exception is different, but one of the filters claimed to handle it, then we need to create a new context with no cause.
        else if (exceptionContext == null || exceptionContext.handled) {
            parentExecutionContext.exceptionContext = new ExceptionContext(exception, getExecutionTrace(), null);
        }

        // if the exception thrown is different and it was not handled, then it must be a cause.
        else {
            parentExecutionContext.exceptionContext = new ExceptionContext(exception, getExecutionTrace(),
                    exceptionContext);
        }
    }

    public static void exceptionHandled(Command command, Exception exception) {
        // make sure that we are in the current command.
        testCurrentCommand(command);

        // mark the exception as handled.
        executionContextStack.peek(1).exceptionContext.handled = true;
    }

    /**
     * Returns the current local context.
     */
    public static JXPathContext getLocalContext() {
        return chainContextStack.peek();
    }

    /**
     * Returns the global context.
     */
    public static JXPathContext getGlobalContext() {
        return executionContextTl.get();
    }

    /**
     * Returns the system id for the currently executing command.
     */
    public static String getSystemId() {
        if (executionTraceStack.isEmpty()) {
            throw new IllegalStateException(
                    "The getCurrentSystemId() function must only be called during an execution.");
        }
        return executionTraceStack.peek().getSystemId();
    }

    /**
     * Returns a copy of the current execution trace stack.  This list contains all of the currently executing
     * xchains starting with the current chain and anding with the first chain that was called.
     */
    public static List<ExecutionTraceElement> getExecutionTrace() {
        // we need clone the entries in this list, since the locations are changing.
        // we will reuse the list, since toList() creates a new list.
        List<ExecutionTraceElement> executionTrace = executionTraceStack.toList();
        for (int i = 0; i < executionTrace.size(); i++) {
            executionTrace.set(i, new ExecutionTraceElement(executionTrace.get(i)));
        }
        return executionTrace;
    }

    /**
     * Tests that the context passed in is the current local context.
     */
    private static void testCurrentLocalContext(JXPathContext context) {
        if ((chainContextStack.isEmpty() && executionContextTl.get() != context)) {
            throw new IllegalStateException(
                    "The global context should have been passed to Execution.startCommandPostProcess().");
        } else if (!chainContextStack.isEmpty() && chainContextStack.peek() != context) {
            throw new IllegalStateException(
                    "The local context passed to Execution.XXXLocalContext() was not the current local context.");
        }
    }

    /**
     * Tests that the command passed in is the current command.
     */
    private static void testCurrentCommand(Command command) {
        if (((CommandExecutionContext) executionContextStack.peek()).command != command) {
            throw new IllegalStateException(
                    "The command passed to Execution.XXXCommand() was not the current command.");
        }
    }

    /**
     * Returns true if this context represents the start of a local scope.  Returns false if the context represents a change of
     * context node inside of a local scope.
     */
    private static boolean representsLocalScopeStart(JXPathContext context) {
        return context.getParentContext() != null && context.getParentContext() == executionContextTl.get();
    }

    /**
     * Base ExcecutionContext.  Contains an ExceptionContext if an exception was encountered during execution.
     */
    private static abstract class ExecutionContext {
        public ExceptionContext exceptionContext = null;
    }

    /**
     * Execution context for commands.
     */
    private static class CommandExecutionContext extends ExecutionContext {
        /** The command being executed. */
        public Command command = null;

        public CommandExecutionContext(Command command) {
            this.command = command;
        }

        public String toString() {
            return "Command:" + command.getClass().getName() + "[" + command.toString() + "]";
        }
    }

    /**
     * Global execution context for any type of execution.
     */
    private static class GlobalExecutionContext extends ExecutionContext {
        public String toString() {
            return "Global";
        }
    }

    /**
     * The ExceptionContext contains information about where an exception was encountered in an XChain.
     */
    public static class ExceptionContext {
        public Exception exception = null;
        public List<ExecutionTraceElement> trace = null;
        public ExceptionContext cause = null;
        public boolean handled = false;

        public ExceptionContext(Exception exception, List<ExecutionTraceElement> trace, ExceptionContext cause) {
            this.exception = exception;
            this.trace = trace;
            this.cause = cause;
        }
    }

    /**
     * This stores the command and context the prefixes were defined fore.
     */
    private static class PrefixMappingContext {
        // The command the custom prefixes belong to.
        private EngineeredCommand command;
        // The context the commands are executing with.
        private JXPathContext context;
        // If a command defines a prefix that already exists, the previous value will be stored in this map.
        private Map<String, String> originalPrefixMap = new HashMap<String, String>();
        private int callCount = 0;

        public PrefixMappingContext(JXPathContext context, EngineeredCommand command) {
            this.context = context;
            this.command = command;
        }

        public void setCommand(EngineeredCommand command) {
            this.command = command;
        }

        public EngineeredCommand getCommand() {
            return this.command;
        }

        public void setContext(JXPathContext context) {
            this.context = context;
        }

        public JXPathContext getContext() {
            return this.context;
        }

        public Map<String, String> getOriginalPrefixMap() {
            return originalPrefixMap;
        }

        public void setCallCount(int callCount) {
            this.callCount = callCount;
        }

        public int getCallCount() {
            return callCount;
        }

        /**
         * Test if this PrefixMappingContext is for the given command and context.
         */
        public boolean isContextFor(JXPathContext testContext, EngineeredCommand testCommand) {
            return this.context == testContext && this.command == testCommand;
        }
    }

    private static void endContext(JXPathContext context) {
        if (context instanceof ScopedJXPathContextImpl) {
            ((ScopedJXPathContextImpl) context).releaseComponents();
        }
    }
}