blob: e9534b41bcdd18d6be3cf25517a67611fdd7f873 [file] [log] [blame]
/*
* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache" and "Apache Software Foundation" and
* "Apache Tapestry" must not be used to endorse or promote products
* derived from this software without prior written permission. For
* written permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* "Apache Tapestry", nor may "Apache" appear in their name, without
* prior written permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
package net.sf.tapestry;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import ognl.OgnlRuntime;
import net.sf.tapestry.bean.BeanProvider;
import net.sf.tapestry.bean.BeanProviderPropertyAccessor;
import net.sf.tapestry.event.ChangeObserver;
import net.sf.tapestry.event.ObservedChangeEvent;
import net.sf.tapestry.listener.ListenerMap;
import net.sf.tapestry.param.ParameterManager;
import net.sf.tapestry.spec.ComponentSpecification;
import net.sf.tapestry.spec.ContainedComponent;
import net.sf.tapestry.util.prop.IPublicBean;
import net.sf.tapestry.util.prop.OgnlUtils;
import net.sf.tapestry.util.prop.PropertyFinder;
import net.sf.tapestry.util.prop.PropertyInfo;
import net.sf.tapestry.util.prop.PublicBeanPropertyAccessor;
/**
* Abstract base class implementing the {@link IComponent} interface.
*
* @author Howard Lewis Ship
* @version $Id$
*
**/
public abstract class AbstractComponent implements IComponent
{
static {
// Register the BeanProviderHelper to provide access to the
// beans of a bean provider as named properties.
OgnlRuntime.setPropertyAccessor(IBeanProvider.class, new BeanProviderPropertyAccessor());
// Same with IPublicBean.
OgnlRuntime.setPropertyAccessor(IPublicBean.class, new PublicBeanPropertyAccessor());
}
/**
* The specification used to originally build the component.
*
*
**/
private ComponentSpecification _specification;
/**
* 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.
*
* @deprecated To be made private in 2.3.
*
**/
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;
/**
* Manages setting and clearing parameter properties for the component.
*
* @since 2.0.3
*
**/
private ParameterManager _parameterManager;
/**
* Provides access to localized Strings for this component.
*
* @since 2.0.4
*
**/
private IComponentStrings _strings;
public void addAsset(String name, IAsset asset)
{
if (_assets == null)
_assets = new HashMap(MAP_SIZE);
_assets.put(name, asset);
}
public void addComponent(IComponent component)
{
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)
{
// 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, ComponentSpecification specification)
throws PageLoaderException
{
finishLoad();
}
protected void fireObservedChange(String propertyName, int newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, Object newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, boolean newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, double newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, float newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, long newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, char newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, byte newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
protected void fireObservedChange(String propertyName, short newValue)
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this, propertyName, newValue);
observer.observeChange(event);
}
/**
* Fires a change event for no single property; the receiver should
* note that the page containing the component is 'dirty' even if
* no property appears to have changed. This is useful in situations
* when a property is a mutable object (such as a Collection) and the
* state of the property value is changing, even though the property
* is not.
*
**/
protected void fireObservedChange()
{
ChangeObserver observer;
ObservedChangeEvent event;
observer = getChangeObserver();
if (observer == null)
return;
event = new ObservedChangeEvent(this);
observer.observeChange(event);
}
/**
* 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 when the name matches a formal parameter (as of 1.0.5,
* informal bindings are weeded out at page load / template load time,
* if they match a formal parameter, or a specificied reserved name).
* For the most part, all the bindings here are either informal parameter,
* or formal parameter without a corresponding JavaBeans property.
*
* <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(IRequestCycle)} 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 generateAttributes(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();
// Skip over formal parameters stored in the bindings
// Map. We're just interested in informal parameters.
if (_specification.getParameter(name) != null)
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(cycle);
}
else
attribute = value.toString();
writer.attribute(name, attribute);
}
}
/**
* Returns the named binding, or null if it doesn't exist.
*
* <p>This method looks for a JavaBeans property with an
* appropriate name, of type {@link IBinding}. The property
* should be named <code><i>name</i>Binding</code>. If it exists
* and is both readable and writable, then it is accessor method
* is invoked. Components which implement such methods can
* access their own binding through their instance variables
* instead of invoking this method, a performance optimization.
*
* @see #setBinding(String,IBinding)
*
**/
public IBinding getBinding(String name)
{
String bindingPropertyName = name + "Binding";
PropertyInfo info = PropertyFinder.getPropertyInfo(getClass(), bindingPropertyName);
if (info != null && info.isReadWrite() && info.getType().equals(IBinding.class))
{
IResourceResolver resolver = getPage().getEngine().getResourceResolver();
return (IBinding) OgnlUtils.get(bindingPropertyName, resolver, this);
}
if (_bindings == null)
return null;
return (IBinding) _bindings.get(name);
}
/**
* Return's the page's change observer. In practical terms, this
* will be an {@link IPageRecorder}.
*
* @see IPage#getChangeObserver()
*
**/
public ChangeObserver getChangeObserver()
{
return _page.getChangeObserver();
}
public IComponent getComponent(String id)
{
IComponent result = null;
if (_components != null)
result = (IComponent) _components.get(id);
if (result == null)
throw new NoSuchComponentException(id, this);
return result;
}
public IComponent getContainer()
{
return _container;
}
public void setContainer(IComponent value)
{
if (_container != null)
throw new ApplicationRuntimeException(Tapestry.getString("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 simply return their name.
*
* @see #getIdPath()
*
**/
public String getExtendedId()
{
if (_page == null)
return null;
return _page.getName() + "/" + getIdPath();
}
public String getId()
{
return _id;
}
public void setId(String value)
{
if (_id != null)
throw new ApplicationRuntimeException(
Tapestry.getString("AbstractComponent.attempt-to-change-component-id"));
_id = value;
}
public String getIdPath()
{
String containerIdPath;
if (_container == null)
throw new NullPointerException(Tapestry.getString("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.getString("AbstractComponent.attempt-to-change-page"));
_page = value;
}
public ComponentSpecification getSpecification()
{
return _specification;
}
public void setSpecification(ComponentSpecification value)
{
if (_specification != null)
throw new ApplicationRuntimeException(Tapestry.getString("AbstractComponent.attempt-to-change-spec"));
_specification = value;
}
/**
* Renders all elements wrapped by the receiver.
*
**/
public void renderBody(IMarkupWriter writer, IRequestCycle cycle) throws RequestCycleException
{
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>This method checks to see if a matching JavaBeans property
* (with a name of <code><i>name</i>Binding</code> and a type of
* {@link IBinding}) exists. If so, that property is updated.
* An optimized component can simply implement accessor and
* mutator methods and then access its bindings via its own
* instance variables, rather than going through {@link
* #getBinding(String)}.
*
* <p>Informal parameters should <em>not</em> be stored in
* instance variables if @link
* #generateAttribute(IMarkupWriter, String[]) is to be used.
* It relies on using the collection of bindings (to store informal parameters).
**/
public void setBinding(String name, IBinding binding)
{
String bindingPropertyName = name + "Binding";
PropertyInfo info = PropertyFinder.getPropertyInfo(getClass(), bindingPropertyName);
if (info != null && info.isReadWrite() && info.getType().equals(IBinding.class))
{
IResourceResolver resolver = getPage().getEngine().getResourceResolver();
OgnlUtils.set(bindingPropertyName, resolver, this, binding);
return;
}
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;
ContainedComponent contained = _container.getSpecification().getComponent(_id);
// If no informal parameters, then it's safe to return
// just the names of the formal parameters.
if (_bindings == null || _bindings.size() == 0)
return contained.getBindingNames();
// The new HTML parser means that sometimes, the informal attributes
// come from the HTML template and aren't known in the contained component
// specification. The only thing to do is to build up a set of
// informal bindings. A degenerate case: an informal binding that has
// setXXXBinding and getXXXBinding methods --- that makes the
// informal parameter invisible to this method (and thus, to the Inspector).
HashSet result = new HashSet(contained.getBindingNames());
// All the informal bindings go into the bindings Map. Also
// formal parameters where there isn't a corresponding JavaBeans property.
result.addAll(_bindings.keySet());
return result;
}
/**
*
* Returns a {@link Map} of all bindings for this component. This implementation
* is expensive, since it has to merge the disassociated bindings (informal parameters,
* and parameters without a JavaBeans property) with the associated bindings (formal
* parameters with a JavaBeans property).
*
* @since 1.0.5
*
**/
public Map getBindings()
{
Map result = new HashMap();
// Add any informal parameters, as well as any formal parameters
// that don't have a correspoinding JavaBeans property.
if (_bindings != null)
result.putAll(_bindings);
// Now work on the formal parameters
Iterator i = _specification.getParameterNames().iterator();
while (i.hasNext())
{
String name = (String) i.next();
if (result.containsKey(name))
continue;
IBinding binding = getBinding(name);
if (binding != null)
result.put(name, binding);
}
return result;
}
/**
* Returns a {@link ListenerMap} for the component. A {@link 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()
{
if (_listeners == null)
_listeners = new ListenerMap(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, ComponentSpecification)}.
* 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) throws RequestCycleException
{
try
{
prepareForRender(cycle);
renderComponent(writer, cycle);
}
finally
{
cleanupAfterRender(cycle);
}
}
/**
* Invoked by {@link #render(IMarkupWriter, IRequestCycle)}
* to prepare the component to render. This implementation
* sets JavaBeans properties from matching bound parameters.
* Subclasses that override this method must invoke this
* implementation as well.
*
* @since 2.0.3
*
**/
protected void prepareForRender(IRequestCycle cycle) throws RequestCycleException
{
if (_parameterManager == null)
_parameterManager = new ParameterManager(this);
_parameterManager.setParameters(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) throws RequestCycleException;
/**
* Invoked by {@link #render(IMarkupWriter, IRequestCycle)}
* after the component renders, to clear any parameters back to
* null (or 0, or false, or whatever the correct default is).
* Primarily, this is used to ensure
* that the component doesn't hold onto any objects that could
* otherwise be garbage collected.
*
* <p>Subclasses may override this implementation, but must
* also invoke it.
*
* @since 2.0.3
*
**/
protected void cleanupAfterRender(IRequestCycle cycle)
{
_parameterManager.resetParameters(cycle);
}
/**
* Obtains the {@link IComponentStrings} for this component
* (if necessary), and gets the string from it.
*
**/
public String getString(String key)
{
if (_strings == null)
_strings = getPage().getEngine().getComponentStringsSource().getStrings(this);
return _strings.getString(key);
}
/**
* Formats a string, using
* {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
*
* @param key the key used to obtain a localized pattern using
* {@link #getString(String)}
* @param arguments passed to the formatter
*
* @since 2.2
*
**/
public String formatString(String key, Object[] arguments)
{
String format = getString(key);
return MessageFormat.format(format, arguments);
}
/**
* Convienience method for {@link #formatString(String, Object[])}
*
* @since 2.2
*
**/
public String formatString(String key, Object argument)
{
return formatString(key, new Object[] { argument });
}
/**
* Convienience method for {@link #formatString(String, Object[])}
*
* @since 2.2
*
**/
public String formatString(String key, Object argument1, Object argument2)
{
return formatString(key, new Object[] { argument1, argument2 });
}
/**
* Convienience method for {@link #formatString(String, Object[])}
*
* @since 2.2
*
**/
public String formatString(String key, Object argument1, Object argument2, Object argument3)
{
return formatString(key, new Object[] { argument1, argument2, argument3 });
}
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;
}
}