1 /*
2 * Scope: a generic MVC framework.
3 * Copyright (c) 2000-2002, The Scope team
4 * All rights reserved.
5 *
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 *
14 * Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 *
18 * Neither the name "Scope" nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
21 *
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 *
35 *
36 * $Id: BasicController.java,v 1.16 2002/09/11 19:15:55 ludovicc Exp $
37 */
38 package org.scopemvc.controller.basic;
39
40 import java.util.LinkedList;
41 import java.util.List;
42 import org.apache.commons.logging.Log;
43 import org.apache.commons.logging.LogFactory;
44
45 import org.scopemvc.core.Control;
46 import org.scopemvc.core.ControlException;
47 import org.scopemvc.core.Controller;
48 import org.scopemvc.core.PropertyView;
49 import org.scopemvc.core.View;
50 import org.scopemvc.util.Debug;
51 import org.scopemvc.util.UIStrings;
52
53 /***
54 * <P>
55 *
56 * Full implementation of {@link org.scopemvc.core.Controller Controller} that
57 * adds:
58 * <UL>
59 * <LI> support for a View to notify its parent Controller when its bound
60 * model object is replaced with another (implemented completely in {@link
61 * org.scopemvc.view.swing.SwingView SwingView}) via the
62 * CHANGE_MODEL_CONTROL_ID ControlID. Note that the PropertyView that a
63 * top-level Controller owns must not have a Selector set: this is only
64 * allowed for child Controllers that are delegated by a parent to handle a
65 * subview and associated submodel that is part of the parent model: the
66 * binding will be handled by the parent in this case. </LI>
67 * <LI> {@link org.scopemvc.core.ControlException ControlException} handling
68 * by using {@link ViewContext#showError}. </LI>
69 * </UL>
70 * </P> <P>
71 *
72 * To implement a subclass of BasicController:
73 * <UL>
74 * <LI> implement a constructor to set up the Controller's initial model and
75 * View, and to create any child Controllers it may need. {@link
76 * #setModelAndView} may be useful here. </LI>
77 * <LI> implement {@link #doHandleControl} to recognise the ID of incoming
78 * {@link org.scopemvc.core.Control Control}s and respond to them
79 * appropriately. For example: <PRE>
80 * protected void doHandleControl(Control inControl) throws ControlException {
81 * if (inControl.matchesID(FOO_CONTROL_ID)) {
82 * doFoo(inControl.getParameter());
83 * } else if (inControl.matchesID(BAR_CONTROL_ID)) {
84 * doBar(inControl.getParameter());
85 * }
86 * }
87 * </PRE> </LI>
88 * <LI> if necessary, implement a startup() method for the Controller to take
89 * its initial action (if your application calls startup() on this
90 * Controller). </LI>
91 * <LI> <FONT COLOR="GRAY">Internal: if using a View that can dynamically
92 * change its bound model, ensure the View sends the appropriate
93 * CHANGE_MODEL_CONTROL_ID Control to inform the parent Controller of the
94 * change. This is fully implemented in {@link
95 * org.scopemvc.view.swing.SwingView SwingView} and is not needed for {@link
96 * org.scopemvc.view.servlet.ServletView ServletView} with the default
97 * implementation in {@link org.scopemvc.view.servlet.xml.XSLPage}. </FONT>
98 * </LI>
99 * </UL>
100 * </P>
101 *
102 * @author <A HREF="mailto:smeyfroi@users.sourceforge.net">Steve Meyfroidt</A>
103 * @created 05 August 2002
104 * @version $Revision: 1.16 $ $Date: 2002/09/11 19:15:55 $
105 */
106 public abstract class BasicController implements Controller {
107
108 /***
109 * ID of error message for RuntimeExceptions caught by BasicController.
110 */
111 public static final String HANDLE_CONTROL_RUNTIME_ERROR_MSG_ID = "_HANDLE_CONTROL_RUNTIME_ERROR_MSG";
112
113 /***
114 * ID of common Control that is handled by BasicController to hide the
115 * current view.
116 */
117 public static final String HIDE_VIEW_CONTROL_ID = "_HIDE_VIEW";
118
119 /***
120 * An internal Control calls changeModel(). This is an internal Control to
121 * keep Controller's model in sync with the currently shown model in its
122 * View. This occurs when a parent controller modifies its model when that
123 * contains the submodel managed by a child controller. This is fully
124 * implemented by the concrete impl of SwingView. ServletView doesn't need
125 * it.
126 */
127 public static final String CHANGE_MODEL_CONTROL_ID = "_CHANGE_MODEL";
128
129 /***
130 * <P>
131 *
132 * A convenience Control that can be used when a Controller wants to exit.
133 * For an application Controller this means exitting the application, but
134 * for a sub-controller it probably means just an exit from that local area
135 * of functionality. This impl causes this Control to propagate up the chain
136 * of responsibility so that if unhandled the application will exit. </P>
137 * <P>
138 *
139 * The parameter of this Control is the Controller that issued it (ie the
140 * one that's shutting down), or null if just issued by <code>this</code>.
141 * The default impl here propagates the Control up, changing the shutdown
142 * Controller as it goes up, until it meets the top of the Controller tree
143 * at which point {@link ViewContext#exit} is called: for Swing this does
144 * System.exit and for Servlets it is ignored. If you use this Control,
145 * recognise it at some parent of the Controller that can issue it, and take
146 * appropriate action. </P>
147 *
148 * @todo The servlet implementation of exit should logout the user from the
149 * web application (ludovicc)
150 */
151 public static final String EXIT_CONTROL_ID = "_exit";
152
153 private static final Log LOG = LogFactory.getLog(BasicController.class);
154
155 private BasicController parent;
156
157 // Note that the only reason for Controllers to keep a handle on
158 // ... their children is to allow the ScopeServlet impl to traverse an
159 // ... application's controller graph to find a view by its id.
160 private LinkedList children = new LinkedList();
161
162 private Object model;
163
164 private View view;
165
166
167 /***
168 * <P>
169 *
170 * Construct subclasses by either using a passed model object and View, or
171 * creating new ones. Use {@link #setModel} and {@link #setView} or {@link
172 * #setModelAndView}. Never show a View on construction: initialisation
173 * should set the application up without actually starting it by showing a
174 * View. An initial startup action implemented in startup() can show a View
175 * when called after construction. </P> <P>
176 *
177 * Throw a ControlException from subclasses if something goes wrong. </P>
178 */
179 public BasicController() { }
180
181
182 // ---------------------- Child management ------------------------
183
184 /***
185 * Returns the list of child Controllers. <br>
186 * Used by ScopeServlet.
187 *
188 * @return List of child Controllers.
189 */
190 public final List getChildren() {
191 return children;
192 }
193
194
195 // ----------------------------- Implement Controller --------------------------------
196
197 /***
198 * Get the parent of this Controller
199 *
200 * @return the parent Controller of this Controller in the chain of command
201 */
202 public final Controller getParent() {
203 return parent;
204 }
205
206
207 /***
208 * Return the model bound to this Controller
209 *
210 * @return the model object bound to the View that this Controller maintains
211 */
212 public final Object getModel() {
213 return model;
214 }
215
216
217 /***
218 * Return the View bound to this Controller.
219 *
220 * @return the View that this Controller maintains
221 */
222 public final View getView() {
223 return view;
224 }
225
226
227 /***
228 * Convenience method. <br>
229 * Get the topmost parent Controller.
230 *
231 * @return the topmost parent Controller in the chain of responsibility.
232 */
233 public final Controller getTopParent() {
234 Controller result = this;
235 while (result.getParent() != null) {
236 result = result.getParent();
237 }
238 return result;
239 }
240
241
242 /***
243 * Sets the model object that this Controller links to its View. If you need
244 * to set both the View and model then use {@link #setModelAndView} rather
245 * than calling setModel and setView separately.
246 *
247 * @param inModel The new model value
248 */
249 public final void setModel(Object inModel) {
250 if (model == inModel) {
251 return;
252 }
253 model = inModel;
254 bindModelToView(view, model);
255 }
256
257
258 /***
259 * Sets the View that this Controller links to its model object. Unlinks the
260 * old View from the current model object and also hides it, however,
261 * doesn't show the new view. If you need to set both the View and model
262 * object then slightly more efficient in establishing the binding to use
263 * {@link #setModelAndView} rather than calling setModel() and setView()
264 * separately.
265 *
266 * @param inView The new view value
267 */
268 public final void setView(View inView) {
269 if (inView == view) {
270 return;
271 }
272 if (view != null) {
273 hideView();
274 bindModelToView(view, null);
275 view.setController(null);
276 }
277 view = inView;
278 if (view != null) {
279 bindModelToView(view, model);
280 view.setController(this);
281 }
282 }
283
284
285 /***
286 * Change to both a new model object and new View, binding the two together
287 * properly. Also disconnect and discard/hide the previous model/View pair.
288 * <br>
289 * Slightly more efficient in changing to a new model/view binding than
290 * calling setModel and setView separately.
291 *
292 * @param inModel new model object, can be null
293 * @param inView new View, cannot be null
294 */
295 public final void setModelAndView(Object inModel, View inView) {
296
297 // break existing model/view connection to avoid hooking
298 // ... new view to old model then immediately rebinding to
299 // ... new model.
300 setModel(null);
301
302 setModel(inModel);
303 setView(inView);
304 }
305
306
307 /***
308 * Add a child Controller. <br>
309 * The child Controller will use this Controller as its parent.
310 *
311 * @param inChild The child Controller to add.
312 */
313 public final void addChild(BasicController inChild) {
314 if (inChild == null) {
315 throw new IllegalArgumentException("Can't add a null child Controller.");
316 }
317 inChild.setParent(this);
318 }
319
320
321 /***
322 * Remove a child Controller from this Controller. <br>
323 * The child Controller will have no more parent.
324 *
325 * @param inChild The child Controller to remove.
326 */
327 public final void removeChild(BasicController inChild) {
328 if (getChildren().contains(inChild)) {
329 inChild.setParent(null);
330 }
331 }
332
333
334 /***
335 * Application writers see {@link #doHandleControl}. This base
336 * implementation handles
337 * <UL>
338 * <LI> HIDE_VIEW_CONTROL_ID</LI>
339 * <LI> the internal CHANGE_MODEL_CONTROL_ID</LI>
340 * <LI> EXIT_CONTROL_ID after allowing application code to intercept in
341 * doHandleControl. If this Controller has a parent, then hideView and
342 * pass it up, else call the ViewContext to do the exit according to
343 * context.</LI>
344 * </UL>
345 *
346 *
347 * @param inControl The Control to handle
348 * @todo Should get children to hideView too on EXIT_CONTROL_ID
349 */
350 public final void handleControl(Control inControl) {
351 if (LOG.isDebugEnabled()) {
352 LOG.debug("handleControl: " + inControl);
353 }
354 if (inControl == null) {
355 throw new IllegalArgumentException("Can't handle a null Control.");
356 }
357
358 try {
359 // Internal CHANGE_MODEL_CONTROL_ID
360 if (inControl.matchesID(CHANGE_MODEL_CONTROL_ID)) {
361 changeModel(inControl.getParameter());
362 } else {
363 // else subclass impl
364 ViewContext.getViewContext().startProgress();
365 try {
366 doHandleControl(inControl);
367 } finally {
368 ViewContext.getViewContext().stopProgress();
369 }
370 }
371 // For unhandled Controls
372 if (!inControl.isMatched()) {
373 if (LOG.isDebugEnabled()) {
374 LOG.debug("handleControl: not matched: " + inControl);
375 }
376 // Default handler for EXIT_CONTROL_ID
377 if (inControl.matchesID(EXIT_CONTROL_ID)) {
378 if (LOG.isDebugEnabled()) {
379 LOG.debug("handleControl: EXIT: " + getParent());
380 }
381 if (getParent() == null) {
382 if (ViewContext.getViewContext() == null) {
383 throw new RuntimeException("No ViewContext: setup at start of application" +
384 "using ViewContext.");
385 }
386 ViewContext.getViewContext().exit();
387 } else {
388 inControl.setParameter(this);
389 // propagate from this
390 inControl.markUnmatched();
391 // allow to bubble upwards to parent
392 passControlToParent(inControl);
393 }
394 } else if (inControl.matchesID(HIDE_VIEW_CONTROL_ID)) {
395 // else default handler for HIDE_VIEW_CONTROL_ID
396 hideView();
397 } else {
398 // else pass up chain of responsibility
399 passControlToParent(inControl);
400 }
401 }
402 if (!inControl.isMatched()) {
403 LOG.warn("Control not handled: " + inControl);
404 }
405 } catch (ControlException exception) {
406 inControl.markMatched();
407 // stop propagation of the Control!
408 inControl.populateControlException(exception);
409 handleControlException(exception);
410 } catch (RuntimeException exception) {
411 // Log unchecked exceptions even if app code ignores
412 LOG.error("Failed to handle Control: " + inControl, exception);
413 ControlException cex = new ControlException(HANDLE_CONTROL_RUNTIME_ERROR_MSG_ID, exception);
414 inControl.markMatched();
415 // stop propagation of the Control!
416 inControl.populateControlException(cex);
417 handleControlException(cex);
418 }
419 }
420
421
422 // ------------- Startup and shutdown -----------
423
424 /***
425 * <p>
426 *
427 * Starts the Controller and its bound View and Model. </p> Call this method
428 * after creating the Controller to make it perform its initial action. This
429 * method is not called automatically by Scope, so you have to call it
430 * yourself.<br>
431 * Default implementation here just calls showView() if a View is set.
432 */
433 public void startup() {
434 if (getView() != null) {
435 showView();
436 }
437 }
438
439
440 /***
441 * <p>
442 *
443 * Shutdown the Controller and its bound View and Model. </p> Can be called
444 * by a parent Controller to shutdown and remove this from the chain of
445 * responsibility. <br>
446 * Default implementation does this:
447 * <UL>
448 * <LI> call shutdown() on every child controller</LI>
449 * <LI> call hideView()</LI>
450 * <LI> setParent(null)</LI>
451 * </UL>
452 *
453 */
454 public void shutdown() {
455 if (Debug.ON) {
456 Debug.assertTrue(getChildren() != null);
457 }
458 // Make an array copy to avoid modifying while iterating
459 BasicController[] c = (BasicController[]) getChildren().toArray(new BasicController[0]);
460 for (int i = 0; i < c.length; ++i) {
461 c[i].shutdown();
462 }
463 hideView();
464 setParent(null);
465 }
466
467
468 /***
469 * Hook this Controller into the chain of responsiblity as a child of the
470 * passed Controller. See {@link #addChild}
471 *
472 * @param inParent The new parent value
473 */
474 protected final void setParent(BasicController inParent) {
475 if (parent != null) {
476 parent.getChildren().remove(this);
477 }
478
479 parent = inParent;
480
481 if (parent != null) {
482 parent.getChildren().add(this);
483 }
484 }
485
486
487 /***
488 * Feed a Control to the parent Controller up the chain of command.
489 *
490 * @param inControl The Control to delegate to the parent Controller
491 */
492 protected final void passControlToParent(Control inControl) {
493 if (inControl == null) {
494 throw new IllegalArgumentException("Can't pass null Control to parent.");
495 }
496
497 // thread-safety
498 Controller localParent = parent;
499 if (LOG.isDebugEnabled()) {
500 LOG.debug("passControlToParent: to: " + localParent + " control: " + inControl);
501 }
502
503 if (localParent == null) {
504 // Reached the top of this chain of command without handling the control
505 return;
506 }
507
508 localParent.handleControl(inControl);
509 }
510
511
512 // ------------- Convenience View management -----------
513 // Methods here just present a simpler API from ViewContext,
514 // so it may be usefull to call ViewContext directly if the
515 // functionality required is not present here
516
517 /***
518 * Show the view bound to this Controller.
519 */
520 protected final void showView() {
521 showView(getView());
522 }
523
524
525 /***
526 * Show the given view.
527 *
528 * @param inView The View to show
529 */
530 protected final void showView(View inView) {
531 if (inView == null) {
532 throw new RuntimeException("No View to show.");
533 }
534 if (ViewContext.getViewContext() == null) {
535 throw new RuntimeException("No ViewContext: setup at start of application using ViewContext.");
536 }
537 try {
538 ViewContext.getViewContext().showView(inView);
539 } catch (Exception e) {
540 // Log unchecked exceptions even if app code ignores
541 LOG.error("Failed to showView: " + inView, e);
542 }
543 }
544
545
546 /***
547 * Hide the View bound to this Controller.
548 */
549 protected final void hideView() {
550 hideView(getView());
551 }
552
553
554 /***
555 * Hide the given View
556 *
557 * @param inView The View to hide
558 */
559 protected final void hideView(View inView) {
560 if (inView == null) {
561 throw new RuntimeException("No View to hide.");
562 }
563 if (ViewContext.getViewContext() == null) {
564 throw new RuntimeException("No ViewContext: setup at start of application using ViewContext.");
565 }
566 try {
567 ViewContext.getViewContext().hideView(inView);
568 } catch (Exception e) {
569 // Log unchecked exceptions even if app code ignores
570 LOG.error("Failed to showView: " + inView, e);
571 }
572 }
573
574
575 /***
576 * Convenience to show an error using the current {@link
577 * org.scopemvc.controller.basic.ViewContext ViewContext}.
578 *
579 * @param inErrorTitle The title for the error message window
580 * @param inErrorMessage The content of the error message
581 */
582 protected final void showError(String inErrorTitle, String inErrorMessage) {
583 if (ViewContext.getViewContext() == null) {
584 throw new RuntimeException("No ViewContext: setup at start of application using ViewContext.");
585 }
586 ViewContext.getViewContext().showError(
587 inErrorTitle, inErrorMessage);
588 }
589
590
591 /***
592 * Bind a model object to a View if that is possible (model and view must be
593 * not null, the view must not have a selector marking it as being handled
594 * by a parent view)
595 *
596 * @param inView the View to bind
597 * @param inModel the model object to bind to the View
598 */
599 protected void bindModelToView(View inView, Object inModel) {
600 if (inView == null) {
601 return;
602 }
603 if (inView instanceof PropertyView && ((PropertyView) inView).getSelector() != null) {
604 // views with selectors set are never bound by this controller: assumed to be handled by parent
605 // ... that has delegated responsibility of a subsystem to this child. The high-level binding
606 // ... is done by the parent.
607 return;
608 }
609 inView.setBoundModel(inModel);
610 }
611
612
613 /***
614 * <P>
615 *
616 * Custom implementation of some presentation logic. </P> <P>
617 *
618 * Override this to recognise Controls that this Controller can handle. Any
619 * unhandled Controls are passed up the chain of responsibility to parent
620 * Controllers. <PRE>
621 * protected void doHandleControl(Control inControl) throws ControlException {
622 * if (inControl.matchesID(FOO_CONTROL_ID)) {
623 * doFoo(inControl.getParameter());
624 * } else if (inControl.matchesID(BAR_CONTROL_ID)) {
625 * doBar(inControl.getParameter());
626 * }
627 * }
628 * </PRE> </P> <P>
629 *
630 * If something goes wrong when running some presentation logic, throw a
631 * {@link org.scopemvc.core.ControlException ControlException} which results
632 * in a call to {@link #handleControlException}). </P>
633 *
634 * @param inControl The Control to handle
635 * @throws ControlException If something goes wrong when running some
636 * presentation logic
637 * @todo The implementation of doHandleControl is hugly, with its long if
638 * ... else if sequence. The Command pattern may help to provide a
639 * cleaner implementation to the users (ludovicc)
640 */
641 protected void doHandleControl(Control inControl) throws ControlException {
642 // do nothing by default -- see handleControl
643 }
644
645
646 /***
647 * Called by {@link #handleControl} when a {@link org.scopemvc.core.Control
648 * Control} throws a {@link org.scopemvc.core.ControlException
649 * ControlException}. <br>
650 * This implementation uses the {@link #showError} method. <br>
651 * A ContolException with a HANDLE_CONTROL_RUNTIME_ERROR_MSG_ID message can
652 * be generated when the Controller runs code that throws some unchecked
653 * exception.
654 *
655 * @param inException An exception thrown when something goes wrong with the
656 * presentation logic.
657 */
658 protected void handleControlException(ControlException inException) {
659 if (LOG.isDebugEnabled()) {
660 LOG.debug("handleControlException: " + inException);
661 }
662 if (inException == null) {
663 throw new IllegalArgumentException("Can't handle null ControlException.");
664 }
665
666 if (inException == null) {
667 showError(UIStrings.get("UnknownErrorTitle"),
668 UIStrings.get("UnknownErrorMessage"));
669 } else {
670 showError(inException.getLocalizedSourceControlName(),
671 inException.getLocalizedMessage());
672 }
673 }
674
675
676 // ---------------------- Internal CHANGE_MODEL_CONTROL_ID Control support ------------------------
677
678 /***
679 * Respond to CHANGE_MODEL_CONTROL_ID to keep the controller's model in sync
680 * with the currently shown model if it is changed as a submodel of a model
681 * managed by a parent controller.
682 *
683 * @param inParameter the new model object to set on this Controller.
684 */
685 private void changeModel(Object inParameter) {
686 setModel(inParameter);
687 }
688 }
689
This page was automatically generated by Maven