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.spec; 016 017 import org.apache.hivemind.ApplicationRuntimeException; 018 import org.apache.hivemind.HiveMind; 019 import org.apache.hivemind.Resource; 020 import org.apache.hivemind.util.Defense; 021 import org.apache.hivemind.util.ToStringBuilder; 022 import org.apache.tapestry.IComponent; 023 import org.apache.tapestry.IForm; 024 import org.apache.tapestry.event.BrowserEvent; 025 import org.apache.tapestry.internal.event.ComponentEventProperty; 026 import org.apache.tapestry.internal.event.EventBoundListener; 027 028 import java.util.*; 029 030 /** 031 * A specification for a component, as read from an XML specification file. 032 * <p> 033 * A specification consists of 034 * <ul> 035 * <li>An implementing class 036 * <li>An optional description 037 * <li>A set of contained components 038 * <li>Bindings for the properties of each contained component 039 * <li>A set of named assets 040 * <li>Definitions for managed beans 041 * <li>Any reserved names (used for HTML attributes) 042 * <li>Declared properties 043 * <li>Property injections 044 * </ul> 045 * <p> 046 * From this information, an actual component may be instantiated and initialized. Instantiating a 047 * component is usually a recursive process, since to initialize a container component, it is 048 * necessary to instantiate and initialize its contained components as well. 049 * 050 * @see org.apache.tapestry.IComponent 051 * @see IContainedComponent 052 * @see org.apache.tapestry.engine.IPageLoader 053 * @author Howard Lewis Ship 054 */ 055 056 public class ComponentSpecification extends LocatablePropertyHolder implements IComponentSpecification 057 { 058 /** 059 * Keyed on component id, value is {@link IContainedComponent}. 060 */ 061 062 protected Map _components; 063 064 /** 065 * Keyed on asset name, value is {@link IAssetSpecification}. 066 */ 067 068 protected Map _assets; 069 070 /** 071 * Defines all formal parameters. Keyed on parameter name, value is 072 * {@link IParameterSpecification}. 073 */ 074 075 protected Map _parameters; 076 077 /** 078 * Defines all helper beans. Keyed on name, value is {@link IBeanSpecification}. 079 * 080 * @since 1.0.4 081 */ 082 083 protected Map _beans; 084 085 /** 086 * The names of all reserved informal parameter names (as lower-case). This allows the page 087 * loader to filter out any informal parameters during page load, rather than during render. 088 * 089 * @since 1.0.5 090 */ 091 092 protected Set _reservedParameterNames; 093 094 private String _componentClassName; 095 096 /** @since 1.0.9 * */ 097 098 private String _description; 099 100 /** 101 * Is the component allowed to have a body (that is, wrap other elements?). 102 */ 103 104 private boolean _allowBody = true; 105 106 /** 107 * Is the component allow to have informal parameter specified. 108 */ 109 110 private boolean _allowInformalParameters = true; 111 112 /** 113 * The XML Public Id used when the page or component specification was read (if applicable). 114 * 115 * @since 2.2 116 */ 117 118 private String _publicId; 119 120 /** 121 * Indicates that the specification is for a page, not a component. 122 * 123 * @since 2.2 124 */ 125 126 private boolean _pageSpecification; 127 128 /** 129 * The location from which the specification was obtained. 130 * 131 * @since 3.0 132 */ 133 134 private Resource _specificationLocation; 135 136 /** 137 * A Map of {@link IPropertySpecification}keyed on the name of the property. 138 * 139 * @since 3.0 140 */ 141 142 private Map _propertySpecifications; 143 144 /** 145 * List of {@link InjectSpecification}. 146 * 147 * @since 4.0 148 */ 149 150 private List _injectSpecifications; 151 152 /** 153 * Keyed on property name, value is some other object (such as an IAssetSpecification) that has 154 * claimed a property of the page. 155 * 156 * @since 4.0 157 */ 158 159 private Map _claimedProperties; 160 161 /** 162 * @since 4.0 163 */ 164 165 private boolean _deprecated = false; 166 167 private Map _componentEvents = new HashMap(); 168 private Map _elementEvents = new HashMap(); 169 170 /** 171 * @throws ApplicationRuntimeException 172 * if the name already exists. 173 */ 174 175 public void addAsset(String name, IAssetSpecification asset) 176 { 177 if (_assets == null) 178 _assets = new HashMap(); 179 180 IAssetSpecification existing = (IAssetSpecification) _assets.get(name); 181 182 if (existing != null) 183 throw new ApplicationRuntimeException(SpecMessages.duplicateAsset(name, existing), 184 asset.getLocation(), null); 185 186 claimProperty(asset.getPropertyName(), asset); 187 188 _assets.put(name, asset); 189 } 190 191 /** 192 * @throws ApplicationRuntimeException if the id is already defined. 193 */ 194 195 public void addComponent(String id, IContainedComponent component) 196 { 197 if (_components == null) 198 _components = new HashMap(); 199 200 IContainedComponent existing = (IContainedComponent) _components.get(id); 201 202 if (existing != null) 203 throw new ApplicationRuntimeException(SpecMessages.duplicateComponent(id, existing), 204 component.getLocation(), null); 205 206 _components.put(id, component); 207 208 claimProperty(component.getPropertyName(), component); 209 } 210 211 /** 212 * Adds the parameter. The name is added as a reserved name. 213 * 214 * @throws ApplicationRuntimeException if the name already exists. 215 */ 216 217 public void addParameter(IParameterSpecification spec) 218 { 219 if (_parameters == null) 220 _parameters = new HashMap(); 221 222 String name = spec.getParameterName(); 223 224 addParameterByName(name, spec); 225 226 Iterator i = spec.getAliasNames().iterator(); 227 while (i.hasNext()) 228 { 229 String alias = (String) i.next(); 230 231 addParameterByName(alias, spec); 232 } 233 234 claimProperty(spec.getPropertyName(), spec); 235 } 236 237 private void addParameterByName(String name, IParameterSpecification spec) 238 { 239 IParameterSpecification existing = (IParameterSpecification) _parameters.get(name); 240 241 if (existing != null) 242 throw new ApplicationRuntimeException(SpecMessages.duplicateParameter(name, existing), 243 spec.getLocation(), null); 244 245 _parameters.put(name, spec); 246 247 addReservedParameterName(name); 248 } 249 250 /** 251 * Returns true if the component is allowed to wrap other elements (static HTML or other 252 * components). The default is true. 253 * 254 * @see #setAllowBody(boolean) 255 */ 256 257 public boolean getAllowBody() 258 { 259 return _allowBody; 260 } 261 262 /** 263 * Returns true if the component allows informal parameters (parameters not formally defined). 264 * Informal parameters are generally used to create additional HTML attributes for an HTML tag 265 * rendered by the component. This is often used to specify JavaScript event handlers or the 266 * class of the component (for Cascarding Style Sheets). 267 * <p> 268 * The default value is true. 269 * 270 * @see #setAllowInformalParameters(boolean) 271 */ 272 273 public boolean getAllowInformalParameters() 274 { 275 return _allowInformalParameters; 276 } 277 278 /** 279 * Returns the {@link IAssetSpecification}with the given name, or null if no such specification 280 * exists. 281 * 282 * @see #addAsset(String,IAssetSpecification) 283 */ 284 285 public IAssetSpecification getAsset(String name) 286 { 287 return (IAssetSpecification) get(_assets, name); 288 } 289 290 /** 291 * Returns a <code>List</code> of the String names of all assets, in alphabetical order. 292 */ 293 294 public List getAssetNames() 295 { 296 return sortedKeys(_assets); 297 } 298 299 /** 300 * Returns the specification of a contained component with the given id, or null if no such 301 * contained component exists. 302 * 303 * @see #addComponent(String, IContainedComponent) 304 */ 305 306 public IContainedComponent getComponent(String id) 307 { 308 return (IContainedComponent) get(_components, id); 309 } 310 311 312 public String getComponentClassName() 313 { 314 return _componentClassName; 315 } 316 317 /** 318 * Returns an <code>List</code> of the String names of the {@link IContainedComponent}s for 319 * this component. 320 * 321 * @see #addComponent(String, IContainedComponent) 322 */ 323 324 public List getComponentIds() 325 { 326 return sortedKeys(_components); 327 } 328 329 /** 330 * Returns the specification of a parameter with the given name, or null if no such parameter 331 * exists. 332 * 333 * @see #addParameterByName(String, IParameterSpecification) 334 */ 335 336 public IParameterSpecification getParameter(String name) 337 { 338 return (IParameterSpecification) get(_parameters, name); 339 } 340 341 public Collection getRequiredParameters() 342 { 343 if (_parameters == null) 344 return Collections.EMPTY_LIST; 345 346 Collection result = new ArrayList(); 347 348 Iterator i = _parameters.entrySet().iterator(); 349 while (i.hasNext()) 350 { 351 Map.Entry entry = (Map.Entry) i.next(); 352 String name = (String) entry.getKey(); 353 IParameterSpecification spec = (IParameterSpecification) entry.getValue(); 354 355 if (!spec.isRequired()) 356 continue; 357 358 if (!name.equals(spec.getParameterName())) 359 continue; 360 361 result.add(spec); 362 } 363 364 return result; 365 } 366 367 /** 368 * Returns a List of of String names of all parameters. This list is in alphabetical order. 369 * 370 * @see #addParameterByName(String, IParameterSpecification) 371 */ 372 373 public List getParameterNames() 374 { 375 return sortedKeys(_parameters); 376 } 377 378 public void setAllowBody(boolean value) 379 { 380 _allowBody = value; 381 } 382 383 public void setAllowInformalParameters(boolean value) 384 { 385 _allowInformalParameters = value; 386 } 387 388 public void setComponentClassName(String value) 389 { 390 _componentClassName = value; 391 } 392 393 /** 394 * @since 1.0.4 395 * @throws ApplicationRuntimeException 396 * if the bean already has a specification. 397 */ 398 399 public void addBeanSpecification(String name, IBeanSpecification specification) 400 { 401 if (_beans == null) 402 _beans = new HashMap(); 403 404 IBeanSpecification existing = (IBeanSpecification) _beans.get(name); 405 406 if (existing != null) 407 throw new ApplicationRuntimeException(SpecMessages.duplicateBean(name, existing), 408 specification.getLocation(), null); 409 410 claimProperty(specification.getPropertyName(), specification); 411 412 _beans.put(name, specification); 413 } 414 415 /** 416 * Returns the {@link IBeanSpecification}for the given name, or null if not such specification 417 * exists. 418 * 419 * @since 1.0.4 420 */ 421 422 public IBeanSpecification getBeanSpecification(String name) 423 { 424 return (IBeanSpecification) get(_beans, name); 425 } 426 427 /** 428 * Returns an unmodifiable collection of the names of all beans. 429 */ 430 431 public Collection getBeanNames() 432 { 433 if (_beans == null) 434 return Collections.EMPTY_LIST; 435 436 return Collections.unmodifiableCollection(_beans.keySet()); 437 } 438 439 /** 440 * Adds the value as a reserved name. Reserved names are not allowed as the names of informal 441 * parameters. Since the comparison is caseless, the value is converted to lowercase before 442 * being stored. 443 * 444 * @since 1.0.5 445 */ 446 447 public void addReservedParameterName(String value) 448 { 449 if (_reservedParameterNames == null) 450 _reservedParameterNames = new HashSet(); 451 452 _reservedParameterNames.add(value.toLowerCase()); 453 } 454 455 /** 456 * Returns true if the value specified is in the reserved name list. The comparison is caseless. 457 * All formal parameters are automatically in the reserved name list, as well as any additional 458 * reserved names specified in the component specification. The latter refer to HTML attributes 459 * generated directly by the component. 460 * 461 * @since 1.0.5 462 */ 463 464 public boolean isReservedParameterName(String value) 465 { 466 if (_reservedParameterNames == null) 467 return false; 468 469 return _reservedParameterNames.contains(value.toLowerCase()); 470 } 471 472 public String toString() 473 { 474 ToStringBuilder builder = new ToStringBuilder(this); 475 476 builder.append("componentClassName", _componentClassName); 477 builder.append("pageSpecification", _pageSpecification); 478 builder.append("specificationLocation", _specificationLocation); 479 builder.append("allowBody", _allowBody); 480 builder.append("allowInformalParameter", _allowInformalParameters); 481 482 return builder.toString(); 483 } 484 485 /** 486 * Returns the documentation for this component. 487 * 488 * @since 1.0.9 489 */ 490 491 public String getDescription() 492 { 493 return _description; 494 } 495 496 /** 497 * Sets the documentation for this component. 498 * 499 * @since 1.0.9 500 */ 501 502 public void setDescription(String description) 503 { 504 _description = description; 505 } 506 507 /** 508 * Returns the XML Public Id for the specification file, or null if not applicable. 509 * <p> 510 * This method exists as a convienience for the Spindle plugin. A previous method used an 511 * arbitrary version string, the public id is more useful and less ambiguous. 512 * 513 * @since 2.2 514 */ 515 516 public String getPublicId() 517 { 518 return _publicId; 519 } 520 521 /** @since 2.2 * */ 522 523 public void setPublicId(String publicId) 524 { 525 _publicId = publicId; 526 } 527 528 /** 529 * Returns true if the specification is known to be a page specification and not a component 530 * specification. Earlier versions of the framework did not distinguish between the two, but 531 * starting in 2.2, there are seperate XML entities for pages and components. Pages omit several 532 * attributes and entities related to parameters, as parameters only make sense for components. 533 * 534 * @since 2.2 535 */ 536 537 public boolean isPageSpecification() 538 { 539 return _pageSpecification; 540 } 541 542 /** @since 2.2 * */ 543 544 public void setPageSpecification(boolean pageSpecification) 545 { 546 _pageSpecification = pageSpecification; 547 } 548 549 /** @since 2.2 * */ 550 551 private List sortedKeys(Map input) 552 { 553 if (input == null) 554 return Collections.EMPTY_LIST; 555 556 List result = new ArrayList(input.keySet()); 557 558 Collections.sort(result); 559 560 return result; 561 } 562 563 /** @since 2.2 * */ 564 565 private Object get(Map map, Object key) 566 { 567 if (map == null) 568 return null; 569 570 return map.get(key); 571 } 572 573 /** @since 3.0 * */ 574 575 public Resource getSpecificationLocation() 576 { 577 return _specificationLocation; 578 } 579 580 /** @since 3.0 * */ 581 582 public void setSpecificationLocation(Resource specificationLocation) 583 { 584 _specificationLocation = specificationLocation; 585 } 586 587 /** 588 * Adds a new property specification. The name of the property must not already be defined (and 589 * must not change after being added). 590 * 591 * @since 3.0 592 */ 593 594 public void addPropertySpecification(IPropertySpecification spec) 595 { 596 if (_propertySpecifications == null) 597 _propertySpecifications = new HashMap(); 598 599 String name = spec.getName(); 600 IPropertySpecification existing = (IPropertySpecification) _propertySpecifications 601 .get(name); 602 603 if (existing != null) 604 throw new ApplicationRuntimeException(SpecMessages.duplicateProperty(name, existing), spec.getLocation(), null); 605 606 claimProperty(name, spec); 607 608 _propertySpecifications.put(name, spec); 609 } 610 611 /** 612 * Returns a sorted, immutable list of the names of all 613 * {@link org.apache.tapestry.spec.IPropertySpecification}s. 614 * 615 * @since 3.0 616 */ 617 618 public List getPropertySpecificationNames() 619 { 620 return sortedKeys(_propertySpecifications); 621 } 622 623 /** 624 * Returns the named {@link org.apache.tapestry.spec.IPropertySpecification}, or null if no 625 * such specification exist. 626 * 627 * @since 3.0 628 * @see #addPropertySpecification(IPropertySpecification) 629 */ 630 631 public IPropertySpecification getPropertySpecification(String name) 632 { 633 return (IPropertySpecification) get(_propertySpecifications, name); 634 } 635 636 public void addInjectSpecification(InjectSpecification spec) 637 { 638 if (_injectSpecifications == null) 639 _injectSpecifications = new ArrayList(); 640 641 claimProperty(spec.getProperty(), spec); 642 643 _injectSpecifications.add(spec); 644 } 645 646 public List getInjectSpecifications() 647 { 648 return safeList(_injectSpecifications); 649 } 650 651 private List safeList(List input) 652 { 653 if (input == null) 654 return Collections.EMPTY_LIST; 655 656 return Collections.unmodifiableList(input); 657 } 658 659 private void claimProperty(String propertyName, Object subSpecification) 660 { 661 if (propertyName == null) 662 return; 663 664 if (_claimedProperties == null) 665 _claimedProperties = new HashMap(); 666 667 Object existing = _claimedProperties.get(propertyName); 668 669 if (existing != null) 670 throw new ApplicationRuntimeException(SpecMessages.claimedProperty( 671 propertyName, 672 existing), HiveMind.getLocation(subSpecification), null); 673 674 _claimedProperties.put(propertyName, subSpecification); 675 } 676 677 /** @since 4.0 */ 678 public boolean isDeprecated() 679 { 680 return _deprecated; 681 } 682 683 /** @since 4.0 */ 684 public void setDeprecated(boolean deprecated) 685 { 686 _deprecated = deprecated; 687 } 688 689 public Set getReservedParameterNames() 690 { 691 if (_reservedParameterNames == null) 692 return Collections.EMPTY_SET; 693 694 return Collections.unmodifiableSet(_reservedParameterNames); 695 } 696 697 /** 698 * {@inheritDoc} 699 */ 700 public void addEventListener(String componentId, String[] events, 701 String methodName, String formId, boolean validateForm, boolean async, boolean focus, boolean autoSubmit) 702 { 703 ComponentEventProperty property = getComponentEvents(componentId); 704 if (property == null) { 705 property = new ComponentEventProperty(componentId); 706 _componentEvents.put(componentId, property); 707 } 708 709 property.addListener(events, methodName, formId, validateForm, async, focus, autoSubmit); 710 } 711 712 /** 713 * {@inheritDoc} 714 */ 715 public void addElementEventListener(String elementId, String[] events, 716 String methodName, String formId, boolean validateForm, boolean async, boolean focus) 717 { 718 ComponentEventProperty property = getElementEvents(elementId); 719 if (property == null) { 720 property = new ComponentEventProperty(elementId); 721 _elementEvents.put(elementId, property); 722 } 723 724 property.addListener(events, methodName, formId, validateForm, async, focus, true); 725 } 726 727 public void connectAutoSubmitEvents(IComponent component, IForm form) 728 { 729 Defense.notNull(form, "form"); 730 731 ComponentEventProperty property = getComponentEvents(component.getExtendedId()); 732 733 if (property == null) 734 return; 735 736 property.connectAutoSubmitEvents(form.getExtendedId()); 737 } 738 739 public void rewireComponentId(String componentId, String extendedId, String idPath) 740 { 741 ComponentEventProperty prop = getComponentEvents(componentId); 742 if (prop == null) 743 return; 744 745 if (_componentEvents.containsKey(extendedId)) 746 return; 747 748 try { 749 750 ComponentEventProperty clone = (ComponentEventProperty) prop.clone(); 751 752 clone.rewireComponentId(extendedId, idPath); 753 754 _componentEvents.put(extendedId, clone); 755 756 } catch (CloneNotSupportedException e) { 757 758 throw new ApplicationRuntimeException(e); 759 } 760 } 761 762 /** 763 * {@inheritDoc} 764 */ 765 public ComponentEventProperty getComponentEvents(String id) 766 { 767 return (ComponentEventProperty)_componentEvents.get(id); 768 } 769 770 public Map getComponentEvents() 771 { 772 return _componentEvents; 773 } 774 775 /** 776 * {@inheritDoc} 777 */ 778 public ComponentEventProperty getElementEvents(String id) 779 { 780 return (ComponentEventProperty)_elementEvents.get(id); 781 } 782 783 /** 784 * {@inheritDoc} 785 */ 786 public Map getElementEvents() 787 { 788 return _elementEvents; 789 } 790 791 /** 792 * {@inheritDoc} 793 */ 794 public EventBoundListener[] getFormEvents(String formId, BrowserEvent event) 795 { 796 List ret = new ArrayList(); 797 798 Iterator it = _componentEvents.keySet().iterator(); 799 while (it.hasNext()) { 800 801 String compId = (String)it.next(); 802 ComponentEventProperty prop = (ComponentEventProperty)_componentEvents.get(compId); 803 804 ret.addAll(prop.getFormEventListeners(formId, event, null)); 805 } 806 807 it = _elementEvents.keySet().iterator(); 808 while (it.hasNext()) { 809 810 String compId = (String)it.next(); 811 ComponentEventProperty prop = (ComponentEventProperty)_elementEvents.get(compId); 812 813 ret.addAll(prop.getFormEventListeners(formId, event, null)); 814 } 815 816 return (EventBoundListener[])ret.toArray(new EventBoundListener[ret.size()]); 817 } 818 819 /** 820 * {@inheritDoc} 821 */ 822 public boolean hasElementEvents() 823 { 824 return _elementEvents.size() > 0; 825 } 826 827 /** 828 * {@inheritDoc} 829 */ 830 public int hashCode() 831 { 832 final int prime = 31; 833 int result = 1; 834 result = prime * result + ((_componentClassName == null) ? 0 : _componentClassName.hashCode()); 835 result = prime * result + ((_description == null) ? 0 : _description.hashCode()); 836 result = prime * result + (_pageSpecification ? 1231 : 1237); 837 result = prime * result + ((_publicId == null) ? 0 : _publicId.hashCode()); 838 return result; 839 } 840 841 /** 842 * {@inheritDoc} 843 */ 844 public boolean equals(Object obj) 845 { 846 if (this == obj) return true; 847 if (obj == null) return false; 848 if (getClass() != obj.getClass()) return false; 849 final ComponentSpecification other = (ComponentSpecification) obj; 850 if (_componentClassName == null) { 851 if (other._componentClassName != null) return false; 852 } else if (!_componentClassName.equals(other._componentClassName)) return false; 853 if (_description == null) { 854 if (other._description != null) return false; 855 } else if (!_description.equals(other._description)) return false; 856 if (_pageSpecification != other._pageSpecification) return false; 857 if (_publicId == null) { 858 if (other._publicId != null) return false; 859 } else if (!_publicId.equals(other._publicId)) return false; 860 if (_specificationLocation == null) { 861 if (other._specificationLocation != null) return false; 862 } else if (!_specificationLocation.getPath().equals(other._specificationLocation.getPath())) return false; 863 return true; 864 } 865 866 867 }