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.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
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
93
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
109
110
111
112 }
113
114
115
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
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
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
279
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
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
327
328
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
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
378 fireOnStart();
379
380
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
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
412 if (executor.getCurrentStatus().isFinal()) {
413 stop(context);
414 }
415
416
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
444 fireOnStop();
445
446 }
447
448
449
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
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
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);
484 }
485
486
487 if (viewId == null) {
488 return;
489 }
490 if (!viewId.startsWith("/")) {
491 viewId = "/" + viewId;
492 }
493
494
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
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