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