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.getMessage("AbstractComponent.attempt-to-change-component-id")); 434 435 _id = value; 436 } 437 438 public String getIdPath() 439 { 440 if (_idPath != null) 441 return _idPath; 442 443 String containerIdPath; 444 445 if (_container == null) 446 throw new NullPointerException(Tapestry.format("AbstractComponent.null-container", this)); 447 448 containerIdPath = _container.getIdPath(); 449 450 if (containerIdPath == null) 451 _idPath = _id; 452 else 453 _idPath = containerIdPath + "." + _id; 454 455 return _idPath; 456 } 457 458 /** 459 * {@inheritDoc} 460 * @since 4.1 461 */ 462 public abstract String getClientId(); 463 464 public abstract void setClientId(String id); 465 466 /** 467 * {@inheritDoc} 468 */ 469 public String peekClientId() 470 { 471 if (getPage() == null) 472 return null; 473 474 String id = getSpecifiedId(); 475 if (id == null) 476 return null; 477 478 return getPage().getRequestCycle().peekUniqueId(TapestryUtils.convertTapestryIdToNMToken(id)); 479 } 480 481 protected void generateClientId() 482 { 483 String id = getSpecifiedId(); 484 485 if (id != null && getPage() != null && getPage().getRequestCycle() != null) 486 setClientId(getPage().getRequestCycle().getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id))); 487 } 488 489 protected String getBoundId() 490 { 491 if (_bindings == null) 492 return null; 493 494 IBinding id = (IBinding)_bindings.get("id"); 495 496 if (id == null || id.getObject() == null) 497 return null; 498 499 return id.getObject().toString(); 500 } 501 502 public String getTemplateTagName() 503 { 504 return _templateTagName; 505 } 506 507 /** 508 * {@inheritDoc} 509 */ 510 public void setTemplateTagName(String tag) 511 { 512 if (_templateTagName != null) 513 throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-template-tag")); 514 515 _templateTagName = tag; 516 } 517 518 public IPage getPage() 519 { 520 return _page; 521 } 522 523 public void setPage(IPage value) 524 { 525 if (_page != null) 526 throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-page")); 527 528 _page = value; 529 } 530 531 /** 532 * Renders all elements wrapped by the receiver. 533 */ 534 535 public void renderBody(IMarkupWriter writer, IRequestCycle cycle) 536 { 537 for (int i = 0; i < _bodyCount; i++) 538 cycle.getResponseBuilder().render(writer, _body[i], cycle); 539 } 540 541 /** 542 * Adds the binding with the given name, replacing any existing binding with that name. 543 * <p> 544 * 545 * @see #getBinding(String) 546 */ 547 548 public void setBinding(String name, IBinding binding) 549 { 550 Defense.notNull(name, "name"); 551 Defense.notNull(binding, "binding"); 552 553 if (_bindings == null) 554 _bindings = new HashMap(MAP_SIZE); 555 556 _bindings.put(name, binding); 557 } 558 559 public String toString() 560 { 561 StringBuffer buffer; 562 563 buffer = new StringBuffer(super.toString()); 564 565 buffer.append('['); 566 567 buffer.append(getExtendedId()); 568 569 buffer.append(']'); 570 571 return buffer.toString(); 572 } 573 574 /** 575 * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null, 576 * but may return an empty map. The returned map is immutable. 577 */ 578 579 public Map getComponents() 580 { 581 if (_components == null) 582 return EMPTY_MAP; 583 584 return _components; 585 586 } 587 588 public Map getAssets() 589 { 590 if (_assets == null) 591 return EMPTY_MAP; 592 593 return _assets; 594 } 595 596 public IAsset getAsset(String name) 597 { 598 if (_assets == null) 599 return null; 600 601 return (IAsset) _assets.get(name); 602 } 603 604 public Collection getBindingNames() 605 { 606 // If no conainer, i.e. a page, then no bindings. 607 608 if (_container == null) 609 return null; 610 611 HashSet result = new HashSet(); 612 613 // All the informal bindings go into the bindings Map. 614 615 if (_bindings != null) 616 result.addAll(_bindings.keySet()); 617 618 // Now, iterate over the formal parameters and add the formal parameters 619 // that have a binding. 620 621 List names = getSpecification().getParameterNames(); 622 623 int count = names.size(); 624 625 for (int i = 0; i < count; i++) 626 { 627 String name = (String) names.get(i); 628 629 if (result.contains(name)) 630 continue; 631 632 if (getBinding(name) != null) 633 result.add(name); 634 } 635 636 return result; 637 } 638 639 /** 640 * Returns an unmodifiable {@link Map}of all bindings for this component. 641 * 642 * @since 1.0.5 643 */ 644 645 public Map getBindings() 646 { 647 if (_bindings == null) 648 return EMPTY_MAP; 649 650 return _bindings; 651 } 652 653 /** 654 * Returns a {@link ListenerMap} for the component. A ListenerMap contains a number of 655 * synthetic read-only properties that implement the {@link IActionListener}interface, but in 656 * fact, cause public instance methods to be invoked. 657 * 658 * @since 1.0.2 659 */ 660 661 public ListenerMap getListeners() 662 { 663 // This is what's called a violation of the Law of Demeter! 664 // This should probably be converted over to some kind of injection, as with 665 // getMessages(), etc. 666 667 if (_listeners == null) 668 _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource().getListenerMapForObject(this); 669 670 return _listeners; 671 } 672 673 /** 674 * Returns the {@link IBeanProvider}for this component. This is lazily created the first time 675 * it is needed. 676 * 677 * @since 1.0.4 678 */ 679 680 public IBeanProvider getBeans() 681 { 682 if (_beans == null) 683 _beans = new BeanProvider(this); 684 685 return _beans; 686 } 687 688 /** 689 * Invoked, as a convienience, from 690 * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation 691 * does nothing. Subclasses may override without invoking this implementation. 692 * 693 * @since 1.0.5 694 */ 695 696 protected void finishLoad() 697 { 698 } 699 700 /** 701 * The main method used to render the component. Invokes 702 * {@link #prepareForRender(IRequestCycle)}, then 703 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}. 704 * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block. 705 * <p> 706 * Subclasses should not override this method; instead they will implement 707 * {@link #renderComponent(IMarkupWriter, IRequestCycle)}. 708 * 709 * @since 2.0.3 710 */ 711 712 public final void render(IMarkupWriter writer, IRequestCycle cycle) 713 { 714 try 715 { 716 _rendering = true; 717 718 cycle.renderStackPush(this); 719 720 generateClientId(); 721 722 prepareForRender(cycle); 723 724 renderComponent(writer, cycle); 725 } 726 finally 727 { 728 _rendering = false; 729 730 cleanupAfterRender(cycle); 731 732 cycle.renderStackPop(); 733 } 734 } 735 736 /** 737 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render. 738 * This implementation sets JavaBeans properties from matching bound parameters. The default 739 * implementation of this method is empty. 740 * 741 * @since 2.0.3 742 */ 743 744 protected void prepareForRender(IRequestCycle cycle) 745 { 746 } 747 748 /** 749 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component 750 * (with any parameter values already set). This is the method that subclasses must implement. 751 * 752 * @since 2.0.3 753 */ 754 755 protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle); 756 757 /** 758 * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders. 759 * 760 * @since 2.0.3 761 */ 762 763 protected void cleanupAfterRender(IRequestCycle cycle) 764 { 765 getRenderWorker().renderComponent(cycle, this); 766 } 767 768 public INamespace getNamespace() 769 { 770 return _namespace; 771 } 772 773 public void setNamespace(INamespace namespace) 774 { 775 _namespace = namespace; 776 } 777 778 /** 779 * Returns the body of the component, the element (which may be static HTML or components) that 780 * the component immediately wraps. May return null. Do not modify the returned array. The array 781 * may be padded with nulls. 782 * 783 * @since 2.3 784 * @see #getBodyCount() 785 */ 786 787 public IRender[] getBody() 788 { 789 return _body; 790 } 791 792 /** 793 * Returns the active number of elements in the the body, which may be zero. 794 * 795 * @since 2.3 796 * @see #getBody() 797 */ 798 799 public int getBodyCount() 800 { 801 return _bodyCount; 802 } 803 804 /** 805 * Empty implementation of 806 * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows 807 * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement 808 * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method. 809 * 810 * @since 3.0 811 */ 812 813 public void pageEndRender(PageEvent event) 814 { 815 } 816 817 /** 818 * @since 4.0 819 */ 820 821 public final boolean isRendering() 822 { 823 return _rendering; 824 } 825 826 /** 827 * Returns true if the component has been transitioned into its active state by invoking 828 * {@link #enterActiveState()}. 829 * 830 * @since 4.0 831 */ 832 833 protected final boolean isInActiveState() 834 { 835 return _active; 836 } 837 838 /** @since 4.0 */ 839 public final void enterActiveState() 840 { 841 _active = true; 842 } 843 844 /** @since 4.0 */ 845 846 protected final void checkActiveLock() 847 { 848 if (_active) 849 throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this)); 850 } 851 852 public Messages getMessages() 853 { 854 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages")); 855 } 856 857 public IComponentSpecification getSpecification() 858 { 859 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification")); 860 } 861 862 /** @since 4.0 */ 863 public final IContainedComponent getContainedComponent() 864 { 865 return _containedComponent; 866 } 867 868 /** @since 4.0 */ 869 public final void setContainedComponent(IContainedComponent containedComponent) 870 { 871 Defense.notNull(containedComponent, "containedComponent"); 872 873 if (_containedComponent != null) 874 throw new ApplicationRuntimeException(TapestryMessages 875 .attemptToChangeContainedComponent(this)); 876 877 _containedComponent = containedComponent; 878 } 879 880 /** 881 * {@inheritDoc} 882 */ 883 public IComponentEventInvoker getEventInvoker() 884 { 885 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getEventInvoker")); 886 } 887 888 /** 889 * {@inheritDoc} 890 */ 891 public void triggerEvent(IRequestCycle cycle, BrowserEvent event) 892 { 893 getEventInvoker().invokeListeners(this, cycle, event); 894 } 895 896 public ComponentRenderWorker getRenderWorker() 897 { 898 throw new IllegalStateException(TapestryMessages.providedByEnhancement("getRenderWorker")); 899 } 900 901 /** 902 * {@inheritDoc} 903 */ 904 public boolean isStateful() 905 { 906 return false; 907 } 908 909 /** 910 * {@inheritDoc} 911 */ 912 public int hashCode() 913 { 914 final int prime = 31; 915 int result = 1; 916 result = prime * result + ((getClientId() == null) ? 0 : getClientId().hashCode()); 917 result = prime * result + ((_id == null) ? 0 : _id.hashCode()); 918 return result; 919 } 920 921 /** 922 * {@inheritDoc} 923 */ 924 public boolean equals(Object obj) 925 { 926 if (this == obj) return true; 927 if (obj == null) return false; 928 if (getClass() != obj.getClass()) return false; 929 final AbstractComponent other = (AbstractComponent) obj; 930 if (getClientId() == null) { 931 if (other.getClientId() != null) return false; 932 } else if (!getClientId().equals(other.getClientId())) return false; 933 if (_id == null) { 934 if (other._id != null) return false; 935 } else if (!_id.equals(other._id)) return false; 936 return true; 937 } 938 }