Package org.apache.commons.scxml.usecases

Source Code of org.apache.commons.scxml.usecases.SCXMLDialogNavigationHandler

/*
* Copyright 2005-2006 The Apache Software Foundation.
*
* 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.
*/
/*
* You may build in a package on your choice. Dependency information:
*
* Commons SCXML dependencies -
* http://jakarta.apache.org/commons/scxml/dependencies.html
*
* Struts Shale dependencies -
* http://struts.apache.org/shale/
* (Currently under "Foundations" section)
*/
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;

/**
* <p>SCXML configuration file(s) driven Shale dialog navigation handler.</p>
*
* <p>Recipe for using SCXML documents to drive Shale dialogs:
*   <ol>
*    <li>Build the <code>SCXMLDialogNavigationHandler</code> (available
*     below, use a Commons SCXML nightly build 10/09/05 or later) and make it
*     available to your web application classpath (<code>WEB-INF/classes</code>).
*    </li>
*    <li>Update the &quot;<code>WEB-INF/faces-config.xml</code>&quot;
*     for your web application such that the
*     &quot;<code>faces-config/application/navigation-handler</code>&quot;
*     entry points to
*     &quot;<code>org.apache.commons.scxml.usecases.SCXMLDialogNavigationHandler</code>&quot;
*     (with the appropriate package name, if you changed it).
*    </li>
*    <li>As an alternative to (1) and (2), you can place a <i>jar</i> in the
*     <code>WEB-INF/lib</code> directory which contains the
*     <code>SCXMLDialogNavigationHandler</code> and a
*     <code>META-INF/faces-config.xml</code> with just the entry in (2).</li>
*    <li>Use SCXML documents to describe Shale dialog flows (details below)
*     in your application. You may have multiple mappings from transition
*     targets to JSF views to support multi-channel applications.</li>
*    <li>The SCXML-based dialog is entered when
*     <code>handleNavigation()</code> is called with a logical outcome
*     of the form &quot;<code>dialog:xxx</code>&quot; and there is no current
*     dialog in progress, where &quot;<code>xxx</code>&quot; is the URL pointing
*     to the SCXML document.</li>
*   </ol>
* </p>
*
* <p>Using SCXML documents to define the Shale dialog "flows":
*   <ul>
*     <li>ActionState instances may be mapped to executable content
*         in UML &lt;onentry&gt (and may be chained similarly).</li>
*     <li>ViewState instances may be mapped to UML transition
*         targets.</li>
*     <li>SubdialogState instances may be mapped to external SCXML
*         documents.</li>
*     <li>EndState instances may be mapped to SCXML final states.</li>
*     <li>The {@link SCXMLDialogNavigationHandler} defines a
*         &quot;faces.outcome&quot; event which the relevant SCXML
*         transitions from a &quot;view state&quot; can wait for.</li>
*   </ul>
* </p>
*
* <p>Towards pluggable dialog management in Shale - A &quot;black box&quot;
*    dialog may consist of the following tuple:
*   <ul>
*     <li>Unique dialog identifier</li>
*     <li>A generic NavigationHandler (i.e. dialog strategy)</li>
*     <li>An dialog/flow configuration resource (Ex: SCXML document)</li>
*     <li>Optionally, multiple other configuration resources,
*      (Ex: one for each channel - web, voice, small device, etc.)</li>
*   </ul>
*    The Shale DialogNavigationHandler may then delegate appropriately.
* </p>
*/
public final class SCXMLDialogNavigationHandler extends NavigationHandler {

    // ------------------------------------------------------------ Constructors
    /**
     * <p>Create a new {@link SCXMLDialogNavigationHandler}, wrapping the
     * specified standard navigation handler implementation.</p>
     *
     * @param handler Standard <code>NavigationHandler</code> we are wrapping
     */
    public SCXMLDialogNavigationHandler(NavigationHandler handler) {

        this.handler = handler;

    }

    // -------------------------------------------------------- Static Variables
    /**
     * <p>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.</p>
     */
    public static final String PREFIX = "dialog:";

    // ------------------------------------------------------ Instance Variables

    /**
     * <p>The standard <code>NavigationHandler</code> implementation that
     * we are wrapping.</p>
     */
    private NavigationHandler handler = null;

    /**
     * <p>The <code>Log</code> instance for this class.</p>
     */
    private final Log log = LogFactory.getLog(getClass());

    /**
     * <p>Key under which we will store the SCXMLExecutor (more generally,
     * some session scoped state pertaining to the current dialog).</p>
     */
    private String dialogKey = null; // Cached on first use

    /**
     * <p>Map storing SCXML state IDs as keys and JSF view IDs as values.</p>
     */
    private Map target2viewMap = null;

    // ----------------------------------------------- NavigationHandler Methods

    /**
     * <p>Handle the navigation request implied by the specified parameters.</p>
     *
     * @param context <code>FacesContext</code> 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);

        }


    }

    /**
     * <p>Return the SCXMLExecutor for the specified SCXML document, if it
     * exists; otherwise, return <code>null</code>.</p>
     *
     * @param context <code>FacesContext</code> 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;
       
    }

    /**
     * <p>Set the {@link SCXMLExecutor} instance for the current user.</p>
     *
     * @param context <code>FacesContext</code> for the current request
     * @param exec <code>SCXMLExecutor</code> 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);
       
    }

    /**
     * <p>Return the {@link SCXMLExecutor} instance for the current user.</p>
     *
     * @param context <code>FacesContext</code> 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;
    }

    /**
     * <p>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
     * <code>Globals.DIALOG_STATE_PARAM</code>, or defaults to the value
     * specified by constant <code>Globals.DIALOG_STATE</code>.</p>
     *
     * @param context <code>FacesContext</code> 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;

    }

    /**
     * <p>Render the view corresponding to the specified view identifier.</p>
     *
     * @param context <code>FacesContext</code> for the current request
     * @param viewId View identifier to be rendered, or <code>null</code>
     *  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);
        }
    }

}
TOP

Related Classes of org.apache.commons.scxml.usecases.SCXMLDialogNavigationHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.