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

For more information, please explore the Attic.

View Javadoc

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  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                         String parentDialogId) {
86          this.manager = manager;
87          this.name = dialog.getName();
88          this.dataClassName = dialog.getDataclassname();
89          this.id = id;
90          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          this.executor = new SCXMLExecutor(new ShaleDialogELEvaluator(),
95                          new SimpleDispatcher(), new SimpleErrorReporter());
96          SCXML statemachine = dialog.getStateMachine();
97          this.executor.setStateMachine(statemachine);
98          Context rootCtx = new ELContext();
99          rootCtx.setLocal(Globals.DIALOG_PROPERTIES, new DialogProperties());
100         this.executor.setRootContext(rootCtx);
101         this.executor.addListener(statemachine, new DelegatingSCXMLListener());
102 
103         if (log().isDebugEnabled()) {
104             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     }
113 
114 
115     // ------------------------------------------------------ DialogContext Variables
116 
117 
118     /***
119      * <p>Flag indicating that this {@link DialogContext} is currently active.</p>
120      */
121     private boolean active = true;
122 
123 
124     /***
125      * <p>Generic data object containing state information for this instance.</p>
126      */
127     private Object data = null;
128 
129 
130     /***
131      * <p>Type of data object (FQCN to be instantiated).</p>
132      */
133     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     private String parentDialogId = null;
142 
143     /***
144      * <p>Dialog identifier for this instance.</p>
145      */
146     private String id = null;
147 
148 
149     /***
150      * <p>{@link DialogContextManager} instance that owns us.</p>
151      */
152     private DialogContextManager manager = null;
153 
154 
155     /***
156      * <p>Logical name of the dialog to be executed.</p>
157      */
158     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     private SCXMLExecutor executor = null;
167 
168 
169     /***
170      * <p>Flag indicating that execution has started for this dialog.</p>
171      */
172     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     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     private transient Log log = null;
189 
190 
191     // ----------------------------------------------------- DialogContext Properties
192 
193 
194     /*** {@inheritDoc} */
195     public boolean isActive() {
196         return this.active;
197     }
198 
199 
200     /*** {@inheritDoc} */
201     public Object getData() {
202          return this.data;
203      }
204 
205 
206 
207     /*** {@inheritDoc} */
208     public void setData(Object data) {
209         Object old = this.data;
210         if ((old != null) && (old instanceof DialogContextListener)) {
211             removeDialogContextListener((DialogContextListener) old);
212         }
213         this.data = data;
214         if ((data != null) && (data instanceof DialogContextListener)) {
215             addDialogContextListener((DialogContextListener) data);
216         }
217     }
218 
219 
220     /*** {@inheritDoc} */
221     public String getId() {
222         return this.id;
223     }
224 
225 
226     /*** {@inheritDoc} */
227     public String getName() {
228         return this.name;
229     }
230 
231 
232     /*** {@inheritDoc} */
233     public Object getOpaqueState() {
234 
235         return stateId;
236 
237     }
238 
239 
240     /*** {@inheritDoc} */
241     public void setOpaqueState(Object opaqueState) {
242 
243         String viewStateId = String.valueOf(opaqueState);
244         if (viewStateId == null) {
245             throw new IllegalArgumentException("Dialog instance '" + getId()
246                 + "' for dialog name '" + getName()
247                 + "': null opaqueState received");
248         }
249 
250         // account for user agent navigation
251         if (!viewStateId.equals(stateId)) {
252 
253             if (log().isTraceEnabled()) {
254                 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             Map targets = executor.getStateMachine().getTargets();
261             State serverState = (State) targets.get(stateId);
262             State clientState = (State) targets.get(viewStateId);
263             if (clientState == null) {
264                 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             Set states = executor.getCurrentStatus().getStates();
271             if (states.size() != 1) {
272                 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             states.remove(serverState);
281             fireOnExit(serverState.getId());
282 
283             fireOnEntry(clientState.getId());
284             states.add(clientState);
285 
286         }
287 
288     }
289 
290 
291     /*** {@inheritDoc} */
292     public DialogContext getParent() {
293 
294         if (this.parentDialogId != null) {
295             DialogContext parent = manager.get(this.parentDialogId);
296             if (parent == null) {
297                 throw new IllegalStateException("Dialog instance '"
298                         + parentDialogId + "' was associated with this instance '"
299                         + getId() + "' but is no longer available");
300             }
301             return parent;
302         } else {
303             return null;
304         }
305 
306     }
307 
308 
309     // -------------------------------------------------------- DialogContext Methods
310 
311 
312     /*** {@inheritDoc} */
313     public void advance(FacesContext context, String outcome) {
314 
315         if (!started) {
316             throw new IllegalStateException("Dialog instance '"
317                     + getId() + "' for dialog name '"
318                     + getName() + "' has not yet been started");
319         }
320 
321         if (log().isDebugEnabled()) {
322             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         if (outcome == null) {
330             if (log().isTraceEnabled()) {
331                 log().trace("advance(outcome is null, stay in same view)");
332             }
333             return;
334         }
335 
336         ((ShaleDialogELEvaluator) executor.getEvaluator()).
337                     setFacesContext(context);
338         executor.getRootContext().setLocal(Globals.POSTBACK_OUTCOME, outcome);
339 
340         try {
341             executor.triggerEvent(new TriggerEvent(Globals.POSTBACK_EVENT,
342                                 TriggerEvent.SIGNAL_EVENT));
343         } catch (ModelException me) {
344             fireOnException(me);
345         }
346 
347         Iterator iterator = executor.getCurrentStatus().getStates().iterator();
348         this.stateId = ((State) iterator.next()).getId();
349         DialogProperties dp = (DialogProperties) executor.getRootContext().
350             get(Globals.DIALOG_PROPERTIES);
351 
352         // If done, stop context
353         if (executor.getCurrentStatus().isFinal()) {
354             stop(context);
355         }
356 
357         navigateTo(stateId, context, dp);
358 
359     }
360 
361 
362     /*** {@inheritDoc} */
363     public void start(FacesContext context) {
364 
365         if (started) {
366             throw new IllegalStateException("Dialog instance '"
367                     + getId() + "' for dialog name '"
368                     + getName() + "' has already been started");
369         }
370         started = true;
371 
372         if (log().isDebugEnabled()) {
373             log().debug("start(id=" + getId() + ", name="
374                       + getName() + ")");
375         }
376 
377         // inform listeners we're good to go
378         fireOnStart();
379 
380         // Construct an appropriate data object for the specified dialog
381         ClassLoader loader = Thread.currentThread().getContextClassLoader();
382         if (loader == null) {
383             loader = SCXMLDialogContext.class.getClassLoader();
384         }
385         Class dataClass = null;
386         try {
387             dataClass = loader.loadClass(dataClassName);
388             data = dataClass.newInstance();
389         } catch (Exception e) {
390             fireOnException(e);
391         }
392 
393         if (data != null && data instanceof DialogContextListener) {
394             addDialogContextListener((DialogContextListener) data);
395         }
396 
397         // set state machine in motion
398         ((ShaleDialogELEvaluator) executor.getEvaluator()).
399             setFacesContext(context);
400         try {
401             executor.go();
402         } catch (ModelException me) {
403             fireOnException(me);
404         }
405 
406         Iterator iterator = executor.getCurrentStatus().getStates().iterator();
407         this.stateId = ((State) iterator.next()).getId();
408         DialogProperties dp = (DialogProperties) executor.getRootContext().
409             get(Globals.DIALOG_PROPERTIES);
410 
411         // Might be done at the beginning itself, if so, stop context
412         if (executor.getCurrentStatus().isFinal()) {
413             stop(context);
414         }
415 
416         // Tell listeners we have been activated as well
417         fireOnActivate();
418 
419         navigateTo(stateId, context, dp);
420 
421     }
422 
423 
424     /*** {@inheritDoc} */
425     public void stop(FacesContext context) {
426 
427         if (!started) {
428             throw new IllegalStateException("Dialog instance '"
429                     + getId() + "' for dialog name '"
430                     + getName() + "' has not yet been started");
431         }
432         started = false;
433 
434         if (log().isDebugEnabled()) {
435             log().debug("stop(id=" + getId() + ", name="
436                       + getName() + ")");
437         }
438 
439         fireOnPassivate();
440         deactivate();
441         manager.remove(this);
442 
443         // inform listeners
444         fireOnStop();
445 
446     }
447 
448 
449     // ------------------------------------------------- Package Private Methods
450 
451 
452     /***
453      * <p>Mark this {@link DialogContext} as being deactivated.  This should only
454      * be called by the <code>remove()</code> method on our associated
455      * {@link DialogContextManager}.</p>
456      */
457     void deactivate() {
458         setData(null);
459         this.active = false;
460     }
461 
462 
463     //  ------------------------------------------------- Private Methods
464 
465 
466     /***
467      * <p>Navigate to the JavaServer Faces <code>view identifier</code>
468      * that is mapped to by the current state identifier for this dialog.</p>
469      *
470      * @param stateId The current state identifier for this dialog.
471      * @param context The current <code>FacesContext</code>
472      * @param dp The <code>DialogProperties</code> for the current dialog
473      */
474     private void navigateTo(String stateId, FacesContext context, DialogProperties dp) {
475         // Determine the view identifier
476         String viewId = dp.getNextViewId();
477         if (viewId == null) {
478             ValueBinding vb = context.getApplication().createValueBinding
479                 ("#{" + Globals.STATE_MAPPER + "}");
480             DialogStateMapper dsm = (DialogStateMapper) vb.getValue(context);
481             viewId = dsm.mapStateId(name, stateId, context);
482         } else {
483             dp.setNextViewId(null); // one time use
484         }
485 
486         // Navigate to the requested view identifier (if any)
487         if (viewId == null) {
488             return;
489         }
490         if (!viewId.startsWith("/")) {
491             viewId = "/" + viewId;
492         }
493 
494         // The public API is advance, so thats part of the message
495         if (log().isDebugEnabled()) {
496             log().debug("advance(id=" + getId() + ", name=" + getName()
497                       + ", navigating to view: '" + viewId + "')");
498         }
499 
500         ViewHandler vh = context.getApplication().getViewHandler();
501         if (dp.isNextRedirect()) {
502             // clear redirect flag
503             dp.setNextRedirect(false);
504             String actionURL = vh.getActionURL(context, viewId);
505             if (actionURL.indexOf('?') < 0) {
506                 actionURL += '?';
507             } else {
508                 actionURL += '&';
509             }
510             actionURL += Constants.DIALOG_ID + "=" + this.id;
511             try {
512                 ExternalContext econtext = context.getExternalContext();
513                 econtext.redirect(econtext.encodeActionURL(actionURL));
514                 context.responseComplete();
515             } catch (IOException e) {
516                 throw new FacesException("Cannot redirect to " + actionURL, e);
517             }
518         } else {
519             UIViewRoot view = vh.createView(context, viewId);
520             view.setViewId(viewId);
521             context.setViewRoot(view);
522             context.renderResponse();
523         }
524     }
525 
526 
527     /***
528      * <p>Return the <code>Log</code> instance for this dialog context,
529      * creating one if necessary.</p>
530      *
531      * @return The log instance.
532      */
533     private Log log() {
534 
535         if (log == null) {
536             log = LogFactory.getLog(SCXMLDialogContext.class);
537         }
538         return log;
539 
540     }
541 
542 
543     /***
544      * A {@link SCXMLListener} that delegates to the Shale
545      * {@link DialogContextListener}s attached to this {@link DialogContext}.
546      */
547     class DelegatingSCXMLListener implements SCXMLListener, Serializable {
548 
549         /***
550          * Serial version UID.
551          */
552         private static final long serialVersionUID = 1L;
553 
554         /***
555          * Handle entry callbacks.
556          *
557          * @param tt The <code>TransitionTarget</code> being entered.
558          */
559         public void onEntry(TransitionTarget tt) {
560 
561             fireOnEntry(tt.getId());
562 
563         }
564 
565         /***
566          * Handle transition callbacks.
567          *
568          * @param from The source <code>TransitionTarget</code>
569          * @param to The destination <code>TransitionTarget</code>
570          * @param t The <code>Transition</code>
571          */
572         public void onTransition(TransitionTarget from, TransitionTarget to,
573                                  Transition t) {
574 
575             fireOnTransition(from.getId(), to.getId());
576 
577         }
578 
579         /***
580          * Handle exit callbacks.
581          *
582          * @param tt The <code>TransitionTarget</code> being exited.
583          */
584         public void onExit(TransitionTarget tt) {
585 
586             fireOnExit(tt.getId());
587 
588         }
589 
590     }
591 
592 }
593