/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ /* * You may build in a package on your choice. Dependency information: * * Commons SCXML dependencies - * http://commons.apache.org/scxml/dependencies.html * * Apache Shale dependencies - * http://shale.apache.org/dependencies.html */ package org.apache.commons.scxml.usecases; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.faces.application.NavigationHandler; import javax.faces.application.ViewHandler; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import org.apache.commons.digester.Digester; import org.apache.commons.digester.Rule; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.scxml.SCXMLDigester; import org.apache.commons.scxml.SCXMLExecutor; import org.apache.commons.scxml.TriggerEvent; import org.apache.commons.scxml.env.SimpleDispatcher; import org.apache.commons.scxml.env.SimpleErrorHandler; import org.apache.commons.scxml.env.SimpleErrorReporter; import org.apache.commons.scxml.env.SimpleSCXMLListener; import org.apache.commons.scxml.env.faces.SessionContext; import org.apache.commons.scxml.env.faces.ShaleDialogELEvaluator; import org.apache.commons.scxml.model.ModelException; import org.apache.commons.scxml.model.SCXML; import org.apache.commons.scxml.model.TransitionTarget; import org.apache.shale.dialog.Globals; import org.apache.shale.dialog.Status; import org.xml.sax.Attributes; import org.xml.sax.InputSource; /** *
SCXML configuration file(s) driven Shale dialog navigation handler.
* *Recipe for using SCXML documents to drive Shale dialogs: *
SCXMLDialogNavigationHandler
(available
* below, use a Commons SCXML nightly build 10/09/05 or later) and make it
* available to your web application classpath (WEB-INF/classes
).
* WEB-INF/faces-config.xml
"
* for your web application such that the
* "faces-config/application/navigation-handler
"
* entry points to
* "org.apache.commons.scxml.usecases.SCXMLDialogNavigationHandler
"
* (with the appropriate package name, if you changed it).
* WEB-INF/lib
directory which contains the
* SCXMLDialogNavigationHandler
and a
* META-INF/faces-config.xml
with just the entry in (2).handleNavigation()
is called with a logical outcome
* of the form "dialog:xxx
" and there is no current
* dialog in progress, where "xxx
" is the URL pointing
* to the SCXML document.Using SCXML documents to define the Shale dialog "flows": *
Towards pluggable dialog management in Shale - A "black box" * dialog may consist of the following tuple: *
Create a new {@link SCXMLDialogNavigationHandler}, wrapping the * specified standard navigation handler implementation.
* * @param handler StandardNavigationHandler
we are wrapping
*/
public SCXMLDialogNavigationHandler(NavigationHandler handler) {
this.handler = handler;
}
// -------------------------------------------------------- Static Variables
/**
* The prefix on a logical outcome String that indicates the remainder * of the string is the URL of a SCXML-based Shale dialog to be entered.
*/ public static final String PREFIX = "dialog:"; // ------------------------------------------------------ Instance Variables /** *The standard NavigationHandler
implementation that
* we are wrapping.
The Log
instance for this class.
Key under which we will store the SCXMLExecutor (more generally, * some session scoped state pertaining to the current dialog).
*/ private String dialogKey = null; // Cached on first use /** *Map storing SCXML state IDs as keys and JSF view IDs as values.
*/ private Map target2viewMap = null; // ----------------------------------------------- NavigationHandler Methods /** *Handle the navigation request implied by the specified parameters.
* * @param contextFacesContext
for the current request
* @param fromAction The action binding expression that was evaluated
* to retrieve the specified outcome (if any)
* @param outcome The logical outcome returned by the specified action
*
* @exception IllegalArgumentException if the configuration information
* for a previously saved position cannot be found
* @exception IllegalArgumentException if an unknown State type is found
*/
public void handleNavigation(FacesContext context, String fromAction,
String outcome) {
if (log.isDebugEnabled()) {
log.debug("handleNavigation(viewId=" +
context.getViewRoot().getViewId() +
",fromAction=" + fromAction +
",outcome=" + outcome + ")");
}
SCXMLExecutor exec = getDialogExecutor(context);
String viewId = null;
if (exec == null && outcome != null && outcome.startsWith(PREFIX)) {
/**** DIALOG ENTRY ****/
// dialog is a state machine, parse & obtain an executor
exec = initDialogExecutor(context, outcome.substring(PREFIX.
length()));
if (exec != null) {
// cache executor in session scope
// TODO: Shale caches Dialog instances. SCXMLExecutor
// knows what state(s) the dialog is in, so Dialog#findState()
// is not needed.
setDialogExecutor(context, exec);
// obtain our initial view
viewId = getCurrentViewId(exec);
}
// else delegate
} else if (exec != null) {
/**** SUBSEQUENT TURNS OF DIALOG ****/
// pass a handle to the current ctx (for evaluating binding exprs)
updateEvaluator(context, outcome);
// fire a "faces.outcome" event on the dialog's state machine
TriggerEvent[] te = { new TriggerEvent("faces.outcome",
TriggerEvent.SIGNAL_EVENT) };
try {
exec.triggerEvents(te);
} catch (ModelException me) {
log.error(me.getMessage(), me);
}
// obtain next view
viewId = getCurrentViewId(exec);
}
if (viewId != null) {
// we understood this "outcome" and we have a new view to render
log.info("Rendering view: " + viewId);
updateDialogStatus(context, exec);
render(context, viewId);
} else {
/**** DELEGATE BY DEFAULT ****/
handler.handleNavigation(context, fromAction, outcome);
}
}
/**
* Return the SCXMLExecutor for the specified SCXML document, if it
* exists; otherwise, return null
.
FacesContext
for the current request
* @param dialogIdentifier URL of the SCXML document for the requested
* dialog
*/
private SCXMLExecutor initDialogExecutor(FacesContext context,
String dialogIdentifier) {
assert context != null;
assert dialogIdentifier != null;
// We're parsing the SCXML dialog just in time here
URL scxmlDocument = null;
try {
scxmlDocument = context.getExternalContext().
getResource(dialogIdentifier);
} catch (MalformedURLException mue) {
log.error(mue.getMessage(), mue);
}
if (scxmlDocument == null) {
log.warn("No SCXML document at: " + dialogIdentifier);
return null;
}
SCXML scxml = null;
ShaleDialogELEvaluator evaluator = new ShaleDialogELEvaluator();
evaluator.setFacesContext(context);
try {
scxml = SCXMLDigester.digest(scxmlDocument,
new SimpleErrorHandler(), new SessionContext(context),
evaluator);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
if (scxml == null) {
log.warn("Could not parse SCXML document at: " + dialogIdentifier);
return null;
}
SCXMLExecutor exec = null;
try {
exec = new SCXMLExecutor(evaluator, new SimpleDispatcher(),
new SimpleErrorReporter());
scxml.addListener(new SimpleSCXMLListener());
exec.setSuperStep(true);
exec.setStateMachine(scxml);
} catch (ModelException me) {
log.warn(me.getMessage(), me);
return null;
}
// read SCXML state IDs to JSF view IDs map, channel dependent
readState2ViewMap(context, dialogIdentifier, null);
// FIXME: Remove dependence on the org.apache.shale.dialog.impl package
// below (introduced so we can reuse the existing StatusImpl and the
// AbstractFacesBean subtypes in the usecases war for the proof of
// concept).
// Ignoring STATUS_PARAM since usecases war doesn't use it for the
// log on / edit profile dialogs.
// TODO: The next line should be Dialog Manager implementation agnostic
Status status = new org.apache.shale.dialog.impl.StatusImpl();
context.getExternalContext().getSessionMap().put(Globals.STATUS, status);
status.push(new Status.Position(dialogIdentifier, getCurrentViewId(exec)));
return exec;
}
/**
* Set the {@link SCXMLExecutor} instance for the current user.
* * @param contextFacesContext
for the current request
* @param exec SCXMLExecutor
that will run the dialog
*/
private void setDialogExecutor(FacesContext context, SCXMLExecutor exec) {
assert context != null;
assert exec != null;
Map map = context.getExternalContext().getSessionMap();
String key = getDialogKey(context);
assert key != null;
map.put(key, exec);
}
/**
* Return the {@link SCXMLExecutor} instance for the current user.
* * @param contextFacesContext
for the current request
*/
private SCXMLExecutor getDialogExecutor(FacesContext context) {
assert context != null;
Map map = context.getExternalContext().getSessionMap();
String key = getDialogKey(context);
return (SCXMLExecutor) map.get(key);
}
/**
* Update evaluator with current FacesContext for evaluation of
* binding expressions used in Shale dialog.
*/
private void updateEvaluator(FacesContext context, String outcome) {
assert context != null;
((ShaleDialogELEvaluator) getDialogExecutor(context).getEvaluator()).
setFacesContext(context);
context.getExternalContext().getSessionMap().put("outcome", outcome);
}
/**
* Update dialog Status
*
* @param context The FacesContext
* @param exec The SCXMLExecutor
*/
private void updateDialogStatus(FacesContext context, SCXMLExecutor exec) {
assert context != null;
assert exec != null;
// TODO: Test this
Status status = (Status) context.getExternalContext().getSessionMap().
get(Globals.STATUS);
if (exec.getCurrentStatus().isFinal()) {
setDialogExecutor(context, null);
status.pop();
} else {
status.peek().setStateName(getCurrentViewId(exec));
}
}
/**
* Get next view to render, assuming one view at a time.
*
* @param currentStates The set of current states
* @return String The JSF viewId of the next view
*/
private String getCurrentViewId(SCXMLExecutor exec) {
assert exec != null;
Set currentStates = exec.getCurrentStatus().getStates();
for (Iterator i = currentStates.iterator(); i.hasNext(); ) {
String targetId = ((TransitionTarget) i.next()).getId();
if (target2viewMap.containsKey(targetId)) {
return (String) target2viewMap.get(targetId);
}
}
return null;
}
/**
* Return the session scope attribute key under which we will
* store dialog state for the current user. The value
* is specified by a context init parameter named by constant
* Globals.DIALOG_STATE_PARAM
, or defaults to the value
* specified by constant Globals.DIALOG_STATE
.
FacesContext
for the current request
*/
private String getDialogKey(FacesContext context) {
assert context != null;
if (dialogKey == null) {
dialogKey =
context.getExternalContext().
getInitParameter(Globals.DIALOG_STATE_PARAM);
if (dialogKey == null) {
dialogKey = Globals.DIALOG_STATE;
}
}
return dialogKey;
}
/**
* Render the view corresponding to the specified view identifier.
* * @param contextFacesContext
for the current request
* @param viewId View identifier to be rendered, or null
* to rerender the current view
*/
private void render(FacesContext context, String viewId) {
assert context != null;
if (log.isDebugEnabled()) {
log.debug("render(viewId=" + viewId + ")");
}
// Stay on the same view if requested
if (viewId == null) {
return;
}
// Create the specified view so that it can be rendered
ViewHandler vh = context.getApplication().getViewHandler();
UIViewRoot view = vh.createView(context, viewId);
view.setViewId(viewId);
context.setViewRoot(view);
}
/**
* FIXME: - Placeholder for SCXML state ID to JSF view ID mapper.
* Provides multi-channel aspect to Shale dialog management.
*
*/
private void readState2ViewMap(FacesContext context,
String dialogIdentifier, String channel) {
assert context != null;
String STATE_TO_VIEW_MAP = "/WEB-INF/dialogstate2view.xml";
target2viewMap = new HashMap();
Digester digester = new Digester();
digester.clear();
digester.setNamespaceAware(false);
digester.setUseContextClassLoader(false);
digester.setValidating(false);
digester.addRule("map/entry", new Rule() {
/** SCXML target ID. */
private String targetId;
/** JSF view ID. */
private String viewId;
/** {@inheritDoc} */
public final void begin(final String namespace, final String name,
final Attributes attributes) {
targetId = attributes.getValue("targetId");
viewId = attributes.getValue("viewId");
}
/** {@inheritDoc} */
public void end(final String namespace, final String name) {
target2viewMap.put(targetId, viewId);
}
});
try {
URL mapURL = context.getExternalContext().getResource(STATE_TO_VIEW_MAP);
InputSource source = new InputSource(mapURL.toExternalForm());
source.setByteStream(mapURL.openStream());
digester.parse(source);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}