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 fireOnActivate();
507
508
509
510 advance(context, START_OUTCOME);
511
512 }
513
514
515 /*** {@inheritDoc} */
516 public void stop(FacesContext context) {
517
518 if (!started) {
519 throw new IllegalStateException("Dialog instance '"
520 + getId() + "' for dialog name '"
521 + getName() + "' has not yet been started");
522 }
523 started = false;
524
525 if (log().isDebugEnabled()) {
526 log().debug("stop(id=" + getId() + ", name="
527 + getName() + ")");
528 }
529
530 fireOnPassivate();
531 deactivate();
532 manager.remove(this);
533
534
535 fireOnStop();
536
537 }
538
539
540
541
542
543 /***
544 * <p>Mark this {@link DialogContext} as being deactivated. This should only
545 * be called by the <code>remove()</code> method on our associated
546 * {@link DialogContextManager}, or the logic of our <code>stop()</code>
547 * method.</p>
548 */
549 void deactivate() {
550
551 while (positions.size() > 0) {
552 pop();
553 }
554 this.active = false;
555
556 }
557
558
559
560
561
562 /***
563 * <p>Return the {@link Dialog} instance for the dialog we are executing,
564 * regenerating it if necessary first.</p>
565 *
566 * @param context FacesContext for the current request
567 * @return The {@link Dialog} instance we are executing
568 */
569 private Dialog dialog(FacesContext context) {
570
571 if (this.dialog == null) {
572 this.dialog = (Dialog) dialogs(context).get(this.dialogName);
573 }
574 return this.dialog;
575
576 }
577
578
579 /***
580 * <p>Return a <code>Map</code> of the configured {@link Dialog}s, keyed
581 * by logical dialog name.</p>
582 *
583 * @param context FacesContext for the current request
584 * @return The map of available dialogs, keyed by dialog logical name
585 *
586 * @exception IllegalStateException if dialog configuration has not
587 * been completed
588 */
589 private Map dialogs(FacesContext context) {
590
591
592 if (this.dialogs != null) {
593 return this.dialogs;
594 }
595
596
597 this.dialogs = (Map)
598 context.getExternalContext().getApplicationMap().get(Globals.DIALOGS);
599 if (this.dialogs != null) {
600 return this.dialogs;
601 }
602
603
604
605 throw new IllegalStateException("Dialog configuration resources have not yet been processed");
606
607 }
608
609
610 /***
611 * <p>Return the <code>Log</code> instance for this dialog context,
612 * creating one if necessary.</p>
613 */
614 private Log log() {
615
616 if (log == null) {
617 log = LogFactory.getLog(BasicDialogContext.class);
618 }
619 return log;
620
621 }
622
623
624 /***
625 * <p>Return a <code>Position</code> representing the currently executing
626 * dialog and state (if any); otherwise, return <code>null</code>.</p>
627 *
628 * @return Position representing the currently executing dialog and
629 * state, may be null
630 */
631 private Position peek() {
632
633 synchronized (positions) {
634 int index = positions.size() - 1;
635 if (index < 0) {
636 return null;
637 }
638 return (Position) positions.get(index);
639 }
640
641 }
642
643
644 /***
645 * <p>Pop the currently executing <code>Position</code> and return the
646 * previously executing <code>Position</code> (if any); otherwise,
647 * return <code>null</code>.</p>
648 *
649 * @return Position representing the previously executing dialog and
650 * state (may be null), currently executing dialog Position
651 * is lost.
652 */
653 private Position pop() {
654
655 synchronized (positions) {
656 int index = positions.size() - 1;
657 if (index < 0) {
658 throw new IllegalStateException("No position to be popped");
659 }
660 Object data = ((Position) positions.get(index)).getData();
661 if ((data != null) && (data instanceof DialogContextListener)) {
662 removeDialogContextListener((DialogContextListener) data);
663 }
664 positions.remove(index);
665 if (index > 0) {
666 return (Position) positions.get(index - 1);
667 } else {
668 return null;
669 }
670 }
671
672 }
673
674
675 /***
676 * <p>Push the specified <code>Position</code>, making it the currently
677 * executing one.</p>
678 *
679 * @param position The new currently executing <code>Position</code>
680 */
681 private void push(Position position) {
682
683 if (position == null) {
684 throw new IllegalArgumentException();
685 }
686 Object data = position.getData();
687 if ((data != null) && (data instanceof DialogContextListener)) {
688 addDialogContextListener((DialogContextListener) data);
689 }
690 synchronized (positions) {
691 positions.add(position);
692 }
693
694 }
695
696
697 /***
698 * <p>Push a new {@link Position} instance representing the starting
699 * {@link State} of the specified {@link Dialog}.</p>
700 *
701 * @param dialog {@link Dialog} instance to be started and pushed
702 */
703 private void start(Dialog dialog) {
704
705
706 State state = dialog.findState(dialog.getStart());
707 if (state == null) {
708 throw new IllegalStateException
709 ("Cannot find starting state '"
710 + dialog.getStart()
711 + "' for dialog '" + dialog.getName() + "'");
712 }
713
714
715 Object data = null;
716 try {
717 data = dialog.getDataClass().newInstance();
718 } catch (Exception e) {
719 fireOnException(e);
720 }
721
722
723 push(new Position(dialog, state, data));
724
725
726 fireOnEntry(state.getName());
727
728 }
729
730
731 /***
732 * <p>Return the strategy identifier for dealing with saving state
733 * information across requests.</p>
734 */
735 private String strategy() {
736
737 if (this.strategy == null) {
738 this.strategy = FacesContext.getCurrentInstance().getExternalContext().getInitParameter(Globals.STRATEGY);
739 if (this.strategy == null) {
740 this.strategy = "top";
741 } else {
742 this.strategy = this.strategy.toLowerCase();
743 }
744 }
745 return this.strategy;
746
747 }
748
749
750 /***
751 * <p>Transition the specified {@link Position}, based on the specified
752 * logical outcome, to the appropriate next {@link State}.</p>
753 *
754 * @param position {@link Position} to be transitioned
755 * @param outcome Logical outcome to use for transitioning
756 */
757 private void transition(Position position, String outcome) {
758
759
760
761 if (outcome == null) {
762 return;
763 }
764
765 State current = position.getState();
766 String fromStateId = current.getName();
767
768
769 Transition transition = current.findTransition(outcome);
770 if (transition == null) {
771 transition = position.getDialog().findTransition(outcome);
772 }
773 if (transition == null) {
774 throw new IllegalStateException
775 ("Cannot find transition '" + outcome
776 + "' for state '" + fromStateId
777 + "' of dialog '" + position.getDialog().getName() + "'");
778 }
779 State next = position.getDialog().findState(transition.getTarget());
780 if (next == null) {
781 throw new IllegalStateException
782 ("Cannot find state '" + transition.getTarget()
783 + "' for dialog '" + position.getDialog().getName() + "'");
784 }
785 String toStateId = next.getName();
786 position.setState(next);
787
788
789
790
791
792
793 fireOnExit(fromStateId);
794
795
796 fireOnTransition(fromStateId, toStateId);
797
798
799 fireOnEntry(toStateId);
800
801 }
802
803
804
805
806
807 /***
808 * <p>Class that represents the saved opaque state information when the
809 * <code>top</code> strategy is selected.</p>
810 */
811 public class TopState implements Serializable {
812
813 /***
814 * Serial version UID.
815 */
816 private static final long serialVersionUID = 1L;
817
818
819 /***
820 * <p>Construct an uninitialized instance of this state class.</p>
821 */
822 public TopState() {
823 ;
824 }
825
826
827 /***
828 * <p>Construct an initialized instance of this state class.</p>
829 *
830 * @param stateName Name of the current state in the topmost position
831 * @param stackDepth Depth of the position stack
832 */
833 public TopState(String stateName, int stackDepth) {
834 this.stateName = stateName;
835 this.stackDepth = stackDepth;
836 }
837
838
839 /***
840 * <p>The name of the current state within the topmost
841 * <code>Position</code> on the stack.</p>
842 */
843 public String stateName;
844
845
846 /***
847 * <p>The stack depth of the <code>Position</code> stack. This is
848 * used to detect scenarios where using the back and forward buttons
849 * navigates across a subdialog boundary, which means that the
850 * saved <code>stateName</code> is likely no longer relevant.</p>
851 */
852 public int stackDepth;
853
854
855 /***
856 * <p>Return a string representation of this instance.</p>
857 */
858 public String toString() {
859 return "TopState[stateName=" + this.stateName
860 + ", stackDepth=" + this.stackDepth + "]";
861 }
862
863
864
865 }
866
867
868
869 }