2009/05/20 - Apache Shale has been retired.

For more information, please explore the Attic.

Coverage Report - org.apache.shale.dialog.scxml.SCXMLDialogContext
 
Classes in this File Line Coverage Branch Coverage Complexity
SCXMLDialogContext
0%
0/164
0%
0/31
3.444
SCXMLDialogContext$DelegatingSCXMLListener
0%
0/7
N/A
3.444
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to you under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 
 18  
 package org.apache.shale.dialog.scxml;
 19  
 
 20  
 import java.io.IOException;
 21  
 import java.io.Serializable;
 22  
 import java.util.Iterator;
 23  
 import java.util.Map;
 24  
 import java.util.Set;
 25  
 
 26  
 import javax.faces.FacesException;
 27  
 import javax.faces.application.ViewHandler;
 28  
 import javax.faces.component.UIViewRoot;
 29  
 import javax.faces.context.ExternalContext;
 30  
 import javax.faces.context.FacesContext;
 31  
 import javax.faces.el.ValueBinding;
 32  
 
 33  
 import org.apache.commons.logging.Log;
 34  
 import org.apache.commons.logging.LogFactory;
 35  
 import org.apache.commons.scxml.Context;
 36  
 import org.apache.commons.scxml.SCXMLExecutor;
 37  
 import org.apache.commons.scxml.SCXMLListener;
 38  
 import org.apache.commons.scxml.TriggerEvent;
 39  
 import org.apache.commons.scxml.env.SimpleDispatcher;
 40  
 import org.apache.commons.scxml.env.SimpleErrorReporter;
 41  
 import org.apache.commons.scxml.env.jsp.ELContext;
 42  
 import org.apache.commons.scxml.model.ModelException;
 43  
 import org.apache.commons.scxml.model.SCXML;
 44  
 import org.apache.commons.scxml.model.State;
 45  
 import org.apache.commons.scxml.model.Transition;
 46  
 import org.apache.commons.scxml.model.TransitionTarget;
 47  
 import org.apache.shale.dialog.Constants;
 48  
 import org.apache.shale.dialog.DialogContext;
 49  
 import org.apache.shale.dialog.DialogContextListener;
 50  
 import org.apache.shale.dialog.DialogContextManager;
 51  
 import org.apache.shale.dialog.base.AbstractDialogContext;
 52  
 import org.apache.shale.dialog.scxml.config.DialogMetadata;
 53  
 
 54  
 /**
 55  
  * <p>Implementation of {@link DialogContextManager} for integrating
 56  
  * Commons SCXML into the Shale Dialog Manager.</p>
 57  
  *
 58  
  *
 59  
  * @since 1.0.4
 60  
  */
 61  0
 final class SCXMLDialogContext extends AbstractDialogContext
 62  
   implements Serializable {
 63  
 
 64  
 
 65  
     // ------------------------------------------------------------ Constructors
 66  
 
 67  
 
 68  
     /**
 69  
      * Serial version UID.
 70  
      */
 71  
     private static final long serialVersionUID = 8423853327094172716L;
 72  
 
 73  
 
 74  
     /**
 75  
      * <p>Construct a new instance.</p>
 76  
      *
 77  
      * @param manager {@link DialogContextManager} instance that owns us
 78  
      * @param dialog The dialog's metadata (whose executable instance needs
 79  
      *               to be created)
 80  
      * @param id Dialog identifier assigned to this instance
 81  
      * @param parentDialogId Dialog identifier assigned to the parent of
 82  
      *                       this instance
 83  
      */
 84  
     SCXMLDialogContext(DialogContextManager manager, DialogMetadata dialog, String id,
 85  0
                        String parentDialogId) {
 86  0
         this.manager = manager;
 87  0
         this.name = dialog.getName();
 88  0
         this.dataClassName = dialog.getDataclassname();
 89  0
         this.id = id;
 90  0
         this.parentDialogId = parentDialogId;
 91  
 
 92  
         // Create a working instance of the state machine for this dialog, but do not
 93  
         // set it in motion
 94  0
         this.executor = new SCXMLExecutor(new ShaleDialogELEvaluator(),
 95  
                         new SimpleDispatcher(), new SimpleErrorReporter());
 96  0
         SCXML statemachine = dialog.getStateMachine();
 97  0
         this.executor.setStateMachine(statemachine);
 98  0
         Context rootCtx = new ELContext();
 99  0
         rootCtx.setLocal(Globals.DIALOG_PROPERTIES, new DialogProperties());
 100  0
         this.executor.setRootContext(rootCtx);
 101  0
         this.executor.addListener(statemachine, new DelegatingSCXMLListener());
 102  
 
 103  0
         if (log().isDebugEnabled()) {
 104  0
             log().debug("Constructor(id=" + id + ", name="
 105  
                       + name + ")");
 106  
         }
 107  
 
 108  
         // TODO - Consider adding an explicit root context backed by either the
 109  
         // request or session map for greater EL capacities in the SCXML
 110  
         // document describing this dialog
 111  
 
 112  0
     }
 113  
 
 114  
 
 115  
     // ------------------------------------------------------ DialogContext Variables
 116  
 
 117  
 
 118  
     /**
 119  
      * <p>Flag indicating that this {@link DialogContext} is currently active.</p>
 120  
      */
 121  0
     private boolean active = true;
 122  
 
 123  
 
 124  
     /**
 125  
      * <p>Generic data object containing state information for this instance.</p>
 126  
      */
 127  0
     private Object data = null;
 128  
 
 129  
 
 130  
     /**
 131  
      * <p>Type of data object (FQCN to be instantiated).</p>
 132  
      */
 133  0
     private String dataClassName = null;
 134  
 
 135  
 
 136  
     /**
 137  
      * <p>Identifier of the parent {@link DialogContext} associated with
 138  
      * this {@link DialogContext}, if any.  If there is no such parent,
 139  
      * this value is set to <code>null</code>.</p>
 140  
      */
 141  0
     private String parentDialogId = null;
 142  
 
 143  
     /**
 144  
      * <p>Dialog identifier for this instance.</p>
 145  
      */
 146  0
     private String id = null;
 147  
 
 148  
 
 149  
     /**
 150  
      * <p>{@link DialogContextManager} instance that owns us.</p>
 151  
      */
 152  0
     private DialogContextManager manager = null;
 153  
 
 154  
 
 155  
     /**
 156  
      * <p>Logical name of the dialog to be executed.</p>
 157  
      */
 158  0
     private String name = null;
 159  
 
 160  
 
 161  
     /**
 162  
      * <p>The {@link SCXMLExecutor}, an instance of the state machine
 163  
      * defined for the SCXML document for this dialog.</p>
 164  
      *
 165  
      */
 166  0
     private SCXMLExecutor executor = null;
 167  
 
 168  
 
 169  
     /**
 170  
      * <p>Flag indicating that execution has started for this dialog.</p>
 171  
      */
 172  0
     private boolean started = false;
 173  
 
 174  
 
 175  
     /**
 176  
      * <p>The current SCXML state ID for this dialog instance, maintained
 177  
      * to reorient the dialog in accordance with any client-side navigation
 178  
      * between "view states" that may have happened since we last left off.
 179  
      * Serves as the "opaqueState" for this implementation.</p>
 180  
      */
 181  0
     private String stateId = null;
 182  
 
 183  
 
 184  
     /**
 185  
      * <p>The <code>Log</code> instance for this dialog context.
 186  
      * This value is lazily created (or recreated) as necessary.</p>
 187  
      */
 188  0
     private transient Log log = null;
 189  
 
 190  
 
 191  
     // ----------------------------------------------------- DialogContext Properties
 192  
 
 193  
 
 194  
     /** {@inheritDoc} */
 195  
     public boolean isActive() {
 196  0
         return this.active;
 197  
     }
 198  
 
 199  
 
 200  
     /** {@inheritDoc} */
 201  
     public Object getData() {
 202  0
          return this.data;
 203  
      }
 204  
 
 205  
 
 206  
 
 207  
     /** {@inheritDoc} */
 208  
     public void setData(Object data) {
 209  0
         Object old = this.data;
 210  0
         if ((old != null) && (old instanceof DialogContextListener)) {
 211  0
             removeDialogContextListener((DialogContextListener) old);
 212  
         }
 213  0
         this.data = data;
 214  0
         if ((data != null) && (data instanceof DialogContextListener)) {
 215  0
             addDialogContextListener((DialogContextListener) data);
 216  
         }
 217  0
     }
 218  
 
 219  
 
 220  
     /** {@inheritDoc} */
 221  
     public String getId() {
 222  0
         return this.id;
 223  
     }
 224  
 
 225  
 
 226  
     /** {@inheritDoc} */
 227  
     public String getName() {
 228  0
         return this.name;
 229  
     }
 230  
 
 231  
 
 232  
     /** {@inheritDoc} */
 233  
     public Object getOpaqueState() {
 234  
 
 235  0
         return stateId;
 236  
 
 237  
     }
 238  
 
 239  
 
 240  
     /** {@inheritDoc} */
 241  
     public void setOpaqueState(Object opaqueState) {
 242  
 
 243  0
         String viewStateId = String.valueOf(opaqueState);
 244  0
         if (viewStateId == null) {
 245  0
             throw new IllegalArgumentException("Dialog instance '" + getId()
 246  
                 + "' for dialog name '" + getName()
 247  
                 + "': null opaqueState received");
 248  
         }
 249  
 
 250  
         // account for user agent navigation
 251  0
         if (!viewStateId.equals(stateId)) {
 252  
 
 253  0
             if (log().isTraceEnabled()) {
 254  0
                 log().trace("Dialog instance '" + getId() + "' of dialog name '"
 255  
                     + getName() + "': user navigated to view for state '"
 256  
                     + viewStateId + "', setting dialog to this state instead"
 257  
                     + " of '" + stateId + "'");
 258  
             }
 259  
 
 260  0
             Map targets = executor.getStateMachine().getTargets();
 261  0
             State serverState = (State) targets.get(stateId);
 262  0
             State clientState = (State) targets.get(viewStateId);
 263  0
             if (clientState == null) {
 264  0
                 throw new IllegalArgumentException("Dialog instance '"
 265  
                     + getId() + "' for dialog name '" + getName()
 266  
                     + "': opaqueState is not a SCXML state ID for the "
 267  
                     + "current dialog state machine");
 268  
             }
 269  
 
 270  0
             Set states = executor.getCurrentStatus().getStates();
 271  0
             if (states.size() != 1) {
 272  0
                 throw new IllegalStateException("Dialog instance '"
 273  
                     + getId() + "' for dialog name '" + getName()
 274  
                     + "': Cannot have multiple leaf states active when the"
 275  
                     + " SCXML dialog is in a 'view' state");
 276  
             }
 277  
 
 278  
             // remove last known server-side state, set to correct
 279  
             // client-side state and fire the appropriate DCL events
 280  0
             states.remove(serverState);
 281  0
             fireOnExit(serverState.getId());
 282  
 
 283  0
             fireOnEntry(clientState.getId());
 284  0
             states.add(clientState);
 285  
 
 286  
         }
 287  
 
 288  0
     }
 289  
 
 290  
 
 291  
     /** {@inheritDoc} */
 292  
     public DialogContext getParent() {
 293  
 
 294  0
         if (this.parentDialogId != null) {
 295  0
             DialogContext parent = manager.get(this.parentDialogId);
 296  0
             if (parent == null) {
 297  0
                 throw new IllegalStateException("Dialog instance '"
 298  
                         + parentDialogId + "' was associated with this instance '"
 299  
                         + getId() + "' but is no longer available");
 300  
             }
 301  0
             return parent;
 302  
         } else {
 303  0
             return null;
 304  
         }
 305  
 
 306  
     }
 307  
 
 308  
 
 309  
     // -------------------------------------------------------- DialogContext Methods
 310  
 
 311  
 
 312  
     /** {@inheritDoc} */
 313  
     public void advance(FacesContext context, String outcome) {
 314  
 
 315  0
         if (!started) {
 316  0
             throw new IllegalStateException("Dialog instance '"
 317  
                     + getId() + "' for dialog name '"
 318  
                     + getName() + "' has not yet been started");
 319  
         }
 320  
 
 321  0
         if (log().isDebugEnabled()) {
 322  0
             log().debug("advance(id=" + getId() + ", name=" + getName()
 323  
                       + ", outcome=" + outcome + ")");
 324  
         }
 325  
 
 326  
         // If the incoming outcome is null, we want to stay in the same
 327  
         // (view) state *without* recreating it, which would destroy
 328  
         // any useful information that components might have stored
 329  0
         if (outcome == null) {
 330  0
             if (log().isTraceEnabled()) {
 331  0
                 log().trace("advance(outcome is null, stay in same view)");
 332  
             }
 333  0
             return;
 334  
         }
 335  
 
 336  0
         ((ShaleDialogELEvaluator) executor.getEvaluator()).
 337  
                     setFacesContext(context);
 338  0
         executor.getRootContext().setLocal(Globals.POSTBACK_OUTCOME, outcome);
 339  
 
 340  
         try {
 341  0
             executor.triggerEvent(new TriggerEvent(Globals.POSTBACK_EVENT,
 342  
                                 TriggerEvent.SIGNAL_EVENT));
 343  0
         } catch (ModelException me) {
 344  0
             fireOnException(me);
 345  0
         }
 346  
 
 347  0
         Iterator iterator = executor.getCurrentStatus().getStates().iterator();
 348  0
         this.stateId = ((State) iterator.next()).getId();
 349  0
         DialogProperties dp = (DialogProperties) executor.getRootContext().
 350  
             get(Globals.DIALOG_PROPERTIES);
 351  
 
 352  
         // If done, stop context
 353  0
         if (executor.getCurrentStatus().isFinal()) {
 354  0
             stop(context);
 355  
         }
 356  
 
 357  0
         navigateTo(stateId, context, dp);
 358  
 
 359  0
     }
 360  
 
 361  
 
 362  
     /** {@inheritDoc} */
 363  
     public void start(FacesContext context) {
 364  
 
 365  0
         if (started) {
 366  0
             throw new IllegalStateException("Dialog instance '"
 367  
                     + getId() + "' for dialog name '"
 368  
                     + getName() + "' has already been started");
 369  
         }
 370  0
         started = true;
 371  
 
 372  0
         if (log().isDebugEnabled()) {
 373  0
             log().debug("start(id=" + getId() + ", name="
 374  
                       + getName() + ")");
 375  
         }
 376  
 
 377  
         // inform listeners we're good to go
 378  0
         fireOnStart();
 379  
 
 380  
         // Construct an appropriate data object for the specified dialog
 381  0
         ClassLoader loader = Thread.currentThread().getContextClassLoader();
 382  0
         if (loader == null) {
 383  0
             loader = SCXMLDialogContext.class.getClassLoader();
 384  
         }
 385  0
         Class dataClass = null;
 386  
         try {
 387  0
             dataClass = loader.loadClass(dataClassName);
 388  0
             data = dataClass.newInstance();
 389  0
         } catch (Exception e) {
 390  0
             fireOnException(e);
 391  0
         }
 392  
 
 393  0
         if (data != null && data instanceof DialogContextListener) {
 394  0
             addDialogContextListener((DialogContextListener) data);
 395  
         }
 396  
 
 397  
         // set state machine in motion
 398  0
         ((ShaleDialogELEvaluator) executor.getEvaluator()).
 399  
             setFacesContext(context);
 400  
         try {
 401  0
             executor.go();
 402  0
         } catch (ModelException me) {
 403  0
             fireOnException(me);
 404  0
         }
 405  
 
 406  0
         Iterator iterator = executor.getCurrentStatus().getStates().iterator();
 407  0
         this.stateId = ((State) iterator.next()).getId();
 408  0
         DialogProperties dp = (DialogProperties) executor.getRootContext().
 409  
             get(Globals.DIALOG_PROPERTIES);
 410  
 
 411  
         // Might be done at the beginning itself, if so, stop context
 412  0
         if (executor.getCurrentStatus().isFinal()) {
 413  0
             stop(context);
 414  
         }
 415  
 
 416  0
         navigateTo(stateId, context, dp);
 417  
 
 418  0
     }
 419  
 
 420  
 
 421  
     /** {@inheritDoc} */
 422  
     public void stop(FacesContext context) {
 423  
 
 424  0
         if (!started) {
 425  0
             throw new IllegalStateException("Dialog instance '"
 426  
                     + getId() + "' for dialog name '"
 427  
                     + getName() + "' has not yet been started");
 428  
         }
 429  0
         started = false;
 430  
 
 431  0
         if (log().isDebugEnabled()) {
 432  0
             log().debug("stop(id=" + getId() + ", name="
 433  
                       + getName() + ")");
 434  
         }
 435  
 
 436  0
         deactivate();
 437  0
         manager.remove(this);
 438  
 
 439  
         // inform listeners
 440  0
         fireOnStop();
 441  
 
 442  0
     }
 443  
 
 444  
 
 445  
     // ------------------------------------------------- Package Private Methods
 446  
 
 447  
 
 448  
     /**
 449  
      * <p>Mark this {@link DialogContext} as being deactivated.  This should only
 450  
      * be called by the <code>remove()</code> method on our associated
 451  
      * {@link DialogContextManager}.</p>
 452  
      */
 453  
     void deactivate() {
 454  0
         setData(null);
 455  0
         this.active = false;
 456  0
     }
 457  
 
 458  
 
 459  
     //  ------------------------------------------------- Private Methods
 460  
 
 461  
 
 462  
     /**
 463  
      * <p>Navigate to the JavaServer Faces <code>view identifier</code>
 464  
      * that is mapped to by the current state identifier for this dialog.</p>
 465  
      *
 466  
      * @param stateId The current state identifier for this dialog.
 467  
      * @param context The current <code>FacesContext</code>
 468  
      * @param dp The <code>DialogProperties</code> for the current dialog
 469  
      */
 470  
     private void navigateTo(String stateId, FacesContext context, DialogProperties dp) {
 471  
         // Determine the view identifier
 472  0
         String viewId = dp.getNextViewId();
 473  0
         if (viewId == null) {
 474  0
             ValueBinding vb = context.getApplication().createValueBinding
 475  
                 ("#{" + Globals.STATE_MAPPER + "}");
 476  0
             DialogStateMapper dsm = (DialogStateMapper) vb.getValue(context);
 477  0
             viewId = dsm.mapStateId(name, stateId, context);
 478  0
         } else {
 479  0
             dp.setNextViewId(null); // one time use
 480  
         }
 481  
 
 482  
         // Navigate to the requested view identifier (if any)
 483  0
         if (viewId == null) {
 484  0
             return;
 485  
         }
 486  0
         if (!viewId.startsWith("/")) {
 487  0
             viewId = "/" + viewId;
 488  
         }
 489  
 
 490  
         // The public API is advance, so thats part of the message
 491  0
         if (log().isDebugEnabled()) {
 492  0
             log().debug("advance(id=" + getId() + ", name=" + getName()
 493  
                       + ", navigating to view: '" + viewId + "')");
 494  
         }
 495  
 
 496  0
         ViewHandler vh = context.getApplication().getViewHandler();
 497  0
         if (dp.isNextRedirect()) {
 498  
             // clear redirect flag
 499  0
             dp.setNextRedirect(false);
 500  0
             String actionURL = vh.getActionURL(context, viewId);
 501  0
             if (actionURL.indexOf('?') < 0) {
 502  0
                 actionURL += '?';
 503  0
             } else {
 504  0
                 actionURL += '&';
 505  
             }
 506  0
             actionURL += Constants.DIALOG_ID + "=" + this.id;
 507  
             try {
 508  0
                 ExternalContext econtext = context.getExternalContext();
 509  0
                 econtext.redirect(econtext.encodeActionURL(actionURL));
 510  0
                 context.responseComplete();
 511  0
             } catch (IOException e) {
 512  0
                 throw new FacesException("Cannot redirect to " + actionURL, e);
 513  0
             }
 514  0
         } else {
 515  0
             UIViewRoot view = vh.createView(context, viewId);
 516  0
             view.setViewId(viewId);
 517  0
             context.setViewRoot(view);
 518  0
             context.renderResponse();
 519  
         }
 520  0
     }
 521  
 
 522  
 
 523  
     /**
 524  
      * <p>Return the <code>Log</code> instance for this dialog context,
 525  
      * creating one if necessary.</p>
 526  
      *
 527  
      * @return The log instance.
 528  
      */
 529  
     private Log log() {
 530  
 
 531  0
         if (log == null) {
 532  0
             log = LogFactory.getLog(SCXMLDialogContext.class);
 533  
         }
 534  0
         return log;
 535  
 
 536  
     }
 537  
 
 538  
 
 539  
     /**
 540  
      * A {@link SCXMLListener} that delegates to the Shale
 541  
      * {@link DialogContextListener}s attached to this {@link DialogContext}.
 542  
      */
 543  0
     class DelegatingSCXMLListener implements SCXMLListener, Serializable {
 544  
 
 545  
         /**
 546  
          * Serial version UID.
 547  
          */
 548  
         private static final long serialVersionUID = 1L;
 549  
 
 550  
         /**
 551  
          * Handle entry callbacks.
 552  
          *
 553  
          * @param tt The <code>TransitionTarget</code> being entered.
 554  
          */
 555  
         public void onEntry(TransitionTarget tt) {
 556  
 
 557  0
             fireOnEntry(tt.getId());
 558  
 
 559  0
         }
 560  
 
 561  
         /**
 562  
          * Handle transition callbacks.
 563  
          *
 564  
          * @param from The source <code>TransitionTarget</code>
 565  
          * @param to The destination <code>TransitionTarget</code>
 566  
          * @param t The <code>Transition</code>
 567  
          */
 568  
         public void onTransition(TransitionTarget from, TransitionTarget to,
 569  
                                  Transition t) {
 570  
 
 571  0
             fireOnTransition(from.getId(), to.getId());
 572  
 
 573  0
         }
 574  
 
 575  
         /**
 576  
          * Handle exit callbacks.
 577  
          *
 578  
          * @param tt The <code>TransitionTarget</code> being exited.
 579  
          */
 580  
         public void onExit(TransitionTarget tt) {
 581  
 
 582  0
             fireOnExit(tt.getId());
 583  
 
 584  0
         }
 585  
 
 586  
     }
 587  
 
 588  
 }
 589