package org.openbp.server;
import java.util.Iterator;
import java.util.Map;
import org.openbp.common.logger.LogUtil;
import org.openbp.core.OpenBPException;
import org.openbp.core.engine.EngineException;
import org.openbp.core.model.ModelException;
import org.openbp.core.model.item.process.InitialNode;
import org.openbp.core.model.item.process.NodeParam;
import org.openbp.core.model.item.process.NodeSocket;
import org.openbp.core.model.item.type.DataTypeItem;
import org.openbp.core.model.item.type.ValidationException;
import org.openbp.server.context.LifecycleRequest;
import org.openbp.server.context.LifecycleState;
import org.openbp.server.context.TokenContext;
import org.openbp.server.context.TokenContextCriteria;
import org.openbp.server.context.TokenContextService;
import org.openbp.server.context.TokenContextUtil;
import org.openbp.server.context.WorkflowTask;
import org.openbp.server.context.WorkflowTaskCriteria;
import org.openbp.server.engine.Engine;
import org.openbp.server.engine.EngineEvent;
import org.openbp.server.engine.EngineRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* The process facade provides the standard functionality for running processes.
* It is a convenient shortcut to a variety of services provided by the OpenBP framework components.
*
* @author Heiko Erhardt
*/
@Service("processFacade")
public class ProcessFacadeImpl
implements ProcessFacade
{
@Autowired
private Engine engine;
@Autowired
private EngineRunner engineRunner;
/**
* Default constructor.
*/
public ProcessFacadeImpl()
{
}
//////////////////////////////////////////////////
// @@ Token and workflow lifecycle and control
//////////////////////////////////////////////////
/**
* Creates a new token.
* This method just creates the context, it does commit it to the database.
* Use the {@link #startToken(TokenContext)} method to start the process and to persist it to the database.
*
* @return The new token
*/
public TokenContext createToken()
{
return engine.getTokenContextService().createContext();
}
/**
* Starts the given token at the given start reference using the optional parameter map.
* The token is marked for further execution by the process engine.
*
* @param context Token context
* @param startRef Reference to the start node/socket ("/Process/Node").
* For a description of the supported formats, see the {@link Engine#resolveSocketRef} method.
* @param inputParamValues A map of parameter name/parameter value pairs that will be assigned to the
* parameters of the target socket or null
*/
public void startToken(TokenContext context, String startRef, Map inputParamValues)
{
setInitialPosition(context, startRef, inputParamValues);
startToken(context);
}
/**
* Starts the given token at its current position.
* The token is marked for further execution by the process engine.
*
* @param context Token context
*/
public void startToken(TokenContext context)
{
engine.startToken(context);
}
/**
* Simply saves the given token as suspended token.
* Can be used to save token that shall be executed by the scheduler to the database without starting them.
* The method commits the changes to the database.
*
* @param context Token context
*/
public void prepareTokenForScheduler(TokenContext context)
{
engine.prepareTokenForScheduler(context);
}
/**
* Resumes the given suspended token at the given position using the optional parameter map.
* The token is marked for further execution by the process engine.
*
* If the token was suspended using a workflow node, you should use the {@link #resumeToken(TokenContext)} method.
*
* @param context Token context
* @param resumptionRef Reference to the point of resumption or null for resumption at the current exit socket.
* For a description of the supported formats, see the {@link Engine#resolveSocketRef} method.
* @param inputParamValues A map of parameter name/parameter value pairs that will be assigned to the
* parameters of the target socket or null
*/
public void resumeToken(TokenContext context, String resumptionRef, Map inputParamValues)
{
setResumptionPosition(context, resumptionRef, inputParamValues);
resumeToken(context);
}
/**
* Resumes the given suspended token at its current position.
* The token is marked for further execution by the process engine.
*
* If the token was suspended using a workflow node, you should use the {@link #resumeToken(TokenContext)} method.
*
* @param context Token context
*/
public void resumeToken(TokenContext context)
{
engine.resumeToken(context);
}
/**
* Ends the token.
* If the token is currently in execution, this method simply calls {@link TokenContext#setLifecycleRequest}
* method with the LifecycleRequest.STOP argument, instructing the OpenBP engine to end the execution gracefully.
*
* Otherwise, the token and all of its child tokens and associated workflow tasks will be deleted from
* the database. In this case, the method also sets the state of the workflow task that is associated
* with this context (if any) to {@link WorkflowTask#STATUS_COMPLETED} or deletes the workflows.
* Also raises the engine events {@link EngineEvent#BEFORE_END_TOKEN} and {@link EngineEvent#AFTER_END_TOKEN}.
*
* @param context Context that holds the token's state information
*/
public void endToken(TokenContext context)
{
// TODO endToken needs to check state of the context and act accordingly: setLifecycleRequest(LifecycleRequest.STOP)
engine.endToken(context);
}
/**
* Resumes a suspended workflow.
* - Assigns the workflow to the current user if desired.<br>
* - Sets the status of the workflow to {@link WorkflowTask#STATUS_RESUMED}.<br>
* - Set the AcceptingUserId and TimeAccepted properties of the workflow task.<br>
* - Determines the socket of the workflow node to resume at.<br>
* - Begins a transaction, saves the workflow task and commit the transaction.<br>
* The lifecycle request of the token will be in state {@link LifecycleRequest#RESUME}.
*
* Use this method instead of {@link #resumeToken(TokenContext)} if the token was suspended using a workflow node.
*
* @param workflowTask Workflow task this workflow refers to
* @param resumptionRef Reference to the point of resumption or null for resumption at the current exit socket
* (usually the name of an exit socket of the current node, but may also specifiy an entry node name of the current process).
* @param currentUserId Id of the user that accepts this workflow (may be null);
* will be assigned to the 'AcceptingUser' property of the workflow and to the 'UserId' of the workflow
* if the 'AssignToCurrentUser' property of the workflow has been set.
* @throws OpenBPException Any exception that may occur during start of the preparation of the workflow task
*/
public void resumeWorkflow(WorkflowTask workflowTask, String resumptionRef, String currentUserId)
{
engine.resumeWorkflow(workflowTask, resumptionRef, currentUserId);
}
/**
* Sets the initial position of a process using the given start reference.
*
* @param context Token context
* @param startRef Reference to the start node/socket. For a description of the supported formats, see the {@link Engine#resolveSocketRef} method.
* @param inputParamValues A map of parameter name/parameter value pairs that will be assigned to the
* parameters of the target socket or null
*/
public void setInitialPosition(TokenContext context, String startRef, Map inputParamValues)
{
NodeSocket startSocket = getEngine().resolveSocketRef(startRef, context.getCurrentSocket(), context, true);
InitialNode initialNode = (InitialNode) startSocket.getNode();
if (initialNode.getEntryScope() != InitialNode.SCOPE_PUBLIC)
{
String msg = LogUtil.error(getClass(), "Node $0 is not a public initial node (start node reference: $1). [{2}]", initialNode.getQualifier(), startRef, context);
throw new ModelException("NoPublicInitialNode", msg);
}
context.setCurrentSocket(startSocket);
if (context.getExecutingModel() == null)
{
context.setExecutingModel(startSocket.getProcess().getModel());
}
bindInputParameters(context, startSocket, inputParamValues);
}
/**
* Sets the resumption position of a suspended process using the given socket name.
*
* @param context Token context
* @param resumptionRef Reference to the point of resumption or null for resumption at the current exit socket.
* For a description of the supported formats, see the {@link Engine#resolveSocketRef} method.
* @param inputParamValues A map of parameter name/parameter value pairs that will be assigned to the
* parameters of the target socket or null
*/
public void setResumptionPosition(TokenContext context, String resumptionRef, Map inputParamValues)
{
NodeSocket startSocket = getEngine().resolveSocketRef(resumptionRef, context.getCurrentSocket(), context, true);
context.setCurrentSocket(startSocket);
bindInputParameters(context, startSocket, inputParamValues);
}
/**
* Binds the given input parameters to the parameters ofthe entry socket.
*
* @param context Token context
* @param socket Entry socket
* @param inputParamValues A map of parameter name/parameter value pairs that will be assigned to the
* parameters of the target socket or null
*/
private void bindInputParameters(TokenContext context, NodeSocket socket, Map inputParamValues)
{
if (inputParamValues != null)
{
LogUtil.debug(getClass(), "Binding request parameters to socket $0.", socket.getQualifier());
Iterator it = socket.getParams();
if (it.hasNext())
{
// Iterate all parameters of the exit socket
while (it.hasNext())
{
NodeParam nodeParam = (NodeParam) it.next();
bindInputParameter(context, nodeParam, inputParamValues);
}
}
}
}
/**
* Handle the parameter binding debending of the dynamic visual.
*
* If there is used a dynamic visual, the parameter of the visual is
* important in regard to the data type for the parameter binding.
*
* Normally the datatype of the node parameter is used.
*
* @param context Token context
* @param param Node parameter
* @param inputParamValues A map of parameter name/parameter value pairs or null
* @throws OpenBPException On any exception
*/
private void bindInputParameter(TokenContext context, NodeParam param, Map inputParamValues)
{
String name = param.getName();
DataTypeItem type = param.getDataType();
if (type == null)
// No type specified, skip this parameter
return;
// Get the current value of the parameter as default we can operate on
// if it exists
Object value = inputParamValues != null ? inputParamValues.get(name) : null;
if (type.isSimpleType())
{
// Get the parameter value
try
{
if (value != null)
{
if (value instanceof String)
{
// Try to convert the string representation to the type
// we expect for this parameter
value = type.convertFromString((String) value, null, null);
}
else
{
// Check if the value type matches the parameter type;
// otherwise the application programmer obviously supplied the wrong type of data in the input parameters.
if (! type.getJavaClass().isAssignableFrom(value.getClass()))
throw new EngineException("IncorrectParameterType", "Cannot bind value of type "
+ value.getClass() + " to parameter '" + param.getQualifier() + "' (type "
+ type.getJavaClass() + ").");
}
}
}
catch (ValidationException e)
{
// Conversion failed, log the error
throw new EngineException("ParamterValidation", "Error binding value of type " + (value != null ? value.getClass() : "?")
+ " to parameter '" + param.getQualifier() + "' (type " + type.getJavaClass() + ").", e);
}
}
else
{
// Complex type; instantiate it if not present yet
if (value != null)
{
if (! type.getJavaClass().isAssignableFrom(value.getClass()))
throw new EngineException("IncorrectParameterType", "Cannot bind value of type " + value.getClass()
+ " to parameter '" + param.getQualifier() + "' (type " + type.getJavaClass() + ").");
}
}
// Assign the result to the parameter
TokenContextUtil.setParamValue(context, param, value);
}
/**
* Begins a transaction on the token context store.
*/
public void begin()
{
engine.begin();
}
/**
* Commits the transaction on the token context store.
*/
public void commit()
{
engine.commit();
}
/**
* Rolls back the transaction on the token context store.
*/
public void rollback()
{
engine.rollback();
}
/**
* Retrieves the output variables of the given context.
* Collects the values of all parameters of the current socket
* (i. e. the exit socket the process ended with) and stores them in the supplied map.
* @param context Token context
* @param outputParamValues Map to fill
*/
public void retrieveOutputParameters(TokenContext context, Map outputParamValues)
{
// Copy parameters of exit socket
NodeSocket socket = context.getCurrentSocket();
if (socket == null)
return;
for (Iterator it = socket.getParams(); it.hasNext();)
{
NodeParam param = (NodeParam) it.next();
Object value = TokenContextUtil.getParamValue(context, param);
outputParamValues.put(param.getName(), value);
}
}
/**
* Retrieves a token context by its id.
*
* @param id Context id
* @return The context or null if no such context exists
*/
public TokenContext getTokenById(Object id)
{
return engine.getTokenContextService().getContextById(id);
}
/**
* Returns an iterator of token contexts that match the given selection criteria.
*
* @param criteria Criteria to match
* @param maxResults Maximum number of result records or 0 for all
* @return An iterator of {@link TokenContext} objects.
* The objects will be sorted by their priority (ascending).
*/
public Iterator getTokens(TokenContextCriteria criteria, int maxResults)
{
return engine.getTokenContextService().getContexts(criteria, maxResults);
}
/**
* Returns an iterator of workflow tasks that match the given selection criteria.
*
* @param criteria Criteria to match
* @return An iterator of {@link WorkflowTask} objects
*/
public Iterator getworkflowTasks(WorkflowTaskCriteria criteria)
{
return engine.getTokenContextService().getworkflowTasks(criteria);
}
/**
* Resets the state of tokens that were currently executing after a system crash.
* Each token of the specified node in the state {@link LifecycleState#SELECTED} or {@link LifecycleState#RUNNING}
* will be set to LifecycleState.SUSPENDED and LifecycleRequest.RESUME.
* This will cause the engine to resume the process.
* Call this method during application startup to prevent tokens from being lost due to a system crash.
* However, NEVER call this method during normal system operation, which might set currently running
* tokens to an invalid state.
*
* @param nodeId Node id or null for the current node
* @return The number of tokens that have been updated
*/
public int resetExecutingTokenState(String nodeId)
{
if (nodeId == null)
{
nodeId = engineRunner.getSystemNameProvider().getSystemName();
}
int ret = engine.getTokenContextService().changeContextState(LifecycleState.SELECTED, LifecycleState.SUSPENDED, LifecycleRequest.RESUME, nodeId);
ret += engine.getTokenContextService().changeContextState(LifecycleState.RUNNING, LifecycleState.SUSPENDED, LifecycleRequest.RESUME, nodeId);
return ret;
}
//////////////////////////////////////////////////
// @@ Process execution
//////////////////////////////////////////////////
/**
* Main execution loop.
* Use this method in the thread that reads pending token contexts and distributes them for execution.
* The method never returns!
* @param sleepTime When there are no context available for execution, the method will sleep for the supplied sleep time (in milli seconds).
*/
public void mainExecutionLoop(int sleepTime)
{
engineRunner.mainExecutionLoop(sleepTime);
}
/**
* Requests the end of the main execution loop and wait until all currently
* executing contexts have come to an halt.
* The method will return if either all contexts have finished executing
* (i. e. reached a wait state or have ended) or if the specified timeout has elapsed.
*
* @param timeoutMS Timeout in milliseconds.
* If this value is 0, the method will just check if everything has completed, but will not wait for any processes.
* If this value is -1, no timeout will apply (i. e. the method will definately wait
* until all context executions have finished).
* @return true If no context is currently executing, false if the timeout has elapsed
*/
public boolean waitForStop(long timeoutMS)
{
return engineRunner.waitForStop(timeoutMS);
}
/**
* Executes token contexts that are ready for execution in a different thread.
* The method will query the executable contexts using the token context service.
* Each context retrieved will be executed using the thread distribution
* strategy of the particular {@link EngineRunner} implementation.
* The method returns after it has processed (i. e. distributed to the threads)
* the last element of the context list returned by the
* {@link TokenContextService#getExecutableContexts} method.
* Since the execution will be asynchronously, this does not mean that the contexts
* have finished their execution at this point in time.
* @return The number of contexts that have been retrieved for execution and passed to the thread pool
*/
public int executePendingContextsInDifferentThread()
{
return engineRunner.executePendingContextsInDifferentThread();
}
/**
* Executes token contexts that are ready fro execution in this thread.
* Since this will block the thread, this method is intended for test cases primarily.
* The method will query the executable contexts using the token context service.
* Each context retrieved will be executed directly in this thread.
* The method returns after it has processed the last element of the context list
* returned by the {@link TokenContextService#getExecutableContexts} method.
* @return true if the method has found contexts for execution, false otherwise
*/
public boolean executePendingContextsInThisThread()
{
return engineRunner.executePendingContextsInThisThread();
}
/**
* Executes the given context immediately in this thread.
* Since this will block the thread, this method is intended for test cases and for situations where
* the completion of a process execution is needed in order to continue the program.
*
* @param context Context to execute
*/
public void executeContextInThisThread(TokenContext context)
{
engineRunner.executeContextInThisThread(context);
}
//////////////////////////////////////////////////
// @@ Component setter/getter
//////////////////////////////////////////////////
/**
* Gets the engine.
*/
public Engine getEngine()
{
return engine;
}
/**
* Sets the engine.
*/
public void setEngine(Engine engine)
{
this.engine = engine;
}
/**
* Gets the engine runner.
*/
public EngineRunner getEngineRunner()
{
return engineRunner;
}
/**
* Sets the engine runner.
*/
public void setEngineRunner(EngineRunner engineRunner)
{
this.engineRunner = engineRunner;
}
}