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