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         navigateTo(stateId, context, dp);
417 
418     }
419 
420 
421     /*** {@inheritDoc} */
422     public void stop(FacesContext context) {
423 
424         if (!started) {
425             throw new IllegalStateException("Dialog instance '"
426                     + getId() + "' for dialog name '"
427                     + getName() + "' has not yet been started");
428         }
429         started = false;
430 
431         if (log().isDebugEnabled()) {
432             log().debug("stop(id=" + getId() + ", name="
433                       + getName() + ")");
434         }
435 
436         deactivate();
437         manager.remove(this);
438 
439         // inform listeners
440         fireOnStop();
441 
442     }
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         setData(null);
455         this.active = false;
456     }
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         String viewId = dp.getNextViewId();
473         if (viewId == null) {
474             ValueBinding vb = context.getApplication().createValueBinding
475                 ("#{" + Globals.STATE_MAPPER + "}");
476             DialogStateMapper dsm = (DialogStateMapper) vb.getValue(context);
477             viewId = dsm.mapStateId(name, stateId, context);
478         } else {
479             dp.setNextViewId(null); // one time use
480         }
481 
482         // Navigate to the requested view identifier (if any)
483         if (viewId == null) {
484             return;
485         }
486         if (!viewId.startsWith("/")) {
487             viewId = "/" + viewId;
488         }
489 
490         // The public API is advance, so thats part of the message
491         if (log().isDebugEnabled()) {
492             log().debug("advance(id=" + getId() + ", name=" + getName()
493                       + ", navigating to view: '" + viewId + "')");
494         }
495 
496         ViewHandler vh = context.getApplication().getViewHandler();
497         if (dp.isNextRedirect()) {
498             // clear redirect flag
499             dp.setNextRedirect(false);
500             String actionURL = vh.getActionURL(context, viewId);
501             if (actionURL.indexOf('?') < 0) {
502                 actionURL += '?';
503             } else {
504                 actionURL += '&';
505             }
506             actionURL += Constants.DIALOG_ID + "=" + this.id;
507             try {
508                 ExternalContext econtext = context.getExternalContext();
509                 econtext.redirect(econtext.encodeActionURL(actionURL));
510                 context.responseComplete();
511             } catch (IOException e) {
512                 throw new FacesException("Cannot redirect to " + actionURL, e);
513             }
514         } else {
515             UIViewRoot view = vh.createView(context, viewId);
516             view.setViewId(viewId);
517             context.setViewRoot(view);
518             context.renderResponse();
519         }
520     }
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         if (log == null) {
532             log = LogFactory.getLog(SCXMLDialogContext.class);
533         }
534         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     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             fireOnEntry(tt.getId());
558 
559         }
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             fireOnTransition(from.getId(), to.getId());
572 
573         }
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             fireOnExit(tt.getId());
583 
584         }
585 
586     }
587 
588 }
589