/**
* 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();
}
}
}