2009/05/20 - Apache Shale has been retired.
For more information, please explore the Attic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
92
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
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
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
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
274
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
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
322
323
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
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
373 fireOnStart();
374
375
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
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
407 if (executor.getCurrentStatus().isFinal()) {
408 stop(context);
409 }
410
411
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
439 fireOnStop();
440
441 }
442
443
444
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
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
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);
479 }
480
481
482 if (viewId == null) {
483 return;
484 }
485 if (!viewId.startsWith("/")) {
486 viewId = "/" + viewId;
487 }
488
489
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
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