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 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
440 fireOnStop();
441
442 }
443
444
445
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
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
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);
480 }
481
482
483 if (viewId == null) {
484 return;
485 }
486 if (!viewId.startsWith("/")) {
487 viewId = "/" + viewId;
488 }
489
490
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
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