| // Copyright 2004, 2005 The Apache Software Foundation |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry.spec; |
| |
| import org.apache.hivemind.ApplicationRuntimeException; |
| import org.apache.hivemind.HiveMind; |
| import org.apache.hivemind.Resource; |
| import org.apache.hivemind.util.Defense; |
| import org.apache.hivemind.util.ToStringBuilder; |
| import org.apache.tapestry.IComponent; |
| import org.apache.tapestry.IForm; |
| import org.apache.tapestry.event.BrowserEvent; |
| import org.apache.tapestry.internal.event.ComponentEventProperty; |
| import org.apache.tapestry.internal.event.EventBoundListener; |
| |
| import java.util.*; |
| |
| /** |
| * A specification for a component, as read from an XML specification file. |
| * <p> |
| * A specification consists of |
| * <ul> |
| * <li>An implementing class |
| * <li>An optional description |
| * <li>A set of contained components |
| * <li>Bindings for the properties of each contained component |
| * <li>A set of named assets |
| * <li>Definitions for managed beans |
| * <li>Any reserved names (used for HTML attributes) |
| * <li>Declared properties |
| * <li>Property injections |
| * </ul> |
| * <p> |
| * From this information, an actual component may be instantiated and initialized. Instantiating a |
| * component is usually a recursive process, since to initialize a container component, it is |
| * necessary to instantiate and initialize its contained components as well. |
| * |
| * @see org.apache.tapestry.IComponent |
| * @see IContainedComponent |
| * @see org.apache.tapestry.engine.IPageLoader |
| * @author Howard Lewis Ship |
| */ |
| |
| public class ComponentSpecification extends LocatablePropertyHolder implements IComponentSpecification |
| { |
| /** |
| * Keyed on component id, value is {@link IContainedComponent}. |
| */ |
| |
| protected Map _components; |
| |
| /** |
| * Keyed on asset name, value is {@link IAssetSpecification}. |
| */ |
| |
| protected Map _assets; |
| |
| /** |
| * Defines all formal parameters. Keyed on parameter name, value is |
| * {@link IParameterSpecification}. |
| */ |
| |
| protected Map _parameters; |
| |
| /** |
| * Defines all helper beans. Keyed on name, value is {@link IBeanSpecification}. |
| * |
| * @since 1.0.4 |
| */ |
| |
| protected Map _beans; |
| |
| /** |
| * The names of all reserved informal parameter names (as lower-case). This allows the page |
| * loader to filter out any informal parameters during page load, rather than during render. |
| * |
| * @since 1.0.5 |
| */ |
| |
| protected Set _reservedParameterNames; |
| |
| private String _componentClassName; |
| |
| /** @since 1.0.9 * */ |
| |
| private String _description; |
| |
| /** |
| * Is the component allowed to have a body (that is, wrap other elements?). |
| */ |
| |
| private boolean _allowBody = true; |
| |
| /** |
| * Is the component allow to have informal parameter specified. |
| */ |
| |
| private boolean _allowInformalParameters = true; |
| |
| /** |
| * The XML Public Id used when the page or component specification was read (if applicable). |
| * |
| * @since 2.2 |
| */ |
| |
| private String _publicId; |
| |
| /** |
| * Indicates that the specification is for a page, not a component. |
| * |
| * @since 2.2 |
| */ |
| |
| private boolean _pageSpecification; |
| |
| /** |
| * The location from which the specification was obtained. |
| * |
| * @since 3.0 |
| */ |
| |
| private Resource _specificationLocation; |
| |
| /** |
| * A Map of {@link IPropertySpecification}keyed on the name of the property. |
| * |
| * @since 3.0 |
| */ |
| |
| private Map _propertySpecifications; |
| |
| /** |
| * List of {@link InjectSpecification}. |
| * |
| * @since 4.0 |
| */ |
| |
| private List _injectSpecifications; |
| |
| /** |
| * Keyed on property name, value is some other object (such as an IAssetSpecification) that has |
| * claimed a property of the page. |
| * |
| * @since 4.0 |
| */ |
| |
| private Map _claimedProperties; |
| |
| /** |
| * @since 4.0 |
| */ |
| |
| private boolean _deprecated = false; |
| |
| private Map _componentEvents = new HashMap(); |
| private Map _elementEvents = new HashMap(); |
| |
| /** |
| * @throws ApplicationRuntimeException |
| * if the name already exists. |
| */ |
| |
| public void addAsset(String name, IAssetSpecification asset) |
| { |
| if (_assets == null) |
| _assets = new HashMap(); |
| |
| IAssetSpecification existing = (IAssetSpecification) _assets.get(name); |
| |
| if (existing != null) |
| throw new ApplicationRuntimeException(SpecMessages.duplicateAsset(name, existing), |
| asset.getLocation(), null); |
| |
| claimProperty(asset.getPropertyName(), asset); |
| |
| _assets.put(name, asset); |
| } |
| |
| /** |
| * @throws ApplicationRuntimeException if the id is already defined. |
| */ |
| |
| public void addComponent(String id, IContainedComponent component) |
| { |
| if (_components == null) |
| _components = new HashMap(); |
| |
| IContainedComponent existing = (IContainedComponent) _components.get(id); |
| |
| if (existing != null) |
| throw new ApplicationRuntimeException(SpecMessages.duplicateComponent(id, existing), |
| component.getLocation(), null); |
| |
| _components.put(id, component); |
| |
| claimProperty(component.getPropertyName(), component); |
| } |
| |
| /** |
| * Adds the parameter. The name is added as a reserved name. |
| * |
| * @throws ApplicationRuntimeException if the name already exists. |
| */ |
| |
| public void addParameter(IParameterSpecification spec) |
| { |
| if (_parameters == null) |
| _parameters = new HashMap(); |
| |
| String name = spec.getParameterName(); |
| |
| addParameterByName(name, spec); |
| |
| Iterator i = spec.getAliasNames().iterator(); |
| while (i.hasNext()) |
| { |
| String alias = (String) i.next(); |
| |
| addParameterByName(alias, spec); |
| } |
| |
| claimProperty(spec.getPropertyName(), spec); |
| } |
| |
| private void addParameterByName(String name, IParameterSpecification spec) |
| { |
| IParameterSpecification existing = (IParameterSpecification) _parameters.get(name); |
| |
| if (existing != null) |
| throw new ApplicationRuntimeException(SpecMessages.duplicateParameter(name, existing), |
| spec.getLocation(), null); |
| |
| _parameters.put(name, spec); |
| |
| addReservedParameterName(name); |
| } |
| |
| /** |
| * Returns true if the component is allowed to wrap other elements (static HTML or other |
| * components). The default is true. |
| * |
| * @see #setAllowBody(boolean) |
| */ |
| |
| public boolean getAllowBody() |
| { |
| return _allowBody; |
| } |
| |
| /** |
| * Returns true if the component allows informal parameters (parameters not formally defined). |
| * Informal parameters are generally used to create additional HTML attributes for an HTML tag |
| * rendered by the component. This is often used to specify JavaScript event handlers or the |
| * class of the component (for Cascarding Style Sheets). |
| * <p> |
| * The default value is true. |
| * |
| * @see #setAllowInformalParameters(boolean) |
| */ |
| |
| public boolean getAllowInformalParameters() |
| { |
| return _allowInformalParameters; |
| } |
| |
| /** |
| * Returns the {@link IAssetSpecification}with the given name, or null if no such specification |
| * exists. |
| * |
| * @see #addAsset(String,IAssetSpecification) |
| */ |
| |
| public IAssetSpecification getAsset(String name) |
| { |
| return (IAssetSpecification) get(_assets, name); |
| } |
| |
| /** |
| * Returns a <code>List</code> of the String names of all assets, in alphabetical order. |
| */ |
| |
| public List getAssetNames() |
| { |
| return sortedKeys(_assets); |
| } |
| |
| /** |
| * Returns the specification of a contained component with the given id, or null if no such |
| * contained component exists. |
| * |
| * @see #addComponent(String, IContainedComponent) |
| */ |
| |
| public IContainedComponent getComponent(String id) |
| { |
| return (IContainedComponent) get(_components, id); |
| } |
| |
| |
| public String getComponentClassName() |
| { |
| return _componentClassName; |
| } |
| |
| /** |
| * Returns an <code>List</code> of the String names of the {@link IContainedComponent}s for |
| * this component. |
| * |
| * @see #addComponent(String, IContainedComponent) |
| */ |
| |
| public List getComponentIds() |
| { |
| return sortedKeys(_components); |
| } |
| |
| /** |
| * Returns the specification of a parameter with the given name, or null if no such parameter |
| * exists. |
| * |
| * @see #addParameterByName(String, IParameterSpecification) |
| */ |
| |
| public IParameterSpecification getParameter(String name) |
| { |
| return (IParameterSpecification) get(_parameters, name); |
| } |
| |
| public Collection getRequiredParameters() |
| { |
| if (_parameters == null) |
| return Collections.EMPTY_LIST; |
| |
| Collection result = new ArrayList(); |
| |
| Iterator i = _parameters.entrySet().iterator(); |
| while (i.hasNext()) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| String name = (String) entry.getKey(); |
| IParameterSpecification spec = (IParameterSpecification) entry.getValue(); |
| |
| if (!spec.isRequired()) |
| continue; |
| |
| if (!name.equals(spec.getParameterName())) |
| continue; |
| |
| result.add(spec); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Returns a List of of String names of all parameters. This list is in alphabetical order. |
| * |
| * @see #addParameterByName(String, IParameterSpecification) |
| */ |
| |
| public List getParameterNames() |
| { |
| return sortedKeys(_parameters); |
| } |
| |
| public void setAllowBody(boolean value) |
| { |
| _allowBody = value; |
| } |
| |
| public void setAllowInformalParameters(boolean value) |
| { |
| _allowInformalParameters = value; |
| } |
| |
| public void setComponentClassName(String value) |
| { |
| _componentClassName = value; |
| } |
| |
| /** |
| * @since 1.0.4 |
| * @throws ApplicationRuntimeException |
| * if the bean already has a specification. |
| */ |
| |
| public void addBeanSpecification(String name, IBeanSpecification specification) |
| { |
| if (_beans == null) |
| _beans = new HashMap(); |
| |
| IBeanSpecification existing = (IBeanSpecification) _beans.get(name); |
| |
| if (existing != null) |
| throw new ApplicationRuntimeException(SpecMessages.duplicateBean(name, existing), |
| specification.getLocation(), null); |
| |
| claimProperty(specification.getPropertyName(), specification); |
| |
| _beans.put(name, specification); |
| } |
| |
| /** |
| * Returns the {@link IBeanSpecification}for the given name, or null if not such specification |
| * exists. |
| * |
| * @since 1.0.4 |
| */ |
| |
| public IBeanSpecification getBeanSpecification(String name) |
| { |
| return (IBeanSpecification) get(_beans, name); |
| } |
| |
| /** |
| * Returns an unmodifiable collection of the names of all beans. |
| */ |
| |
| public Collection getBeanNames() |
| { |
| if (_beans == null) |
| return Collections.EMPTY_LIST; |
| |
| return Collections.unmodifiableCollection(_beans.keySet()); |
| } |
| |
| /** |
| * Adds the value as a reserved name. Reserved names are not allowed as the names of informal |
| * parameters. Since the comparison is caseless, the value is converted to lowercase before |
| * being stored. |
| * |
| * @since 1.0.5 |
| */ |
| |
| public void addReservedParameterName(String value) |
| { |
| if (_reservedParameterNames == null) |
| _reservedParameterNames = new HashSet(); |
| |
| _reservedParameterNames.add(value.toLowerCase()); |
| } |
| |
| /** |
| * Returns true if the value specified is in the reserved name list. The comparison is caseless. |
| * All formal parameters are automatically in the reserved name list, as well as any additional |
| * reserved names specified in the component specification. The latter refer to HTML attributes |
| * generated directly by the component. |
| * |
| * @since 1.0.5 |
| */ |
| |
| public boolean isReservedParameterName(String value) |
| { |
| if (_reservedParameterNames == null) |
| return false; |
| |
| return _reservedParameterNames.contains(value.toLowerCase()); |
| } |
| |
| public String toString() |
| { |
| ToStringBuilder builder = new ToStringBuilder(this); |
| |
| builder.append("componentClassName", _componentClassName); |
| builder.append("pageSpecification", _pageSpecification); |
| builder.append("specificationLocation", _specificationLocation); |
| builder.append("allowBody", _allowBody); |
| builder.append("allowInformalParameter", _allowInformalParameters); |
| |
| return builder.toString(); |
| } |
| |
| /** |
| * Returns the documentation for this component. |
| * |
| * @since 1.0.9 |
| */ |
| |
| public String getDescription() |
| { |
| return _description; |
| } |
| |
| /** |
| * Sets the documentation for this component. |
| * |
| * @since 1.0.9 |
| */ |
| |
| public void setDescription(String description) |
| { |
| _description = description; |
| } |
| |
| /** |
| * Returns the XML Public Id for the specification file, or null if not applicable. |
| * <p> |
| * This method exists as a convienience for the Spindle plugin. A previous method used an |
| * arbitrary version string, the public id is more useful and less ambiguous. |
| * |
| * @since 2.2 |
| */ |
| |
| public String getPublicId() |
| { |
| return _publicId; |
| } |
| |
| /** @since 2.2 * */ |
| |
| public void setPublicId(String publicId) |
| { |
| _publicId = publicId; |
| } |
| |
| /** |
| * Returns true if the specification is known to be a page specification and not a component |
| * specification. Earlier versions of the framework did not distinguish between the two, but |
| * starting in 2.2, there are seperate XML entities for pages and components. Pages omit several |
| * attributes and entities related to parameters, as parameters only make sense for components. |
| * |
| * @since 2.2 |
| */ |
| |
| public boolean isPageSpecification() |
| { |
| return _pageSpecification; |
| } |
| |
| /** @since 2.2 * */ |
| |
| public void setPageSpecification(boolean pageSpecification) |
| { |
| _pageSpecification = pageSpecification; |
| } |
| |
| /** @since 2.2 * */ |
| |
| private List sortedKeys(Map input) |
| { |
| if (input == null) |
| return Collections.EMPTY_LIST; |
| |
| List result = new ArrayList(input.keySet()); |
| |
| Collections.sort(result); |
| |
| return result; |
| } |
| |
| /** @since 2.2 * */ |
| |
| private Object get(Map map, Object key) |
| { |
| if (map == null) |
| return null; |
| |
| return map.get(key); |
| } |
| |
| /** @since 3.0 * */ |
| |
| public Resource getSpecificationLocation() |
| { |
| return _specificationLocation; |
| } |
| |
| /** @since 3.0 * */ |
| |
| public void setSpecificationLocation(Resource specificationLocation) |
| { |
| _specificationLocation = specificationLocation; |
| } |
| |
| /** |
| * Adds a new property specification. The name of the property must not already be defined (and |
| * must not change after being added). |
| * |
| * @since 3.0 |
| */ |
| |
| public void addPropertySpecification(IPropertySpecification spec) |
| { |
| if (_propertySpecifications == null) |
| _propertySpecifications = new HashMap(); |
| |
| String name = spec.getName(); |
| IPropertySpecification existing = (IPropertySpecification) _propertySpecifications |
| .get(name); |
| |
| if (existing != null) |
| throw new ApplicationRuntimeException(SpecMessages.duplicateProperty(name, existing), spec.getLocation(), null); |
| |
| claimProperty(name, spec); |
| |
| _propertySpecifications.put(name, spec); |
| } |
| |
| /** |
| * Returns a sorted, immutable list of the names of all |
| * {@link org.apache.tapestry.spec.IPropertySpecification}s. |
| * |
| * @since 3.0 |
| */ |
| |
| public List getPropertySpecificationNames() |
| { |
| return sortedKeys(_propertySpecifications); |
| } |
| |
| /** |
| * Returns the named {@link org.apache.tapestry.spec.IPropertySpecification}, or null if no |
| * such specification exist. |
| * |
| * @since 3.0 |
| * @see #addPropertySpecification(IPropertySpecification) |
| */ |
| |
| public IPropertySpecification getPropertySpecification(String name) |
| { |
| return (IPropertySpecification) get(_propertySpecifications, name); |
| } |
| |
| public void addInjectSpecification(InjectSpecification spec) |
| { |
| if (_injectSpecifications == null) |
| _injectSpecifications = new ArrayList(); |
| |
| claimProperty(spec.getProperty(), spec); |
| |
| _injectSpecifications.add(spec); |
| } |
| |
| public List getInjectSpecifications() |
| { |
| return safeList(_injectSpecifications); |
| } |
| |
| private List safeList(List input) |
| { |
| if (input == null) |
| return Collections.EMPTY_LIST; |
| |
| return Collections.unmodifiableList(input); |
| } |
| |
| private void claimProperty(String propertyName, Object subSpecification) |
| { |
| if (propertyName == null) |
| return; |
| |
| if (_claimedProperties == null) |
| _claimedProperties = new HashMap(); |
| |
| Object existing = _claimedProperties.get(propertyName); |
| |
| if (existing != null) |
| throw new ApplicationRuntimeException(SpecMessages.claimedProperty( |
| propertyName, |
| existing), HiveMind.getLocation(subSpecification), null); |
| |
| _claimedProperties.put(propertyName, subSpecification); |
| } |
| |
| /** @since 4.0 */ |
| public boolean isDeprecated() |
| { |
| return _deprecated; |
| } |
| |
| /** @since 4.0 */ |
| public void setDeprecated(boolean deprecated) |
| { |
| _deprecated = deprecated; |
| } |
| |
| public Set getReservedParameterNames() |
| { |
| if (_reservedParameterNames == null) |
| return Collections.EMPTY_SET; |
| |
| return Collections.unmodifiableSet(_reservedParameterNames); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void addEventListener(String componentId, String[] events, |
| String methodName, String formId, boolean validateForm, boolean async, boolean focus, boolean autoSubmit) |
| { |
| ComponentEventProperty property = getComponentEvents(componentId); |
| if (property == null) { |
| property = new ComponentEventProperty(componentId); |
| _componentEvents.put(componentId, property); |
| } |
| |
| property.addListener(events, methodName, formId, validateForm, async, focus, autoSubmit); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void addElementEventListener(String elementId, String[] events, |
| String methodName, String formId, boolean validateForm, boolean async, boolean focus) |
| { |
| ComponentEventProperty property = getElementEvents(elementId); |
| if (property == null) { |
| property = new ComponentEventProperty(elementId); |
| _elementEvents.put(elementId, property); |
| } |
| |
| property.addListener(events, methodName, formId, validateForm, async, focus, true); |
| } |
| |
| public void connectAutoSubmitEvents(IComponent component, IForm form) |
| { |
| Defense.notNull(form, "form"); |
| |
| ComponentEventProperty property = getComponentEvents(component.getExtendedId()); |
| |
| if (property == null) |
| return; |
| |
| property.connectAutoSubmitEvents(form.getExtendedId()); |
| } |
| |
| public void rewireComponentId(String componentId, String extendedId, String idPath) |
| { |
| ComponentEventProperty prop = getComponentEvents(componentId); |
| if (prop == null) |
| return; |
| |
| if (_componentEvents.containsKey(extendedId)) |
| return; |
| |
| try { |
| |
| ComponentEventProperty clone = (ComponentEventProperty) prop.clone(); |
| |
| clone.rewireComponentId(extendedId, idPath); |
| |
| _componentEvents.put(extendedId, clone); |
| |
| } catch (CloneNotSupportedException e) { |
| |
| throw new ApplicationRuntimeException(e); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public ComponentEventProperty getComponentEvents(String id) |
| { |
| return (ComponentEventProperty)_componentEvents.get(id); |
| } |
| |
| public Map getComponentEvents() |
| { |
| return _componentEvents; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public ComponentEventProperty getElementEvents(String id) |
| { |
| return (ComponentEventProperty)_elementEvents.get(id); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public Map getElementEvents() |
| { |
| return _elementEvents; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public EventBoundListener[] getFormEvents(String formId, BrowserEvent event) |
| { |
| List ret = new ArrayList(); |
| |
| Iterator it = _componentEvents.keySet().iterator(); |
| while (it.hasNext()) { |
| |
| String compId = (String)it.next(); |
| ComponentEventProperty prop = (ComponentEventProperty)_componentEvents.get(compId); |
| |
| ret.addAll(prop.getFormEventListeners(formId, event, null)); |
| } |
| |
| it = _elementEvents.keySet().iterator(); |
| while (it.hasNext()) { |
| |
| String compId = (String)it.next(); |
| ComponentEventProperty prop = (ComponentEventProperty)_elementEvents.get(compId); |
| |
| ret.addAll(prop.getFormEventListeners(formId, event, null)); |
| } |
| |
| return (EventBoundListener[])ret.toArray(new EventBoundListener[ret.size()]); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean hasElementEvents() |
| { |
| return _elementEvents.size() > 0; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public int hashCode() |
| { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + ((_componentClassName == null) ? 0 : _componentClassName.hashCode()); |
| result = prime * result + ((_description == null) ? 0 : _description.hashCode()); |
| result = prime * result + (_pageSpecification ? 1231 : 1237); |
| result = prime * result + ((_publicId == null) ? 0 : _publicId.hashCode()); |
| return result; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public boolean equals(Object obj) |
| { |
| if (this == obj) return true; |
| if (obj == null) return false; |
| if (getClass() != obj.getClass()) return false; |
| final ComponentSpecification other = (ComponentSpecification) obj; |
| if (_componentClassName == null) { |
| if (other._componentClassName != null) return false; |
| } else if (!_componentClassName.equals(other._componentClassName)) return false; |
| if (_description == null) { |
| if (other._description != null) return false; |
| } else if (!_description.equals(other._description)) return false; |
| if (_pageSpecification != other._pageSpecification) return false; |
| if (_publicId == null) { |
| if (other._publicId != null) return false; |
| } else if (!_publicId.equals(other._publicId)) return false; |
| if (_specificationLocation == null) { |
| if (other._specificationLocation != null) return false; |
| } else if (!_specificationLocation.getPath().equals(other._specificationLocation.getPath())) return false; |
| return true; |
| } |
| |
| |
| } |