001 // Copyright 2004, 2005 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry; 016 017 import java.util.Collection; 018 import java.util.Collections; 019 import java.util.HashMap; 020 import java.util.HashSet; 021 import java.util.Iterator; 022 import java.util.List; 023 import java.util.Map; 024 025 import org.apache.hivemind.ApplicationRuntimeException; 026 import org.apache.hivemind.Messages; 027 import org.apache.hivemind.impl.BaseLocatable; 028 import org.apache.hivemind.util.Defense; 029 import org.apache.hivemind.util.PropertyUtils; 030 import org.apache.tapestry.bean.BeanProvider; 031 import org.apache.tapestry.engine.IPageLoader; 032 import org.apache.tapestry.event.PageEvent; 033 import org.apache.tapestry.listener.ListenerMap; 034 import org.apache.tapestry.spec.IComponentSpecification; 035 import org.apache.tapestry.spec.IContainedComponent; 036 037 /** 038 * Abstract base class implementing the {@link IComponent}interface. 039 * 040 * @author Howard Lewis Ship 041 */ 042 043 public abstract class AbstractComponent extends BaseLocatable implements IComponent 044 { 045 /** 046 * The page that contains the component, possibly itself (if the component is in fact, a page). 047 */ 048 049 private IPage _page; 050 051 /** 052 * The component which contains the component. This will only be null if the component is 053 * actually a page. 054 */ 055 056 private IComponent _container; 057 058 /** 059 * The simple id of this component. 060 */ 061 062 private String _id; 063 064 /** 065 * The fully qualified id of this component. This is calculated the first time it is needed, 066 * then cached for later. 067 */ 068 069 private String _idPath; 070 071 private static final int MAP_SIZE = 5; 072 073 /** 074 * A {@link Map}of all bindings (for which there isn't a corresponding JavaBeans property); the 075 * keys are the names of formal and informal parameters. 076 */ 077 078 private Map _bindings; 079 080 private Map _components; 081 082 private static final int BODY_INIT_SIZE = 5; 083 084 private INamespace _namespace; 085 086 /** 087 * Used in place of JDK 1.3's Collections.EMPTY_MAP (which is not available in JDK 1.2). 088 */ 089 090 private static final Map EMPTY_MAP = Collections.unmodifiableMap(new HashMap(1)); 091 092 /** 093 * The number of {@link IRender}objects in the body of this component. 094 */ 095 096 private int _bodyCount = 0; 097 098 /** 099 * An aray of elements in the body of this component. 100 */ 101 102 private IRender[] _body; 103 104 /** 105 * The components' asset map. 106 */ 107 108 private Map _assets; 109 110 /** 111 * A mapping that allows public instance methods to be dressed up as {@link IActionListener} 112 * listener objects. 113 * 114 * @since 1.0.2 115 */ 116 117 private ListenerMap _listeners; 118 119 /** 120 * A bean provider; these are lazily created as needed. 121 * 122 * @since 1.0.4 123 */ 124 125 private IBeanProvider _beans; 126 127 /** 128 * Returns true if the component is currently rendering. 129 * 130 * @see #prepareForRender(IRequestCycle) 131 * @see #cleanupAfterRender(IRequestCycle) 132 * @since 4.0 133 */ 134 135 private boolean _rendering; 136 137 /** 138 * @since 4.0 139 */ 140 141 private boolean _active; 142 143 /** @since 4.0 */ 144 145 private IContainedComponent _containedComponent; 146 147 public void addAsset(String name, IAsset asset) 148 { 149 Defense.notNull(name, "name"); 150 Defense.notNull(asset, "asset"); 151 152 checkActiveLock(); 153 154 if (_assets == null) 155 _assets = new HashMap(MAP_SIZE); 156 157 _assets.put(name, asset); 158 } 159 160 public void addComponent(IComponent component) 161 { 162 Defense.notNull(component, "component"); 163 164 checkActiveLock(); 165 166 if (_components == null) 167 _components = new HashMap(MAP_SIZE); 168 169 _components.put(component.getId(), component); 170 } 171 172 /** 173 * Adds an element (which may be static text or a component) as a body element of this 174 * component. Such elements are rendered by {@link #renderBody(IMarkupWriter, IRequestCycle)}. 175 * 176 * @since 2.2 177 */ 178 179 public void addBody(IRender element) 180 { 181 Defense.notNull(element, "element"); 182 183 // TODO: Tweak the ordering of operations inside the PageLoader so that this 184 // check is allowable. Currently, the component is entering active state 185 // before it loads its template. 186 187 // checkActiveLock(); 188 189 // Should check the specification to see if this component 190 // allows body. Curently, this is checked by the component 191 // in render(), which is silly. 192 193 if (_body == null) 194 { 195 _body = new IRender[BODY_INIT_SIZE]; 196 _body[0] = element; 197 198 _bodyCount = 1; 199 return; 200 } 201 202 // No more room? Make the array bigger. 203 204 if (_bodyCount == _body.length) 205 { 206 IRender[] newWrapped; 207 208 newWrapped = new IRender[_body.length * 2]; 209 210 System.arraycopy(_body, 0, newWrapped, 0, _bodyCount); 211 212 _body = newWrapped; 213 } 214 215 _body[_bodyCount++] = element; 216 } 217 218 /** 219 * Invokes {@link #finishLoad()}. Subclasses may overide as needed, but must invoke this 220 * implementation. {@link BaseComponent} loads its HTML template. 221 */ 222 223 public void finishLoad(IRequestCycle cycle, IPageLoader loader, 224 IComponentSpecification specification) 225 { 226 finishLoad(); 227 } 228 229 /** 230 * Converts informal parameters into additional attributes on the curently open tag. 231 * <p> 232 * Invoked from subclasses to allow additional attributes to be specified within a tag (this 233 * works best when there is a one-to-one corespondence between an {@link IComponent}and a HTML 234 * element. 235 * <p> 236 * Iterates through the bindings for this component. Filters out bindings for formal parameters. 237 * <p> 238 * For each acceptible key, the value is extracted using {@link IBinding#getObject()}. If the 239 * value is null, no attribute is written. 240 * <p> 241 * If the value is an instance of {@link IAsset}, then {@link IAsset#buildURL()} 242 * is invoked to convert the asset to a URL. 243 * <p> 244 * Finally, {@link IMarkupWriter#attribute(String,String)}is invoked with the value (or the 245 * URL). 246 * <p> 247 * The most common use for informal parameters is to support the HTML class attribute (for use 248 * with cascading style sheets) and to specify JavaScript event handlers. 249 * <p> 250 * Components are only required to generate attributes on the result phase; this can be skipped 251 * during the rewind phase. 252 */ 253 254 protected void renderInformalParameters(IMarkupWriter writer, IRequestCycle cycle) 255 { 256 String attribute; 257 258 if (_bindings == null) 259 return; 260 261 Iterator i = _bindings.entrySet().iterator(); 262 263 while (i.hasNext()) 264 { 265 Map.Entry entry = (Map.Entry) i.next(); 266 String name = (String) entry.getKey(); 267 268 if (isFormalParameter(name)) 269 continue; 270 271 IBinding binding = (IBinding) entry.getValue(); 272 273 Object value = binding.getObject(); 274 if (value == null) 275 continue; 276 277 if (value instanceof IAsset) 278 { 279 IAsset asset = (IAsset) value; 280 281 // Get the URL of the asset and insert that. 282 283 attribute = asset.buildURL(); 284 } 285 else 286 attribute = value.toString(); 287 288 writer.attribute(name, attribute); 289 } 290 291 } 292 293 /** @since 4.0 */ 294 private boolean isFormalParameter(String name) 295 { 296 Defense.notNull(name, "name"); 297 298 return getSpecification().getParameter(name) != null; 299 } 300 301 /** 302 * Returns the named binding, or null if it doesn't exist. 303 * <p> 304 * In Tapestry 3.0, it was possible to force a binding to be stored in a component property by 305 * defining a concrete or abstract property named "nameBinding" of type {@link IBinding}. This 306 * has been removed in release 4.0 and bindings are always stored inside a Map of the component. 307 * 308 * @see #setBinding(String,IBinding) 309 */ 310 311 public IBinding getBinding(String name) 312 { 313 Defense.notNull(name, "name"); 314 315 if (_bindings == null) 316 return null; 317 318 return (IBinding) _bindings.get(name); 319 } 320 321 /** 322 * Returns true if the specified parameter is bound. 323 * 324 * @since 4.0 325 */ 326 327 public boolean isParameterBound(String parameterName) 328 { 329 Defense.notNull(parameterName, "parameterName"); 330 331 return _bindings != null && _bindings.containsKey(parameterName); 332 } 333 334 public IComponent getComponent(String id) 335 { 336 Defense.notNull(id, "id"); 337 338 IComponent result = null; 339 340 if (_components != null) 341 result = (IComponent) _components.get(id); 342 343 if (result == null) 344 throw new ApplicationRuntimeException(Tapestry.format("no-such-component", this, id), 345 this, null, null); 346 347 return result; 348 } 349 350 public IComponent getContainer() 351 { 352 return _container; 353 } 354 355 public void setContainer(IComponent value) 356 { 357 checkActiveLock(); 358 359 if (_container != null) 360 throw new ApplicationRuntimeException(Tapestry 361 .getMessage("AbstractComponent.attempt-to-change-container")); 362 363 _container = value; 364 } 365 366 /** 367 * Returns the name of the page, a slash, and this component's id path. Pages are different, 368 * they override this method to simply return their page name. 369 * 370 * @see #getIdPath() 371 */ 372 373 public String getExtendedId() 374 { 375 if (_page == null) 376 return null; 377 378 return _page.getPageName() + "/" + getIdPath(); 379 } 380 381 public String getId() 382 { 383 return _id; 384 } 385 386 public void setId(String value) 387 { 388 if (_id != null) 389 throw new ApplicationRuntimeException(Tapestry 390 .getMessage("AbstractComponent.attempt-to-change-component-id")); 391 392 _id = value; 393 } 394 395 public String getIdPath() 396 { 397 String containerIdPath; 398 399 if (_container == null) 400 throw new NullPointerException(Tapestry 401 .format("AbstractComponent.null-container", this)); 402 403 containerIdPath = _container.getIdPath(); 404 405 if (containerIdPath == null) 406 _idPath = _id; 407 else 408 _idPath = containerIdPath + "." + _id; 409 410 return _idPath; 411 } 412 413 public IPage getPage() 414 { 415 return _page; 416 } 417 418 public void setPage(IPage value) 419 { 420 if (_page != null) 421 throw new ApplicationRuntimeException(Tapestry 422 .getMessage("AbstractComponent.attempt-to-change-page")); 423 424 _page = value; 425 } 426 427 /** 428 * Renders all elements wrapped by the receiver. 429 */ 430 431 public void renderBody(IMarkupWriter writer, IRequestCycle cycle) 432 { 433 for (int i = 0; i < _bodyCount; i++) 434 _body[i].render(writer, cycle); 435 } 436 437 /** 438 * Adds the binding with the given name, replacing any existing binding with that name. 439 * <p> 440 * 441 * @see #getBinding(String) 442 */ 443 444 public void setBinding(String name, IBinding binding) 445 { 446 Defense.notNull(name, "name"); 447 Defense.notNull(binding, "binding"); 448 449 if (_bindings == null) 450 _bindings = new HashMap(MAP_SIZE); 451 452 _bindings.put(name, binding); 453 } 454 455 public String toString() 456 { 457 StringBuffer buffer; 458 459 buffer = new StringBuffer(super.toString()); 460 461 buffer.append('['); 462 463 buffer.append(getExtendedId()); 464 465 buffer.append(']'); 466 467 return buffer.toString(); 468 } 469 470 /** 471 * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null, 472 * but may return an empty map. The returned map is immutable. 473 */ 474 475 public Map getComponents() 476 { 477 if (_components == null) 478 return EMPTY_MAP; 479 480 return Collections.unmodifiableMap(_components); 481 482 } 483 484 public Map getAssets() 485 { 486 if (_assets == null) 487 return EMPTY_MAP; 488 489 return Collections.unmodifiableMap(_assets); 490 } 491 492 public IAsset getAsset(String name) 493 { 494 if (_assets == null) 495 return null; 496 497 return (IAsset) _assets.get(name); 498 } 499 500 public Collection getBindingNames() 501 { 502 // If no conainer, i.e. a page, then no bindings. 503 504 if (_container == null) 505 return null; 506 507 HashSet result = new HashSet(); 508 509 // All the informal bindings go into the bindings Map. 510 511 if (_bindings != null) 512 result.addAll(_bindings.keySet()); 513 514 // Now, iterate over the formal parameters and add the formal parameters 515 // that have a binding. 516 517 List names = getSpecification().getParameterNames(); 518 519 int count = names.size(); 520 521 for (int i = 0; i < count; i++) 522 { 523 String name = (String) names.get(i); 524 525 if (result.contains(name)) 526 continue; 527 528 if (getBinding(name) != null) 529 result.add(name); 530 } 531 532 return result; 533 } 534 535 /** 536 * Returns an unmodifiable {@link Map}of all bindings for this component. 537 * 538 * @since 1.0.5 539 */ 540 541 public Map getBindings() 542 { 543 if (_bindings == null) 544 return Collections.EMPTY_MAP; 545 546 return Collections.unmodifiableMap(_bindings); 547 } 548 549 /** 550 * Returns a {@link ListenerMap} for the component. A ListenerMap contains a number of 551 * synthetic read-only properties that implement the {@link IActionListener}interface, but in 552 * fact, cause public instance methods to be invoked. 553 * 554 * @since 1.0.2 555 */ 556 557 public ListenerMap getListeners() 558 { 559 // This is what's called a violation of the Law of Demeter! 560 // This should probably be converted over to some kind of injection, as with 561 // getMessages(), etc. 562 563 if (_listeners == null) 564 _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource() 565 .getListenerMapForObject(this); 566 567 return _listeners; 568 } 569 570 /** 571 * Returns the {@link IBeanProvider}for this component. This is lazily created the first time 572 * it is needed. 573 * 574 * @since 1.0.4 575 */ 576 577 public IBeanProvider getBeans() 578 { 579 if (_beans == null) 580 _beans = new BeanProvider(this); 581 582 return _beans; 583 } 584 585 /** 586 * Invoked, as a convienience, from 587 * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation 588 * does nothing. Subclasses may override without invoking this implementation. 589 * 590 * @since 1.0.5 591 */ 592 593 protected void finishLoad() 594 { 595 } 596 597 /** 598 * The main method used to render the component. Invokes 599 * {@link #prepareForRender(IRequestCycle)}, then 600 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}. 601 * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block. 602 * <p> 603 * Subclasses should not override this method; instead they will implement 604 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}. 605 * 606 * @since 2.0.3 607 */ 608 609 public final void render(IMarkupWriter writer, IRequestCycle cycle) 610 { 611 try 612 { 613 _rendering = true; 614 615 prepareForRender(cycle); 616 617 renderComponent(writer, cycle); 618 } 619 finally 620 { 621 _rendering = false; 622 623 cleanupAfterRender(cycle); 624 } 625 } 626 627 /** 628 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render. 629 * This implementation sets JavaBeans properties from matching bound parameters. This 630 * implementation does nothing. 631 * 632 * @since 2.0.3 633 */ 634 635 protected void prepareForRender(IRequestCycle cycle) 636 { 637 } 638 639 /** 640 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component 641 * (with any parameter values already set). This is the method that subclasses must implement. 642 * 643 * @since 2.0.3 644 */ 645 646 protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle); 647 648 /** 649 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders. This 650 * implementation does nothing. 651 * 652 * @since 2.0.3 653 */ 654 655 protected void cleanupAfterRender(IRequestCycle cycle) 656 { 657 } 658 659 public INamespace getNamespace() 660 { 661 return _namespace; 662 } 663 664 public void setNamespace(INamespace namespace) 665 { 666 _namespace = namespace; 667 } 668 669 /** 670 * Returns the body of the component, the element (which may be static HTML or components) that 671 * the component immediately wraps. May return null. Do not modify the returned array. The array 672 * may be padded with nulls. 673 * 674 * @since 2.3 675 * @see #getBodyCount() 676 */ 677 678 public IRender[] getBody() 679 { 680 return _body; 681 } 682 683 /** 684 * Returns the active number of elements in the the body, which may be zero. 685 * 686 * @since 2.3 687 * @see #getBody() 688 */ 689 690 public int getBodyCount() 691 { 692 return _bodyCount; 693 } 694 695 /** 696 * Empty implementation of 697 * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows 698 * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement 699 * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method. 700 * 701 * @since 3.0 702 */ 703 704 public void pageEndRender(PageEvent event) 705 { 706 } 707 708 /** 709 * Sets a property of a component. 710 * 711 * @see IComponent 712 * @since 3.0 713 * @deprecated 714 */ 715 public void setProperty(String propertyName, Object value) 716 { 717 PropertyUtils.write(this, propertyName, value); 718 } 719 720 /** 721 * Gets a property of a component. 722 * 723 * @see IComponent 724 * @since 3.0 725 * @deprecated 726 */ 727 public Object getProperty(String propertyName) 728 { 729 return PropertyUtils.read(this, propertyName); 730 } 731 732 /** 733 * @since 4.0 734 */ 735 736 public final boolean isRendering() 737 { 738 return _rendering; 739 } 740 741 /** 742 * Returns true if the component has been transitioned into its active state by invoking 743 * {@link #enterActiveState()} 744 * 745 * @since 4.0 746 */ 747 748 protected final boolean isInActiveState() 749 { 750 return _active; 751 } 752 753 /** @since 4.0 */ 754 public final void enterActiveState() 755 { 756 _active = true; 757 } 758 759 /** @since 4.0 */ 760 761 protected final void checkActiveLock() 762 { 763 if (_active) 764 throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this)); 765 } 766 767 public Messages getMessages() 768 { 769 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages")); 770 } 771 772 public IComponentSpecification getSpecification() 773 { 774 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification")); 775 } 776 777 /** 778 * Returns a localized message. 779 * 780 * @since 3.0 781 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. 782 */ 783 784 public String getMessage(String key) 785 { 786 return getMessages().getMessage(key); 787 } 788 789 /** 790 * Formats a localized message string, using 791 * {@link Messages#format(java.lang.String, java.lang.Object[])}. 792 * 793 * @param key 794 * the key used to obtain a localized pattern 795 * @param arguments 796 * passed to the formatter 797 * @since 3.0 798 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. 799 */ 800 801 public String format(String key, Object[] arguments) 802 { 803 return getMessages().format(key, arguments); 804 } 805 806 /** 807 * Convienience method for invoking {@link IMessages#format(String, Locale, Object)} 808 * 809 * @since 3.0 810 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. 811 */ 812 813 public String format(String key, Object argument) 814 { 815 return getMessages().format(key, argument); 816 } 817 818 /** 819 * Convienience method for invoking {@link Messages#format(String, Object, Object)}. 820 * 821 * @since 3.0 822 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. 823 */ 824 825 public String format(String key, Object argument1, Object argument2) 826 { 827 return getMessages().format(key, argument1, argument2); 828 } 829 830 /** 831 * Convienience method for {@link Messages#format(String, Object, Object, Object)}. 832 * 833 * @since 3.0 834 * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. 835 */ 836 837 public String format(String key, Object argument1, Object argument2, Object argument3) 838 { 839 return getMessages().format(key, argument1, argument2, argument3); 840 } 841 842 /** @since 4.0 */ 843 public final IContainedComponent getContainedComponent() 844 { 845 return _containedComponent; 846 } 847 848 /** @since 4.0 */ 849 public final void setContainedComponent(IContainedComponent containedComponent) 850 { 851 Defense.notNull(containedComponent, "containedComponent"); 852 853 if (_containedComponent != null) 854 throw new ApplicationRuntimeException(TapestryMessages 855 .attemptToChangeContainedComponent(this)); 856 857 _containedComponent = containedComponent; 858 } 859 860 }