package org.jboss.seam.pageflow;
import static org.jboss.seam.annotations.Install.BUILT_IN;
import java.io.Serializable;
import java.util.List;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseId;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.PerNestedConversation;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.bpm.Jbpm;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.contexts.FacesLifecycle;
import org.jboss.seam.core.AbstractMutable;
import org.jboss.seam.core.Events;
import org.jboss.seam.faces.FacesManager;
import org.jboss.seam.faces.FacesMessages;
import org.jboss.seam.faces.FacesPage;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.navigation.Pages;
import org.jbpm.JbpmContext;
import org.jbpm.graph.def.Action;
import org.jbpm.graph.def.Event;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.ProcessInstance;
/**
* A Seam component that manages the current
* jBPM ProcessInstance used for pageflow.
*
* @author Gavin King
*/
@Scope(ScopeType.CONVERSATION)
@PerNestedConversation
@Name("org.jboss.seam.pageflow.pageflow")
@BypassInterceptors
@Install(dependencies="org.jboss.seam.bpm.jbpm", precedence=BUILT_IN, classDependencies="javax.faces.context.FacesContext")
public class Pageflow extends AbstractMutable implements Serializable
{
private static final long serialVersionUID = -2337682140346213333L;
private static final LogProvider log = Logging.getLogProvider(Pageflow.class);
private int counter;
private ProcessInstance processInstance;
public boolean isInProcess()
{
return processInstance!=null;
}
public ProcessInstance getProcessInstance()
{
return processInstance;
}
public void setProcessInstance(ProcessInstance processInstance)
{
this.processInstance = processInstance;
setDirty();
}
public static Pageflow instance()
{
if ( !Contexts.isConversationContextActive() )
{
throw new IllegalStateException("No active conversation context");
}
return (Pageflow) Component.getInstance(Pageflow.class, ScopeType.CONVERSATION);
}
/**
* Get the current counter value, used for detecting
* illegal use of the backbutton.
*/
public int getPageflowCounter()
{
return counter;
}
/**
* Check that the current state of the pageflow matches
* what is expected by the faces request.
*/
public void validatePageflow(FacesContext facesContext)
{
if ( processInstance!=null )
{
org.jboss.seam.faces.FacesPage page = org.jboss.seam.faces.FacesPage.instance();
String pageflowName = page.getPageflowName();
String pageflowNodeName = page.getPageflowNodeName();
boolean canReposition = getPage().isBackEnabled() &&
getSubProcessInstance().getProcessDefinition().getName().equals(pageflowName) && //possibly not necessary?
pageflowNodeName!=null;
if (canReposition)
{
//check the node name to make sure we are still on the same node
if ( !pageflowNodeName.equals( getNode().getName() ) )
{
//legal use of back/forward button, so reposition the pageflow
reposition(pageflowNodeName);
}
}
else
{
//check the counter to detect illegal use of backbutton
//(we user counter instead of view id since multiple
//nodes can share the same view id)
Integer pageCounter = org.jboss.seam.faces.FacesPage.instance().getPageflowCounter();
if ( pageCounter!=null && getPageflowCounter()!=pageCounter )
{
illegalNavigationError();
}
}
//now check that the restored view id matches what we expect
//from the pageflow node
//TODO: we need some way to disable this check, since users
// might want some adhoc nav in and out of a pageflow?
String viewId = Pages.getViewId(facesContext);
if ( !viewId.equals( getPage().getViewId() ) )
{
illegalNavigationError();
}
}
}
private void illegalNavigationError()
{
FacesContext context = FacesContext.getCurrentInstance();
navigate(context);
illegalNavigation();
context.renderResponse();
}
/**
* Add a message to indicate that illegal navigation
* occurred. May be overridden by user to perform
* special processing.
*/
protected void illegalNavigation()
{
FacesMessages.instance().addFromResourceBundleOrDefault(
FacesMessage.SEVERITY_WARN,
"org.jboss.seam.IllegalNavigation",
"Illegal navigation"
);
}
/**
* Get the current Node of the pageflow.
*/
public Node getNode()
{
if (processInstance==null) return null;
Node node = getSubProcessInstance().getRootToken().getNode();
if (node==null)
{
throw new IllegalStateException("pageflow has not yet started");
}
return node;
}
public ProcessInstance getSubProcessInstance()
{
return getSubProcess(processInstance);
}
private static ProcessInstance getSubProcess(ProcessInstance processInstance)
{
ProcessInstance subProcess = processInstance.getRootToken().getSubProcessInstance();
if (subProcess!=null)
{
return getSubProcess(subProcess);
}
else
{
return processInstance;
}
}
/**
* Reposition the pageflow at the named node.
*
* @param nodeName the name of a node
*/
public void reposition(String nodeName)
{
if (processInstance==null)
{
throw new IllegalStateException("no pageflow in progress");
}
ProcessInstance subProcess = getSubProcessInstance();
Node node = subProcess.getProcessDefinition().getNode(nodeName);
if (node==null)
{
throw new IllegalArgumentException(
"no node named: " + nodeName +
" for pageflow: " + subProcess.getProcessDefinition().getName()
);
}
subProcess.getRootToken().setNode(node);
setDirty();
}
/**
* Get the current Page of the pageflow.
*/
public Page getPage()
{
Node node = getNode();
if ( node!=null && !(node instanceof Page) )
{
throw new IllegalStateException("pageflow is not currently at a <page> or <start-page> node (note that pageflows that begin during the RENDER_RESPONSE phase should use <start-page> instead of <start-state>)");
}
return (Page) node;
}
/**
* Navigate to the current page.
*/
protected void navigate(FacesContext context)
{
Page page = getPage();
if ( !page.isRedirect() )
{
render(context, page);
}
else
{
redirect(page);
}
counter++;
setDirty();
}
/**
* Redirect to the Page.
*/
protected void redirect(Page page)
{
FacesManager.instance().redirect( getViewId(page) );
}
/**
* Proceed to render the Page.
*/
protected void render(FacesContext context, Page page)
{
UIViewRoot viewRoot = context.getApplication().getViewHandler()
.createView( context, getViewId(page) );
context.setViewRoot(viewRoot);
}
/**
* Allows the user to extend this class and use some
* logical naming of pages other than the JSF view id
* in their pageflow.
*
* @param page the Page object
* @return a JSF view id
*/
protected String getViewId(Page page)
{
return page.getViewId();
}
/**
* Get the JSF view id of the current page in the
* pageflow.
*/
public String getPageViewId()
{
Page page = getPage();
return page==null ? null : getViewId(page);
}
/**
* Does the current node have a default transition?
*/
public boolean hasDefaultTransition()
{
//we don't use jBPM's default transition,
//instead we use the "anonymous" transition
return getNode().getLeavingTransition(null)!=null;
}
private boolean isNullOutcome(String outcome)
{
return outcome==null || "".equals(outcome);
}
public boolean hasTransition(String outcome)
{
return isNullOutcome(outcome) ?
hasDefaultTransition() :
getNode().getLeavingTransition(outcome)!=null;
}
/**
* Given the JSF action outcome, perform navigation according
* to the current pageflow.
*/
public void navigate(FacesContext context, String outcome)
{
ProcessInstance subProcess = getSubProcessInstance();
if ( isNullOutcome(outcome) )
{
//if it has a default transition defined, trigger it,
//otherwise just redisplay the page
if ( hasDefaultTransition() )
{
//we don't use jBPM's default transition,
//instead we use the "anonymous" transition
Pageflow.signal(subProcess, null);
navigate(context);
}
}
else
{
//trigger the named transition
Pageflow.signal(subProcess, outcome);
navigate(context);
}
raiseEndEventIfNecessary();
}
protected void raiseEndEventIfNecessary()
{
if ( processInstance.hasEnded() )
{
Events.instance().raiseEvent(
"org.jboss.seam.endPageflow." +
processInstance.getProcessDefinition().getName()
);
}
}
/**
* Process events defined in the pageflow.
*
* @param type one of: "process-validations", "update-model-values",
* "invoke-application", "render-response"
*/
public void processEvents(String type)
{
Event event = getNode().getEvent(type);
if (event!=null)
{
for ( Action action: (List<Action>) event.getActions() )
{
try
{
action.execute( ExecutionContext.currentExecutionContext() );
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
}
/**
* Begin executing a pageflow.
*
* @param pageflowDefinitionName the name of the pageflow definition
*/
public void begin(String pageflowDefinitionName)
{
if ( log.isDebugEnabled() )
{
log.debug("beginning pageflow: " + pageflowDefinitionName);
}
processInstance = Pageflow.createInstance( getPageflowProcessDefinition(pageflowDefinitionName) );
//if ( Lifecycle.getPhaseId().equals(PhaseId.RENDER_RESPONSE) )
//{
//if a pageflow starts during the render response phase
//(as a result of a @Create method), we know the navigation
//handler will not get called, so we should force the
//pageflow out of the start state immediately
//TODO: this is not actually completely true, what about <s:actionLink/>
//pi.signal();
//}
setDirty();
raiseBeginEvent(pageflowDefinitionName);
storePageflowToViewRootIfNecessary();
}
protected void raiseBeginEvent(String pageflowDefinitionName)
{
Events.instance().raiseEvent("org.jboss.seam.beginPageflow." + pageflowDefinitionName);
}
private void storePageflowToViewRootIfNecessary()
{
FacesContext facesContext = FacesContext.getCurrentInstance();
if ( facesContext!=null && FacesLifecycle.getPhaseId()==PhaseId.RENDER_RESPONSE )
{
FacesPage.instance().storePageflow();
}
}
public String getNoConversationViewId(String pageflowName, String pageflowNodeName)
{
ProcessDefinition pageflowProcessDefinition = getPageflowProcessDefinition(pageflowName);
Node node = pageflowProcessDefinition.getNode(pageflowNodeName);
if (node!=null && node instanceof Page)
{
return ( (Page) node ).getNoConversationViewId();
}
else
{
return null;
}
}
protected ProcessDefinition getPageflowProcessDefinition(String pageflowName)
{
ProcessDefinition pageflowProcessDefinition = Jbpm.instance().getPageflowProcessDefinition(pageflowName);
if (pageflowProcessDefinition==null)
{
throw new IllegalArgumentException("pageflow definition not found: " + pageflowName);
}
return pageflowProcessDefinition;
}
@Override
public String toString()
{
String name = processInstance==null ?
"null" : processInstance.getProcessDefinition().getName();
return "Pageflow(" + name + ")";
}
private static ProcessInstance createInstance(ProcessDefinition processDefinition)
{
JbpmContext jbpmContext = Jbpm.createPageflowContext();
try
{
log.debug( "new pageflow instance for definition: " + processDefinition.getName() );
return processDefinition.createProcessInstance();
}
finally
{
jbpmContext.close();
}
}
private static void signal(ProcessInstance processInstance, String outcome)
{
JbpmContext jbpmContext = Jbpm.createPageflowContext();
try
{
log.debug("signaling pageflow transition for outcome: " + outcome);
processInstance.signal(outcome);
}
finally
{
jbpmContext.close();
}
}
}