View Javadoc
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