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.basic;
19
20 import java.io.IOException;
21 import java.io.Serializable;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.Map;
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.MethodBinding;
32
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 import org.apache.shale.dialog.Constants;
36 import org.apache.shale.dialog.DialogContext;
37 import org.apache.shale.dialog.DialogContextListener;
38 import org.apache.shale.dialog.DialogContextManager;
39 import org.apache.shale.dialog.base.AbstractDialogContext;
40 import org.apache.shale.dialog.basic.model.ActionState;
41 import org.apache.shale.dialog.basic.model.Dialog;
42 import org.apache.shale.dialog.basic.model.EndState;
43 import org.apache.shale.dialog.basic.model.State;
44 import org.apache.shale.dialog.basic.model.SubdialogState;
45 import org.apache.shale.dialog.basic.model.Transition;
46 import org.apache.shale.dialog.basic.model.ViewState;
47
48 /***
49 * <p>Implementation of {@link DialogContext} for integrating
50 * basic dialog support into the Shale Dialog Manager.</p>
51 *
52 * <p><strong>IMPLEMENTATION NOTE</strong> - Takes on the responsibilities
53 * of the <code>org.apache.shale.dialog.Status</code> implementation in the
54 * original approach.</p>
55 *
56 * @since 1.0.4
57 */
58 final class BasicDialogContext extends AbstractDialogContext
59 implements Serializable {
60
61
62
63
64
65 /***
66 * Serial version UID.
67 */
68 private static final long serialVersionUID = 4858871161274193403L;
69
70
71 /***
72 * <p>Construct a new instance.</p>
73 *
74 * @param manager {@link DialogContextManager} instance that owns us
75 * @param dialog Configured dialog definition we will be following
76 * @param id Dialog identifier assigned to this instance
77 * @param parentDialogId Dialog identifier of the parent DialogContext
78 * instance associated with this one (if any)
79 */
80 BasicDialogContext(DialogContextManager manager, Dialog dialog,
81 String id, String parentDialogId) {
82
83 this.manager = manager;
84 this.dialog = dialog;
85 this.dialogName = dialog.getName();
86 this.id = id;
87 this.parentDialogId = parentDialogId;
88
89 if (log().isDebugEnabled()) {
90 log().debug("Constructor(id=" + id + ", name="
91 + dialogName + ")");
92 }
93
94 }
95
96
97
98
99
100 /***
101 * <p>Flag indicating that this {@link DialogContext} is currently active.</p>
102 */
103 private boolean active = true;
104
105
106 /***
107 * <p>The {@link Dialog} that this instance is executing. This value is
108 * transient and may need to be regenerated.</p>
109 */
110 private transient Dialog dialog = null;
111
112
113 /***
114 * <p>The name of the {@link Dialog} that this instance is executing.</p>
115 */
116 private String dialogName = null;
117
118
119 /***
120 * <p><code>Map</code> of configured dialogs, keyed by logical name. This value is
121 * transient and may need to be regenerated.</p>
122 */
123 private transient Map dialogs = null;
124
125
126 /***
127 * <p>Dialog identifier for this instance.</p>
128 */
129 private String id = null;
130
131
132 /***
133 * <p>{@link DialogContextManager} instance that owns us.</p>
134 */
135 private DialogContextManager manager = null;
136
137
138 /***
139 * <p>The <code>Log</code> instance for this dialog context.
140 * This value is lazily created (or recreated) as necessary.</p>
141 */
142 private transient Log log = null;
143
144
145 /***
146 * <p>Identifier of the parent {@link DialogContext} associated with
147 * this {@link DialogContext}, if any. If there is no such parent,
148 * this value is set to <code>null</code>.</p>
149 */
150 private String parentDialogId = null;
151
152
153 /***
154 * <p>The stack of currently stored Position instances. If there
155 * are no entries, we have completed the exit state and should deactivate
156 * ourselves.</p>
157 */
158 private List positions = new ArrayList();
159
160
161 /***
162 * <p>Flag indicating that execution has started for this dialog.</p>
163 */
164 private boolean started = false;
165
166
167 /***
168 * <p>The strategy identifier for dealing with saving and restoring
169 * state information across requests. This value is lazily
170 * instantiated, and can be recalculated on demand as needed.</p>
171 */
172 private transient String strategy = null;
173
174
175 /***
176 * <p>An empty parameter list to pass to the action method called by
177 * a method binding expression.</p>
178 */
179 private static final Object[] ACTION_STATE_PARAMETERS = new Object[0];
180
181
182 /***
183 * <p>Parameter signature we create for method bindings used to execute
184 * expressions specified by an {@link ActionState}.</p>
185 */
186 private static final Class[] ACTION_STATE_SIGNATURE = new Class[0];
187
188
189 /***
190 * <p>Flag outcome value that signals a need to transition to the
191 * start state for this dialog.</p>
192 */
193 private static final String START_OUTCOME =
194 "org.apache.shale.dialog.basic.START_OUTCOME";
195
196
197
198
199
200 /*** {@inheritDoc} */
201 public boolean isActive() {
202
203 return this.active;
204
205 }
206
207
208 /*** {@inheritDoc} */
209 public Object getData() {
210
211 synchronized (positions) {
212 int index = positions.size() - 1;
213 if (index < 0) {
214 return null;
215 }
216 Position position = (Position) positions.get(index);
217 return position.getData();
218 }
219
220 }
221
222
223
224 /*** {@inheritDoc} */
225 public void setData(Object data) {
226
227 synchronized (positions) {
228 int index = positions.size() - 1;
229 if (index < 0) {
230 throw new IllegalStateException("Cannot set data when no positions are stacked");
231 }
232 Position position = (Position) positions.get(index);
233 Object old = position.getData();
234 if ((old != null) && (old instanceof DialogContextListener)) {
235 removeDialogContextListener((DialogContextListener) old);
236 }
237 position.setData(data);
238 if ((data != null) && (data instanceof DialogContextListener)) {
239 addDialogContextListener((DialogContextListener) data);
240 }
241 }
242
243 }
244
245
246 /*** {@inheritDoc} */
247 public String getId() {
248
249 return this.id;
250
251 }
252
253
254 /*** {@inheritDoc} */
255 public String getName() {
256
257 Position position = peek();
258 if (position != null) {
259 return position.getDialog().getName();
260 } else {
261 return null;
262 }
263
264 }
265
266
267 /*** {@inheritDoc} */
268 public Object getOpaqueState() {
269
270 if ("top".equals(strategy())) {
271 if (log().isTraceEnabled()) {
272 log().trace("getOpaqueState<top> returns " + new TopState(peek().getState().getName(), positions.size()));
273 }
274 return new TopState(peek().getState().getName(), positions.size());
275 } else if ("stack".equals(strategy())) {
276 if (log().isTraceEnabled()) {
277 log().trace("getOpaqueStrategy<stack> returns stack of " + positions.size());
278 }
279 return positions;
280 } else {
281 if (log().isTraceEnabled()) {
282 log().trace("getOpaqueStrategy<none> returns nothing");
283 }
284 return null;
285 }
286
287 }
288
289
290 /*** {@inheritDoc} */
291 public void setOpaqueState(Object opaqueState) {
292
293 if ("top".equals(strategy())) {
294 TopState topState = (TopState) opaqueState;
295 if (log().isTraceEnabled()) {
296 log().trace("setOpaqueState<top> restores " + topState);
297 }
298 if (topState.stackDepth != positions.size()) {
299 throw new IllegalStateException("Restored stack depth expects "
300 + positions.size() + " but is actually "
301 + topState.stackDepth);
302 }
303 Position top = peek();
304 String oldStateName = top.getState().getName();
305 if (!oldStateName.equals(topState.stateName)) {
306 fireOnExit(oldStateName);
307 top.setState(top.getDialog().findState(topState.stateName));
308 fireOnEntry(topState.stateName);
309 }
310 } else if ("stack".equals(strategy())) {
311 if (log().isTraceEnabled()) {
312 log().trace("setOpaqueState<stack> restores stack of " + ((List) opaqueState).size());
313 }
314 List list = (List) opaqueState;
315 String oldStateName = peek().getState().getName();
316 String newStateName = ((Position) list.get(list.size() - 1)).getState().getName();
317 if (!oldStateName.equals(newStateName) || (list.size() != positions.size())) {
318 fireOnExit(oldStateName);
319 positions = list;
320 fireOnEntry(newStateName);
321 }
322 } else {
323 if (log().isTraceEnabled()) {
324 log().trace("setOpaqueState<none> restores nothing");
325 }
326 ;
327 }
328
329 }
330
331
332 /*** {@inheritDoc} */
333 public DialogContext getParent() {
334
335 if (this.parentDialogId != null) {
336 DialogContext parent = manager.get(this.parentDialogId);
337 if (parent == null) {
338 throw new IllegalStateException("Dialog instance '"
339 + parentDialogId + "' was associated with this instance '"
340 + getId() + "' but is no longer available");
341 }
342 return parent;
343 } else {
344 return null;
345 }
346
347 }
348
349
350
351
352
353 /*** {@inheritDoc} */
354 public void advance(FacesContext context, String outcome) {
355
356 if (!started) {
357 throw new IllegalStateException("Dialog instance '"
358 + getId() + "' for dialog name '"
359 + getName() + "' has not yet been started");
360 }
361
362 if (log().isDebugEnabled()) {
363 log().debug("advance(id=" + getId() + ", name=" + getName()
364 + ", outcome=" + outcome + ")");
365 }
366
367
368
369
370 if (outcome == null) {
371 if (log().isTraceEnabled()) {
372 log().trace("punt early since outcome is null");
373 }
374 return;
375 }
376
377
378
379 Position position = peek();
380 if (!START_OUTCOME.equals(outcome)) {
381 transition(position, outcome);
382 }
383 State state = position.getState();
384 String viewId = null;
385 boolean redirect = false;
386
387
388
389 while (true) {
390
391 if (state instanceof ActionState) {
392 ActionState astate = (ActionState) state;
393 if (log().isTraceEnabled()) {
394 log().trace("-->ActionState(method=" + astate.getMethod() + ")");
395 }
396 try {
397 MethodBinding mb = context.getApplication().
398 createMethodBinding(astate.getMethod(), ACTION_STATE_SIGNATURE);
399 outcome = (String) mb.invoke(context, ACTION_STATE_PARAMETERS);
400 } catch (Exception e) {
401 fireOnException(e);
402 }
403 transition(position, outcome);
404 state = position.getState();
405 continue;
406 } else if (state instanceof EndState) {
407 if (log().isTraceEnabled()) {
408 log().trace("-->EndState()");
409 }
410 pop();
411 position = peek();
412 if (position == null) {
413 stop(context);
414 }
415 viewId = ((EndState) state).getViewId();
416 redirect = ((EndState) state).isRedirect();
417 break;
418 } else if (state instanceof SubdialogState) {
419 SubdialogState sstate = (SubdialogState) state;
420 if (log().isTraceEnabled()) {
421 log().trace("-->SubdialogState(dialogName="
422 + sstate.getDialogName() + ")");
423 }
424 Dialog subdialog = (Dialog) dialogs(context).get(sstate.getDialogName());
425 if (subdialog == null) {
426 throw new IllegalStateException("Cannot find dialog definition '"
427 + sstate.getDialogName() + "'");
428 }
429 start(subdialog);
430 position = peek();
431 state = position.getState();
432 continue;
433 } else if (state instanceof ViewState) {
434 viewId = ((ViewState) state).getViewId();
435 redirect = ((ViewState) state).isRedirect();
436 if (log().isTraceEnabled()) {
437 log().trace("-->ViewState(viewId="
438 + ((ViewState) state).getViewId()
439 + ",redirect=" + ((ViewState) state).isRedirect()
440 + ")");
441 }
442 break;
443 } else {
444 throw new IllegalStateException
445 ("State '" + state.getName()
446 + "' of dialog '" + position.getDialog().getName()
447 + "' is of unknown type '" + state.getClass().getName() + "'");
448 }
449 }
450
451
452 if (viewId == null) {
453 return;
454 }
455 if (log().isTraceEnabled()) {
456 log().trace("-->Navigate(viewId=" + viewId + ")");
457 }
458 ViewHandler vh = context.getApplication().getViewHandler();
459 if (redirect) {
460 String actionURL = vh.getActionURL(context, viewId);
461 if (actionURL.indexOf('?') < 0) {
462 actionURL += '?';
463 } else {
464 actionURL += '&';
465 }
466 actionURL += Constants.DIALOG_ID + "=" + this.id;
467 try {
468 ExternalContext econtext = context.getExternalContext();
469 econtext.redirect(econtext.encodeActionURL(actionURL));
470 context.responseComplete();
471 } catch (IOException e) {
472 throw new FacesException("Cannot redirect to " + actionURL, e);
473 }
474 } else {
475 UIViewRoot view = vh.createView(context, viewId);
476 view.setViewId(viewId);
477 context.setViewRoot(view);
478 context.renderResponse();
479 }
480
481 }
482
483
484 /*** {@inheritDoc} */
485 public void start(FacesContext context) {
486
487 if (started) {
488 throw new IllegalStateException("Dialog instance '"
489 + getId() + "' for dialog name '"
490 + getName() + "' has already been started");
491 }
492 started = true;
493
494 if (log().isDebugEnabled()) {
495 log().debug("start(id=" + getId() + ", name="
496 + getName() + ")");
497 }
498
499
500 fireOnStart();
501
502
503 start(dialog);
504
505
506
507 advance(context, START_OUTCOME);
508
509 }
510
511
512 /*** {@inheritDoc} */
513 public void stop(FacesContext context) {
514
515 if (!started) {
516 throw new IllegalStateException("Dialog instance '"
517 + getId() + "' for dialog name '"
518 + getName() + "' has not yet been started");
519 }
520 started = false;
521
522 if (log().isDebugEnabled()) {
523 log().debug("stop(id=" + getId() + ", name="
524 + getName() + ")");
525 }
526
527 deactivate();
528 manager.remove(this);
529
530
531 fireOnStop();
532
533 }
534
535
536
537
538
539 /***
540 * <p>Mark this {@link DialogContext} as being deactivated. This should only
541 * be called by the <code>remove()</code> method on our associated
542 * {@link DialogContextManager}, or the logic of our <code>stop()</code>
543 * method.</p>
544 */
545 void deactivate() {
546
547 while (positions.size() > 0) {
548 pop();
549 }
550 this.active = false;
551
552 }
553
554
555
556
557
558 /***
559 * <p>Return the {@link Dialog} instance for the dialog we are executing,
560 * regenerating it if necessary first.</p>
561 *
562 * @param context FacesContext for the current request
563 * @return The {@link Dialog} instance we are executing
564 */
565 private Dialog dialog(FacesContext context) {
566
567 if (this.dialog == null) {
568 this.dialog = (Dialog) dialogs(context).get(this.dialogName);
569 }
570 return this.dialog;
571
572 }
573
574
575 /***
576 * <p>Return a <code>Map</code> of the configured {@link Dialog}s, keyed
577 * by logical dialog name.</p>
578 *
579 * @param context FacesContext for the current request
580 * @return The map of available dialogs, keyed by dialog logical name
581 *
582 * @exception IllegalStateException if dialog configuration has not
583 * been completed
584 */
585 private Map dialogs(FacesContext context) {
586
587
588 if (this.dialogs != null) {
589 return this.dialogs;
590 }
591
592
593 this.dialogs = (Map)
594 context.getExternalContext().getApplicationMap().get(Globals.DIALOGS);
595 if (this.dialogs != null) {
596 return this.dialogs;
597 }
598
599
600
601 throw new IllegalStateException("Dialog configuration resources have not yet been processed");
602
603 }
604
605
606 /***
607 * <p>Return the <code>Log</code> instance for this dialog context,
608 * creating one if necessary.</p>
609 */
610 private Log log() {
611
612 if (log == null) {
613 log = LogFactory.getLog(BasicDialogContext.class);
614 }
615 return log;
616
617 }
618
619
620 /***
621 * <p>Return a <code>Position</code> representing the currently executing
622 * dialog and state (if any); otherwise, return <code>null</code>.</p>
623 *
624 * @return Position representing the currently executing dialog and
625 * state, may be null
626 */
627 private Position peek() {
628
629 synchronized (positions) {
630 int index = positions.size() - 1;
631 if (index < 0) {
632 return null;
633 }
634 return (Position) positions.get(index);
635 }
636
637 }
638
639
640 /***
641 * <p>Pop the currently executing <code>Position</code> and return the
642 * previously executing <code>Position</code> (if any); otherwise,
643 * return <code>null</code>.</p>
644 *
645 * @return Position representing the previously executing dialog and
646 * state (may be null), currently executing dialog Position
647 * is lost.
648 */
649 private Position pop() {
650
651 synchronized (positions) {
652 int index = positions.size() - 1;
653 if (index < 0) {
654 throw new IllegalStateException("No position to be popped");
655 }
656 Object data = ((Position) positions.get(index)).getData();
657 if ((data != null) && (data instanceof DialogContextListener)) {
658 removeDialogContextListener((DialogContextListener) data);
659 }
660 positions.remove(index);
661 if (index > 0) {
662 return (Position) positions.get(index - 1);
663 } else {
664 return null;
665 }
666 }
667
668 }
669
670
671 /***
672 * <p>Push the specified <code>Position</code>, making it the currently
673 * executing one.</p>
674 *
675 * @param position The new currently executing <code>Position</code>
676 */
677 private void push(Position position) {
678
679 if (position == null) {
680 throw new IllegalArgumentException();
681 }
682 Object data = position.getData();
683 if ((data != null) && (data instanceof DialogContextListener)) {
684 addDialogContextListener((DialogContextListener) data);
685 }
686 synchronized (positions) {
687 positions.add(position);
688 }
689
690 }
691
692
693 /***
694 * <p>Push a new {@link Position} instance representing the starting
695 * {@link State} of the specified {@link Dialog}.</p>
696 *
697 * @param dialog {@link Dialog} instance to be started and pushed
698 */
699 private void start(Dialog dialog) {
700
701
702 State state = dialog.findState(dialog.getStart());
703 if (state == null) {
704 throw new IllegalStateException
705 ("Cannot find starting state '"
706 + dialog.getStart()
707 + "' for dialog '" + dialog.getName() + "'");
708 }
709
710
711 Object data = null;
712 try {
713 data = dialog.getDataClass().newInstance();
714 } catch (Exception e) {
715 fireOnException(e);
716 }
717
718
719 push(new Position(dialog, state, data));
720
721
722 fireOnEntry(state.getName());
723
724 }
725
726
727 /***
728 * <p>Return the strategy identifier for dealing with saving state
729 * information across requests.</p>
730 */
731 private String strategy() {
732
733 if (this.strategy == null) {
734 this.strategy = FacesContext.getCurrentInstance().getExternalContext().getInitParameter(Globals.STRATEGY);
735 if (this.strategy == null) {
736 this.strategy = "top";
737 } else {
738 this.strategy = this.strategy.toLowerCase();
739 }
740 }
741 return this.strategy;
742
743 }
744
745
746 /***
747 * <p>Transition the specified {@link Position}, based on the specified
748 * logical outcome, to the appropriate next {@link State}.</p>
749 *
750 * @param position {@link Position} to be transitioned
751 * @param outcome Logical outcome to use for transitioning
752 */
753 private void transition(Position position, String outcome) {
754
755
756
757 if (outcome == null) {
758 return;
759 }
760
761 State current = position.getState();
762 String fromStateId = current.getName();
763
764
765 Transition transition = current.findTransition(outcome);
766 if (transition == null) {
767 transition = position.getDialog().findTransition(outcome);
768 }
769 if (transition == null) {
770 throw new IllegalStateException
771 ("Cannot find transition '" + outcome
772 + "' for state '" + fromStateId
773 + "' of dialog '" + position.getDialog().getName() + "'");
774 }
775 State next = position.getDialog().findState(transition.getTarget());
776 if (next == null) {
777 throw new IllegalStateException
778 ("Cannot find state '" + transition.getTarget()
779 + "' for dialog '" + position.getDialog().getName() + "'");
780 }
781 String toStateId = next.getName();
782 position.setState(next);
783
784
785
786
787
788
789 fireOnExit(fromStateId);
790
791
792 fireOnTransition(fromStateId, toStateId);
793
794
795 fireOnEntry(toStateId);
796
797 }
798
799
800
801
802
803 /***
804 * <p>Class that represents the saved opaque state information when the
805 * <code>top</code> strategy is selected.</p>
806 */
807 public class TopState implements Serializable {
808
809 /***
810 * Serial version UID.
811 */
812 private static final long serialVersionUID = 1L;
813
814
815 /***
816 * <p>Construct an uninitialized instance of this state class.</p>
817 */
818 public TopState() {
819 ;
820 }
821
822
823 /***
824 * <p>Construct an initialized instance of this state class.</p>
825 *
826 * @param stateName Name of the current state in the topmost position
827 * @param stackDepth Depth of the position stack
828 */
829 public TopState(String stateName, int stackDepth) {
830 this.stateName = stateName;
831 this.stackDepth = stackDepth;
832 }
833
834
835 /***
836 * <p>The name of the current state within the topmost
837 * <code>Position</code> on the stack.</p>
838 */
839 public String stateName;
840
841
842 /***
843 * <p>The stack depth of the <code>Position</code> stack. This is
844 * used to detect scenarios where using the back and forward buttons
845 * navigates across a subdialog boundary, which means that the
846 * saved <code>stateName</code> is likely no longer relevant.</p>
847 */
848 public int stackDepth;
849
850
851 /***
852 * <p>Return a string representation of this instance.</p>
853 */
854 public String toString() {
855 return "TopState[stateName=" + this.stateName
856 + ", stackDepth=" + this.stackDepth + "]";
857 }
858
859
860
861 }
862
863
864
865 }