blob: 7f6e2862f62aa787f1dd2de197d0c4c9a8d9f759 [file] [log] [blame]
// 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 org.apache.hivemind.ApplicationRuntimeException;
import org.apache.hivemind.Messages;
import org.apache.hivemind.impl.BaseLocatable;
import org.apache.hivemind.util.Defense;
import org.apache.tapestry.bean.BeanProvider;
import org.apache.tapestry.engine.IPageLoader;
import org.apache.tapestry.event.BrowserEvent;
import org.apache.tapestry.event.PageEvent;
import org.apache.tapestry.internal.Component;
import org.apache.tapestry.internal.event.IComponentEventInvoker;
import org.apache.tapestry.listener.ListenerMap;
import org.apache.tapestry.services.ComponentRenderWorker;
import org.apache.tapestry.spec.IComponentSpecification;
import org.apache.tapestry.spec.IContainedComponent;
import java.util.*;
/**
* Abstract base class implementing the {@link IComponent}interface.
*
* @author Howard Lewis Ship
*/
public abstract class AbstractComponent extends BaseLocatable implements IDirectEvent, Component {
private static final int MAP_SIZE = 5;
private static final int BODY_INIT_SIZE = 5;
/**
* 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 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;
/**
* The html tag name that was used to reference the component.
*/
private String _templateTagName;
/**
* 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 INamespace _namespace;
/**
* The number of {@link IRender}objects in the body of this component.
*/
protected int _bodyCount = 0;
/**
* An aray of elements in the body of this component.
*/
protected 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;
private boolean _hasEvents;
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");
// 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;
}
public IRender[] getContainedRenderers()
{
return _body;
}
public IRender[] getInnerRenderers()
{
return null;
}
public boolean hasEvents()
{
return _hasEvents;
}
public void setHasEvents(boolean hasEvents)
{
_hasEvents = hasEvents;
}
/**
* 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);
}
}
/**
* Renders the (unique) id attribute for this component.
*
* @param writer
* The writer to render attribute in.
* @param cycle
* The current request.
*/
protected void renderIdAttribute(IMarkupWriter writer, IRequestCycle cycle)
{
String id = getClientId();
if (id != null)
writer.attribute("id", id);
}
/** @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();
}
/** @since 4.1 */
public String getSpecifiedId()
{
String id = getBoundId();
if (id != null)
return id;
return getId();
}
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()
{
if (_idPath != null)
return _idPath;
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;
}
/**
* {@inheritDoc}
* @since 4.1
*/
public abstract String getClientId();
public abstract void setClientId(String id);
/**
* {@inheritDoc}
*/
public String peekClientId()
{
if (getPage() == null)
return null;
String id = getSpecifiedId();
if (id == null)
return null;
return getPage().getRequestCycle().peekUniqueId(TapestryUtils.convertTapestryIdToNMToken(id));
}
protected void generateClientId()
{
String id = getSpecifiedId();
if (id != null && getPage() != null && getPage().getRequestCycle() != null)
setClientId(getPage().getRequestCycle().getUniqueId(TapestryUtils.convertTapestryIdToNMToken(id)));
}
protected String getBoundId()
{
if (_bindings == null)
return null;
IBinding id = (IBinding)_bindings.get("id");
if (id == null || id.getObject() == null)
return null;
return id.getObject().toString();
}
public String getTemplateTagName()
{
return _templateTagName;
}
/**
* {@inheritDoc}
*/
public void setTemplateTagName(String tag)
{
if (_templateTagName != null)
throw new ApplicationRuntimeException(Tapestry.getMessage("AbstractComponent.attempt-to-change-template-tag"));
_templateTagName = tag;
}
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++)
cycle.getResponseBuilder().render(writer, _body[i], 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 _components;
}
public Map getAssets()
{
if (_assets == null)
return EMPTY_MAP;
return _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 EMPTY_MAP;
return _bindings;
}
/**
* Returns a {@link ListenerMap}&nbsp;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;
cycle.renderStackPush(this);
generateClientId();
prepareForRender(cycle);
renderComponent(writer, cycle);
}
finally
{
_rendering = false;
cleanupAfterRender(cycle);
cycle.renderStackPop();
}
}
/**
* Invoked by {@link #render(IMarkupWriter, IRequestCycle)}to prepare the component to render.
* This implementation sets JavaBeans properties from matching bound parameters. The default
* implementation of this method is empty.
*
* @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.
*
* @since 2.0.3
*/
protected void cleanupAfterRender(IRequestCycle cycle)
{
getRenderWorker().renderComponent(cycle, this);
}
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)
{
}
/**
* @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"));
}
/** @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;
}
/**
* {@inheritDoc}
*/
public IComponentEventInvoker getEventInvoker()
{
throw new IllegalStateException(TapestryMessages.providedByEnhancement("getEventInvoker"));
}
/**
* {@inheritDoc}
*/
public void triggerEvent(IRequestCycle cycle, BrowserEvent event)
{
getEventInvoker().invokeListeners(this, cycle, event);
}
public ComponentRenderWorker getRenderWorker()
{
throw new IllegalStateException(TapestryMessages.providedByEnhancement("getRenderWorker"));
}
/**
* {@inheritDoc}
*/
public boolean isStateful()
{
return false;
}
/**
* {@inheritDoc}
*/
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((getClientId() == null) ? 0 : getClientId().hashCode());
result = prime * result + ((_id == null) ? 0 : _id.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 AbstractComponent other = (AbstractComponent) obj;
if (getClientId() == null) {
if (other.getClientId() != null) return false;
} else if (!getClientId().equals(other.getClientId())) return false;
if (_id == null) {
if (other._id != null) return false;
} else if (!_id.equals(other._id)) return false;
return true;
}
}