| // 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; |
| |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.hivemind.ApplicationRuntimeException; |
| import org.apache.hivemind.Messages; |
| import org.apache.hivemind.impl.BaseLocatable; |
| import org.apache.hivemind.util.Defense; |
| import org.apache.hivemind.util.PropertyUtils; |
| import org.apache.tapestry.bean.BeanProvider; |
| import org.apache.tapestry.engine.IPageLoader; |
| import org.apache.tapestry.event.PageEvent; |
| import org.apache.tapestry.listener.ListenerMap; |
| import org.apache.tapestry.spec.IComponentSpecification; |
| import org.apache.tapestry.spec.IContainedComponent; |
| |
| /** |
| * Abstract base class implementing the {@link IComponent}interface. |
| * |
| * @author Howard Lewis Ship |
| */ |
| |
| public abstract class AbstractComponent extends BaseLocatable implements IComponent |
| { |
| /** |
| * The page that contains the component, possibly itself (if the component is in fact, a page). |
| */ |
| |
| private IPage _page; |
| |
| /** |
| * The component which contains the component. This will only be null if the component is |
| * actually a page. |
| */ |
| |
| private IComponent _container; |
| |
| /** |
| * The simple id of this component. |
| */ |
| |
| private String _id; |
| |
| /** |
| * The fully qualified id of this component. This is calculated the first time it is needed, |
| * then cached for later. |
| */ |
| |
| private String _idPath; |
| |
| private static final int MAP_SIZE = 5; |
| |
| /** |
| * A {@link Map}of all bindings (for which there isn't a corresponding JavaBeans property); the |
| * keys are the names of formal and informal parameters. |
| */ |
| |
| private Map _bindings; |
| |
| private Map _components; |
| |
| private static final int BODY_INIT_SIZE = 5; |
| |
| private INamespace _namespace; |
| |
| /** |
| * Used in place of JDK 1.3's Collections.EMPTY_MAP (which is not available in JDK 1.2). |
| */ |
| |
| private static final Map EMPTY_MAP = Collections.unmodifiableMap(new HashMap(1)); |
| |
| /** |
| * The number of {@link IRender}objects in the body of this component. |
| */ |
| |
| private int _bodyCount = 0; |
| |
| /** |
| * An aray of elements in the body of this component. |
| */ |
| |
| private IRender[] _body; |
| |
| /** |
| * The components' asset map. |
| */ |
| |
| private Map _assets; |
| |
| /** |
| * A mapping that allows public instance methods to be dressed up as {@link IActionListener} |
| * listener objects. |
| * |
| * @since 1.0.2 |
| */ |
| |
| private ListenerMap _listeners; |
| |
| /** |
| * A bean provider; these are lazily created as needed. |
| * |
| * @since 1.0.4 |
| */ |
| |
| private IBeanProvider _beans; |
| |
| /** |
| * Returns true if the component is currently rendering. |
| * |
| * @see #prepareForRender(IRequestCycle) |
| * @see #cleanupAfterRender(IRequestCycle) |
| * @since 4.0 |
| */ |
| |
| private boolean _rendering; |
| |
| /** |
| * @since 4.0 |
| */ |
| |
| private boolean _active; |
| |
| /** @since 4.0 */ |
| |
| private IContainedComponent _containedComponent; |
| |
| public void addAsset(String name, IAsset asset) |
| { |
| Defense.notNull(name, "name"); |
| Defense.notNull(asset, "asset"); |
| |
| checkActiveLock(); |
| |
| if (_assets == null) |
| _assets = new HashMap(MAP_SIZE); |
| |
| _assets.put(name, asset); |
| } |
| |
| public void addComponent(IComponent component) |
| { |
| Defense.notNull(component, "component"); |
| |
| checkActiveLock(); |
| |
| if (_components == null) |
| _components = new HashMap(MAP_SIZE); |
| |
| _components.put(component.getId(), component); |
| } |
| |
| /** |
| * Adds an element (which may be static text or a component) as a body element of this |
| * component. Such elements are rendered by {@link #renderBody(IMarkupWriter, IRequestCycle)}. |
| * |
| * @since 2.2 |
| */ |
| |
| public void addBody(IRender element) |
| { |
| Defense.notNull(element, "element"); |
| |
| // TODO: Tweak the ordering of operations inside the PageLoader so that this |
| // check is allowable. Currently, the component is entering active state |
| // before it loads its template. |
| |
| // checkActiveLock(); |
| |
| // Should check the specification to see if this component |
| // allows body. Curently, this is checked by the component |
| // in render(), which is silly. |
| |
| if (_body == null) |
| { |
| _body = new IRender[BODY_INIT_SIZE]; |
| _body[0] = element; |
| |
| _bodyCount = 1; |
| return; |
| } |
| |
| // No more room? Make the array bigger. |
| |
| if (_bodyCount == _body.length) |
| { |
| IRender[] newWrapped; |
| |
| newWrapped = new IRender[_body.length * 2]; |
| |
| System.arraycopy(_body, 0, newWrapped, 0, _bodyCount); |
| |
| _body = newWrapped; |
| } |
| |
| _body[_bodyCount++] = element; |
| } |
| |
| /** |
| * Invokes {@link #finishLoad()}. Subclasses may overide as needed, but must invoke this |
| * implementation. {@link BaseComponent} loads its HTML template. |
| */ |
| |
| public void finishLoad(IRequestCycle cycle, IPageLoader loader, |
| IComponentSpecification specification) |
| { |
| finishLoad(); |
| } |
| |
| /** |
| * Converts informal parameters into additional attributes on the curently open tag. |
| * <p> |
| * Invoked from subclasses to allow additional attributes to be specified within a tag (this |
| * works best when there is a one-to-one corespondence between an {@link IComponent}and a HTML |
| * element. |
| * <p> |
| * Iterates through the bindings for this component. Filters out bindings for formal parameters. |
| * <p> |
| * For each acceptible key, the value is extracted using {@link IBinding#getObject()}. If the |
| * value is null, no attribute is written. |
| * <p> |
| * If the value is an instance of {@link IAsset}, then {@link IAsset#buildURL()} |
| * is invoked to convert the asset to a URL. |
| * <p> |
| * Finally, {@link IMarkupWriter#attribute(String,String)}is invoked with the value (or the |
| * URL). |
| * <p> |
| * The most common use for informal parameters is to support the HTML class attribute (for use |
| * with cascading style sheets) and to specify JavaScript event handlers. |
| * <p> |
| * Components are only required to generate attributes on the result phase; this can be skipped |
| * during the rewind phase. |
| */ |
| |
| protected void renderInformalParameters(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| String attribute; |
| |
| if (_bindings == null) |
| return; |
| |
| Iterator i = _bindings.entrySet().iterator(); |
| |
| while (i.hasNext()) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| String name = (String) entry.getKey(); |
| |
| if (isFormalParameter(name)) |
| continue; |
| |
| IBinding binding = (IBinding) entry.getValue(); |
| |
| Object value = binding.getObject(); |
| if (value == null) |
| continue; |
| |
| if (value instanceof IAsset) |
| { |
| IAsset asset = (IAsset) value; |
| |
| // Get the URL of the asset and insert that. |
| |
| attribute = asset.buildURL(); |
| } |
| else |
| attribute = value.toString(); |
| |
| writer.attribute(name, attribute); |
| } |
| |
| } |
| |
| /** @since 4.0 */ |
| private boolean isFormalParameter(String name) |
| { |
| Defense.notNull(name, "name"); |
| |
| return getSpecification().getParameter(name) != null; |
| } |
| |
| /** |
| * Returns the named binding, or null if it doesn't exist. |
| * <p> |
| * In Tapestry 3.0, it was possible to force a binding to be stored in a component property by |
| * defining a concrete or abstract property named "nameBinding" of type {@link IBinding}. This |
| * has been removed in release 4.0 and bindings are always stored inside a Map of the component. |
| * |
| * @see #setBinding(String,IBinding) |
| */ |
| |
| public IBinding getBinding(String name) |
| { |
| Defense.notNull(name, "name"); |
| |
| if (_bindings == null) |
| return null; |
| |
| return (IBinding) _bindings.get(name); |
| } |
| |
| /** |
| * Returns true if the specified parameter is bound. |
| * |
| * @since 4.0 |
| */ |
| |
| public boolean isParameterBound(String parameterName) |
| { |
| Defense.notNull(parameterName, "parameterName"); |
| |
| return _bindings != null && _bindings.containsKey(parameterName); |
| } |
| |
| public IComponent getComponent(String id) |
| { |
| Defense.notNull(id, "id"); |
| |
| IComponent result = null; |
| |
| if (_components != null) |
| result = (IComponent) _components.get(id); |
| |
| if (result == null) |
| throw new ApplicationRuntimeException(Tapestry.format("no-such-component", this, id), |
| this, null, null); |
| |
| return result; |
| } |
| |
| public IComponent getContainer() |
| { |
| return _container; |
| } |
| |
| public void setContainer(IComponent value) |
| { |
| checkActiveLock(); |
| |
| if (_container != null) |
| throw new ApplicationRuntimeException(Tapestry |
| .getMessage("AbstractComponent.attempt-to-change-container")); |
| |
| _container = value; |
| } |
| |
| /** |
| * Returns the name of the page, a slash, and this component's id path. Pages are different, |
| * they override this method to simply return their page name. |
| * |
| * @see #getIdPath() |
| */ |
| |
| public String getExtendedId() |
| { |
| if (_page == null) |
| return null; |
| |
| return _page.getPageName() + "/" + getIdPath(); |
| } |
| |
| public String getId() |
| { |
| return _id; |
| } |
| |
| public void setId(String value) |
| { |
| if (_id != null) |
| throw new ApplicationRuntimeException(Tapestry |
| .getMessage("AbstractComponent.attempt-to-change-component-id")); |
| |
| _id = value; |
| } |
| |
| public String getIdPath() |
| { |
| String containerIdPath; |
| |
| if (_container == null) |
| throw new NullPointerException(Tapestry |
| .format("AbstractComponent.null-container", this)); |
| |
| containerIdPath = _container.getIdPath(); |
| |
| if (containerIdPath == null) |
| _idPath = _id; |
| else |
| _idPath = containerIdPath + "." + _id; |
| |
| return _idPath; |
| } |
| |
| public IPage getPage() |
| { |
| return _page; |
| } |
| |
| public void setPage(IPage value) |
| { |
| if (_page != null) |
| throw new ApplicationRuntimeException(Tapestry |
| .getMessage("AbstractComponent.attempt-to-change-page")); |
| |
| _page = value; |
| } |
| |
| /** |
| * Renders all elements wrapped by the receiver. |
| */ |
| |
| public void renderBody(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| for (int i = 0; i < _bodyCount; i++) |
| _body[i].render(writer, cycle); |
| } |
| |
| /** |
| * Adds the binding with the given name, replacing any existing binding with that name. |
| * <p> |
| * |
| * @see #getBinding(String) |
| */ |
| |
| public void setBinding(String name, IBinding binding) |
| { |
| Defense.notNull(name, "name"); |
| Defense.notNull(binding, "binding"); |
| |
| if (_bindings == null) |
| _bindings = new HashMap(MAP_SIZE); |
| |
| _bindings.put(name, binding); |
| } |
| |
| public String toString() |
| { |
| StringBuffer buffer; |
| |
| buffer = new StringBuffer(super.toString()); |
| |
| buffer.append('['); |
| |
| buffer.append(getExtendedId()); |
| |
| buffer.append(']'); |
| |
| return buffer.toString(); |
| } |
| |
| /** |
| * Returns an unmodifiable {@link Map}of components, keyed on component id. Never returns null, |
| * but may return an empty map. The returned map is immutable. |
| */ |
| |
| public Map getComponents() |
| { |
| if (_components == null) |
| return EMPTY_MAP; |
| |
| return Collections.unmodifiableMap(_components); |
| |
| } |
| |
| public Map getAssets() |
| { |
| if (_assets == null) |
| return EMPTY_MAP; |
| |
| return Collections.unmodifiableMap(_assets); |
| } |
| |
| public IAsset getAsset(String name) |
| { |
| if (_assets == null) |
| return null; |
| |
| return (IAsset) _assets.get(name); |
| } |
| |
| public Collection getBindingNames() |
| { |
| // If no conainer, i.e. a page, then no bindings. |
| |
| if (_container == null) |
| return null; |
| |
| HashSet result = new HashSet(); |
| |
| // All the informal bindings go into the bindings Map. |
| |
| if (_bindings != null) |
| result.addAll(_bindings.keySet()); |
| |
| // Now, iterate over the formal parameters and add the formal parameters |
| // that have a binding. |
| |
| List names = getSpecification().getParameterNames(); |
| |
| int count = names.size(); |
| |
| for (int i = 0; i < count; i++) |
| { |
| String name = (String) names.get(i); |
| |
| if (result.contains(name)) |
| continue; |
| |
| if (getBinding(name) != null) |
| result.add(name); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Returns an unmodifiable {@link Map}of all bindings for this component. |
| * |
| * @since 1.0.5 |
| */ |
| |
| public Map getBindings() |
| { |
| if (_bindings == null) |
| return Collections.EMPTY_MAP; |
| |
| return Collections.unmodifiableMap(_bindings); |
| } |
| |
| /** |
| * Returns a {@link ListenerMap} for the component. A ListenerMap contains a number of |
| * synthetic read-only properties that implement the {@link IActionListener}interface, but in |
| * fact, cause public instance methods to be invoked. |
| * |
| * @since 1.0.2 |
| */ |
| |
| public ListenerMap getListeners() |
| { |
| // This is what's called a violation of the Law of Demeter! |
| // This should probably be converted over to some kind of injection, as with |
| // getMessages(), etc. |
| |
| if (_listeners == null) |
| _listeners = getPage().getEngine().getInfrastructure().getListenerMapSource() |
| .getListenerMapForObject(this); |
| |
| return _listeners; |
| } |
| |
| /** |
| * Returns the {@link IBeanProvider}for this component. This is lazily created the first time |
| * it is needed. |
| * |
| * @since 1.0.4 |
| */ |
| |
| public IBeanProvider getBeans() |
| { |
| if (_beans == null) |
| _beans = new BeanProvider(this); |
| |
| return _beans; |
| } |
| |
| /** |
| * Invoked, as a convienience, from |
| * {@link #finishLoad(IRequestCycle, IPageLoader, IComponentSpecification)}. This implemenation |
| * does nothing. Subclasses may override without invoking this implementation. |
| * |
| * @since 1.0.5 |
| */ |
| |
| protected void finishLoad() |
| { |
| } |
| |
| /** |
| * The main method used to render the component. Invokes |
| * {@link #prepareForRender(IRequestCycle)}, then |
| * {@link #renderComponent(IMarkupWriter, IRequestCycle)}. |
| * {@link #cleanupAfterRender(IRequestCycle)}is invoked in a <code>finally</code> block. |
| * <p> |
| * Subclasses should not override this method; instead they will implement |
| * {@link #renderComponent(IMarkupWriter, IRequestCycle)}. |
| * |
| * @since 2.0.3 |
| */ |
| |
| public final void render(IMarkupWriter writer, IRequestCycle cycle) |
| { |
| try |
| { |
| _rendering = true; |
| |
| prepareForRender(cycle); |
| |
| renderComponent(writer, cycle); |
| } |
| finally |
| { |
| _rendering = false; |
| |
| cleanupAfterRender(cycle); |
| } |
| } |
| |
| /** |
| * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render. |
| * This implementation sets JavaBeans properties from matching bound parameters. This |
| * implementation does nothing. |
| * |
| * @since 2.0.3 |
| */ |
| |
| protected void prepareForRender(IRequestCycle cycle) |
| { |
| } |
| |
| /** |
| * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to actually render the component |
| * (with any parameter values already set). This is the method that subclasses must implement. |
| * |
| * @since 2.0.3 |
| */ |
| |
| protected abstract void renderComponent(IMarkupWriter writer, IRequestCycle cycle); |
| |
| /** |
| * Invoked by {@link #render(IMarkupWriter, IRequestCycle)}after the component renders. This |
| * implementation does nothing. |
| * |
| * @since 2.0.3 |
| */ |
| |
| protected void cleanupAfterRender(IRequestCycle cycle) |
| { |
| } |
| |
| public INamespace getNamespace() |
| { |
| return _namespace; |
| } |
| |
| public void setNamespace(INamespace namespace) |
| { |
| _namespace = namespace; |
| } |
| |
| /** |
| * Returns the body of the component, the element (which may be static HTML or components) that |
| * the component immediately wraps. May return null. Do not modify the returned array. The array |
| * may be padded with nulls. |
| * |
| * @since 2.3 |
| * @see #getBodyCount() |
| */ |
| |
| public IRender[] getBody() |
| { |
| return _body; |
| } |
| |
| /** |
| * Returns the active number of elements in the the body, which may be zero. |
| * |
| * @since 2.3 |
| * @see #getBody() |
| */ |
| |
| public int getBodyCount() |
| { |
| return _bodyCount; |
| } |
| |
| /** |
| * Empty implementation of |
| * {@link org.apache.tapestry.event.PageRenderListener#pageEndRender(PageEvent)}. This allows |
| * classes to implement {@link org.apache.tapestry.event.PageRenderListener}and only implement |
| * the {@link org.apache.tapestry.event.PageRenderListener#pageBeginRender(PageEvent)}method. |
| * |
| * @since 3.0 |
| */ |
| |
| public void pageEndRender(PageEvent event) |
| { |
| } |
| |
| /** |
| * Sets a property of a component. |
| * |
| * @see IComponent |
| * @since 3.0 |
| * @deprecated |
| */ |
| public void setProperty(String propertyName, Object value) |
| { |
| PropertyUtils.write(this, propertyName, value); |
| } |
| |
| /** |
| * Gets a property of a component. |
| * |
| * @see IComponent |
| * @since 3.0 |
| * @deprecated |
| */ |
| public Object getProperty(String propertyName) |
| { |
| return PropertyUtils.read(this, propertyName); |
| } |
| |
| /** |
| * @since 4.0 |
| */ |
| |
| public final boolean isRendering() |
| { |
| return _rendering; |
| } |
| |
| /** |
| * Returns true if the component has been transitioned into its active state by invoking |
| * {@link #enterActiveState()} |
| * |
| * @since 4.0 |
| */ |
| |
| protected final boolean isInActiveState() |
| { |
| return _active; |
| } |
| |
| /** @since 4.0 */ |
| public final void enterActiveState() |
| { |
| _active = true; |
| } |
| |
| /** @since 4.0 */ |
| |
| protected final void checkActiveLock() |
| { |
| if (_active) |
| throw new UnsupportedOperationException(TapestryMessages.componentIsLocked(this)); |
| } |
| |
| public Messages getMessages() |
| { |
| throw new IllegalStateException(TapestryMessages.providedByEnhancement("getMessages")); |
| } |
| |
| public IComponentSpecification getSpecification() |
| { |
| throw new IllegalStateException(TapestryMessages.providedByEnhancement("getSpecification")); |
| } |
| |
| /** |
| * Returns a localized message. |
| * |
| * @since 3.0 |
| * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. |
| */ |
| |
| public String getMessage(String key) |
| { |
| return getMessages().getMessage(key); |
| } |
| |
| /** |
| * Formats a localized message string, using |
| * {@link Messages#format(java.lang.String, java.lang.Object[])}. |
| * |
| * @param key |
| * the key used to obtain a localized pattern |
| * @param arguments |
| * passed to the formatter |
| * @since 3.0 |
| * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. |
| */ |
| |
| public String format(String key, Object[] arguments) |
| { |
| return getMessages().format(key, arguments); |
| } |
| |
| /** |
| * Convienience method for invoking {@link IMessages#format(String, Locale, Object)} |
| * |
| * @since 3.0 |
| * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. |
| */ |
| |
| public String format(String key, Object argument) |
| { |
| return getMessages().format(key, argument); |
| } |
| |
| /** |
| * Convienience method for invoking {@link Messages#format(String, Object, Object)}. |
| * |
| * @since 3.0 |
| * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. |
| */ |
| |
| public String format(String key, Object argument1, Object argument2) |
| { |
| return getMessages().format(key, argument1, argument2); |
| } |
| |
| /** |
| * Convienience method for {@link Messages#format(String, Object, Object, Object)}. |
| * |
| * @since 3.0 |
| * @deprecated To be removed in 4.1. Use {@link #getMessages()} instead. |
| */ |
| |
| public String format(String key, Object argument1, Object argument2, Object argument3) |
| { |
| return getMessages().format(key, argument1, argument2, argument3); |
| } |
| |
| /** @since 4.0 */ |
| public final IContainedComponent getContainedComponent() |
| { |
| return _containedComponent; |
| } |
| |
| /** @since 4.0 */ |
| public final void setContainedComponent(IContainedComponent containedComponent) |
| { |
| Defense.notNull(containedComponent, "containedComponent"); |
| |
| if (_containedComponent != null) |
| throw new ApplicationRuntimeException(TapestryMessages |
| .attemptToChangeContainedComponent(this)); |
| |
| _containedComponent = containedComponent; |
| } |
| |
| } |