blob: a6ffbbc219a521c385c40cc6f43bfba21564132d [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.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import net.sf.tapestry.parse.ComponentTemplate;
import net.sf.tapestry.parse.TemplateToken;
import net.sf.tapestry.parse.TokenType;
import net.sf.tapestry.spec.ComponentSpecification;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Base implementation for most components that use an HTML template.
*
* @author Howard Lewis Ship
* @version $Id$
*
**/
public class BaseComponent extends AbstractComponent
{
private static final Log LOG = LogFactory.getLog(BaseComponent.class);
private int _outerCount = 0;
private static final int OUTER_INIT_SIZE = 5;
private IRender[] _outer;
/**
* A class used with invisible localizations. Constructed
* from {@link TokenType#LOCALIZATION} {@link TemplateToken}s.
*
* @since 2.0.4
*
**/
private class LocalizedStringRender implements IRender
{
private String _key;
private Map _attributes;
private boolean _raw;
private LocalizedStringRender(String key, boolean raw, Map attributes)
{
_key = key;
_raw = raw;
_attributes = attributes;
}
public void render(IMarkupWriter writer, IRequestCycle cycle) throws RequestCycleException
{
if (cycle.isRewinding())
return;
if (_attributes != null)
{
writer.begin("span");
Iterator i = _attributes.entrySet().iterator();
while (i.hasNext())
{
Map.Entry entry = (Map.Entry) i.next();
String attributeName = (String) entry.getKey();
String attributeValue = (String) entry.getValue();
writer.attribute(attributeName, attributeValue);
}
}
String value = getString(_key);
if (_raw)
writer.printRaw(value);
else
writer.print(value);
if (_attributes != null)
writer.end();
}
public String toString()
{
StringBuffer buffer = new StringBuffer("LocalizedStringRender@");
buffer.append(Integer.toHexString(hashCode()));
buffer.append('[');
buffer.append(_key);
if (_attributes != null)
{
buffer.append(' ');
buffer.append(_attributes);
}
buffer.append(']');
return buffer.toString();
}
}
/**
* Adds an element as an outer element for the receiver. Outer
* elements are elements that should be directly rendered by the
* receiver's <code>render()</code> method. That is, they are
* top-level elements on the HTML template.
*
*
**/
private void addOuter(IRender element)
{
if (_outer == null)
{
_outer = new IRender[OUTER_INIT_SIZE];
_outer[0] = element;
_outerCount = 1;
return;
}
// No more room? Make the array bigger.
if (_outerCount == _outer.length)
{
IRender[] newOuter;
newOuter = new IRender[_outer.length * 2];
System.arraycopy(_outer, 0, newOuter, 0, _outerCount);
_outer = newOuter;
}
_outer[_outerCount++] = element;
}
/**
*
* Reads the receiver's template and figures out which elements wrap which
* other elements.
*
* <P>This is coded as a single, big, ugly method for efficiency.
*
**/
private void readTemplate(IRequestCycle cycle, IPageLoader loader) throws PageLoaderException
{
Set seenIds = new HashSet();
IPageSource pageSource = loader.getEngine().getPageSource();
if (LOG.isDebugEnabled())
LOG.debug(this +" reading template");
ITemplateSource source = loader.getTemplateSource();
ComponentTemplate componentTemplate = source.getTemplate(cycle, this);
int count = componentTemplate.getTokenCount();
// The stack can never be as large as the number of tokens, so this is safe.
IComponent[] componentStack = new IComponent[count];
IComponent activeComponent = null;
int stackx = 0;
for (int i = 0; i < count; i++)
{
TemplateToken token = componentTemplate.getToken(i);
TokenType type = token.getType();
if (type == TokenType.TEXT)
{
addText(activeComponent, token);
continue;
}
if (type == TokenType.OPEN)
{
IComponent component = addStartComponent(activeComponent, token, pageSource, seenIds);
componentStack[stackx++] = activeComponent;
activeComponent = component;
continue;
}
if (type == TokenType.CLOSE)
{
try
{
activeComponent = componentStack[--stackx];
}
catch (IndexOutOfBoundsException ex)
{
// This is now almost impossible to reach, because the
// TemplateParser does a great job of checking for most of these cases.
throw new PageLoaderException(Tapestry.getString("BaseComponent.unbalanced-close-tags"), this);
}
continue;
}
if (type == TokenType.LOCALIZATION)
{
addStringLocalization(activeComponent, token);
continue;
}
}
// This is also pretty much unreachable, and the message is kind of out
// of date, too.
if (stackx != 0)
throw new PageLoaderException(Tapestry.getString("BaseComponent.unbalance-open-tags"), this);
checkAllComponentsReferenced(seenIds);
if (LOG.isDebugEnabled())
LOG.debug(this +" finished reading template");
}
/** @since 2.1 **/
private void addStringLocalization(IComponent activeComponent, TemplateToken token)
{
IRender renderer = new LocalizedStringRender(token.getId(), token.isRaw(), token.getAttributes());
if (activeComponent == null)
addOuter(renderer);
else
activeComponent.addBody(renderer);
}
/** @since 2.1 **/
private IComponent addStartComponent(
IComponent activeComponent,
TemplateToken token,
IPageSource pageSource,
Set seenIds)
throws PageLoaderException, BodylessComponentException
{
String id = token.getId();
IComponent component = null;
try
{
component = getComponent(id);
}
catch (NoSuchComponentException ex)
{
throw new PageLoaderException(
Tapestry.getString("BaseComponent.undefined-embedded-component", getExtendedId(), id),
this,
ex);
}
// Make sure the template contains each component only once.
if (seenIds.contains(id))
throw new PageLoaderException(
Tapestry.getString("BaseComponent.multiple-component-references", getExtendedId(), id),
this);
seenIds.add(id);
if (activeComponent == null)
addOuter(component);
else
{
// If you use a <jwc> tag in the template, you can get here.
// If you use a normal tag and a jwcid attribute, the
// body is automatically editted out.
if (!activeComponent.getSpecification().getAllowBody())
throw new BodylessComponentException(activeComponent);
activeComponent.addBody(component);
}
addStaticBindings(component, token.getAttributes(), pageSource);
return component;
}
/** @since 2.1 **/
private void addText(IComponent activeComponent, TemplateToken token) throws BodylessComponentException
{
// Get a render for the token. This allows the token and the render
// to be shared across sessions.
IRender element = token.getRender();
if (activeComponent == null)
addOuter(element);
else
{
// The new template parser edits text out automatically;
// this code probably can't be reached.
if (!activeComponent.getSpecification().getAllowBody())
throw new BodylessComponentException(activeComponent);
activeComponent.addBody(element);
}
}
/**
* Adds static bindings for any attrributes specified in the HTML
* template, skipping any that are reserved (explicitly, or
* because they match a formal parameter name).
*
**/
private void addStaticBindings(IComponent component, Map attributes, IPageSource pageSource)
{
if (attributes == null || attributes.isEmpty())
return;
ComponentSpecification spec = component.getSpecification();
boolean rejectInformal = !spec.getAllowInformalParameters();
Iterator i = attributes.entrySet().iterator();
while (i.hasNext())
{
Map.Entry e = (Map.Entry) i.next();
String name = (String) e.getKey();
// If matches a formal parameter name, allow it to be set
// unless there's already a binding.
boolean isFormal = (spec.getParameter(name) != null);
if (isFormal)
{
if (component.getBinding(name) != null)
continue;
}
else
{
// Skip informal parameters if the component doesn't allow them.
if (rejectInformal)
continue;
// If the name is reserved (matches a formal parameter
// or reserved name, caselessly), then skip it.
if (spec.isReservedParameterName(name))
continue;
}
String value = (String) e.getValue();
IBinding binding = pageSource.getStaticBinding(value);
component.setBinding(name, binding);
}
}
private void checkAllComponentsReferenced(Set seenIds) throws PageLoaderException
{
// First, contruct a modifiable copy of the ids of all expected components
// (that is, components declared in the specification).
Map components = getComponents();
Set ids = components.keySet();
// If the seen ids ... ids referenced in the template, matches
// all the ids in the specification then we're fine.
if (seenIds.containsAll(ids))
return;
// Create a modifiable copy. Remove the ids that are referenced in
// the template. The remainder are worthy of note.
ids = new HashSet(ids);
ids.removeAll(seenIds);
int count = ids.size();
String key =
(count == 1) ? "BaseComponent.missing-component-spec-single" : "BaseComponent.missing-component-spec-multi";
StringBuffer buffer = new StringBuffer(Tapestry.getString(key, getExtendedId()));
Iterator i = ids.iterator();
int j = 1;
while (i.hasNext())
{
if (j == 1)
buffer.append(' ');
else
if (j == count)
{
buffer.append(' ');
buffer.append(Tapestry.getString("BaseComponent.and"));
buffer.append(' ');
}
else
buffer.append(", ");
buffer.append(i.next());
j++;
}
buffer.append('.');
LOG.error(buffer.toString());
}
/**
* Renders the top level components contained by the receiver.
*
* @since 2.0.3
**/
protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) throws RequestCycleException
{
if (LOG.isDebugEnabled())
LOG.debug("Begin render " + getExtendedId());
for (int i = 0; i < _outerCount; i++)
_outer[i].render(writer, cycle);
if (LOG.isDebugEnabled())
LOG.debug("End render " + getExtendedId());
}
/**
* Loads the template for the component, and invokes
* {@link #finishLoad()}. Subclasses must invoke this method first,
* before adding any additional behavior, though its usually
* simpler to override {@link #finishLoad()} instead.
*
**/
public void finishLoad(IRequestCycle cycle, IPageLoader loader, ComponentSpecification specification)
throws PageLoaderException
{
readTemplate(cycle, loader);
finishLoad();
}
}