blob: 12628d8f5ea959f1b51b4d1bb785fbdc5645eb5f [file] [log] [blame]
// Copyright 2004 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.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.binding.ExpressionBinding;
import org.apache.tapestry.binding.StaticBinding;
import org.apache.tapestry.binding.StringBinding;
import org.apache.tapestry.engine.ExpressionEvaluator;
import org.apache.tapestry.engine.IPageLoader;
import org.apache.tapestry.engine.IPageSource;
import org.apache.tapestry.engine.ITemplateSource;
import org.apache.tapestry.parse.*;
import org.apache.tapestry.spec.IComponentSpecification;
import org.apache.tapestry.spec.IContainedComponent;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Utility class instantiated by {@link org.apache.tapestry.BaseComponent} to
* process the component's {@link org.apache.tapestry.parse.ComponentTemplate template},
* which involves working through the nested structure of the template and hooking
* the various static template blocks and components together using
* {@link IComponent#addBody(IRender)} and
* {@link org.apache.tapestry.BaseComponent#addOuter(IRender)}.
*
* @author Howard Lewis Ship
* @version $Id$
* @since 3.0
*/
public class BaseComponentTemplateLoader
{
private static final Log LOG = LogFactory.getLog(BaseComponentTemplateLoader.class);
private IPageLoader _pageLoader;
private IRequestCycle _requestCycle;
private BaseComponent _loadComponent;
private IPageSource _pageSource;
private ComponentTemplate _template;
private IComponent[] _stack;
private int _stackx = 0;
private IComponent _activeComponent = null;
private ExpressionEvaluator _evaluator;
private Set _seenIds = new HashSet();
/**
* A class used with invisible localizations. Constructed
* from a {@link TextToken}.
*/
private static class LocalizedStringRender implements IRender
{
private IComponent _component;
private String _key;
private Map _attributes;
private String _value;
private boolean _raw;
private LocalizedStringRender(IComponent component, LocalizationToken token)
{
_component = component;
_key = token.getKey();
_raw = token.isRaw();
_attributes = token.getAttributes();
}
public void render(IMarkupWriter writer, IRequestCycle cycle)
{
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);
}
}
if (_value == null)
_value = _component.getMessage(_key);
if (_raw)
writer.printRaw(_value);
else
writer.print(_value);
if (_attributes != null)
writer.end();
}
public String toString()
{
ToStringBuilder builder = new ToStringBuilder(this);
builder.append("component", _component);
builder.append("key", _key);
builder.append("raw", _raw);
builder.append("attributes", _attributes);
return builder.toString();
}
}
public BaseComponentTemplateLoader(
IRequestCycle requestCycle,
IPageLoader pageLoader,
BaseComponent loadComponent,
ComponentTemplate template,
IPageSource pageSource)
{
_requestCycle = requestCycle;
_pageLoader = pageLoader;
_loadComponent = loadComponent;
_template = template;
_pageSource = pageSource;
_stack = new IComponent[template.getTokenCount()];
}
public void process()
{
int count = _template.getTokenCount();
for (int i = 0; i < count; i++)
{
TemplateToken token = _template.getToken(i);
TokenType type = token.getType();
if (type == TokenType.TEXT)
{
process((TextToken) token);
continue;
}
if (type == TokenType.OPEN)
{
process((OpenToken) token);
continue;
}
if (type == TokenType.CLOSE)
{
process((CloseToken) token);
continue;
}
if (type == TokenType.LOCALIZATION)
{
process((LocalizationToken) token);
continue;
}
}
// This is also pretty much unreachable, and the message is kind of out
// of date, too.
if (_stackx != 0)
throw new ApplicationRuntimeException(
Tapestry.getMessage("BaseComponent.unbalance-open-tags"),
_loadComponent,
null,
null);
checkAllComponentsReferenced();
}
/**
* Adds the token (which implements {@link IRender})
* to the active component (using {@link IComponent#addBody(IRender)}),
* or to this component {@link BaseComponent#addOuter(IRender)}.
*
* <p>
* A check is made that the active component allows a body.
*/
private void process(TextToken token)
{
if (_activeComponent == null)
{
_loadComponent.addOuter(token);
return;
}
if (!_activeComponent.getSpecification().getAllowBody())
throw createBodylessComponentException(_activeComponent);
_activeComponent.addBody(token);
}
private void process(OpenToken token)
{
String id = token.getId();
IComponent component = null;
String componentType = token.getComponentType();
if (componentType == null)
component = getEmbeddedComponent(id);
else
{
checkForDuplicateId(id, token.getLocation());
component = createImplicitComponent(id, componentType, token.getLocation());
}
// Make sure the template contains each component only once.
if (_seenIds.contains(id))
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.multiple-component-references",
_loadComponent.getExtendedId(),
id),
_loadComponent,
token.getLocation(),
null);
_seenIds.add(id);
if (_activeComponent == null)
_loadComponent.addOuter(component);
else
{
// Note: this code may no longer be reachable (because the
// template parser does this check first).
if (!_activeComponent.getSpecification().getAllowBody())
throw createBodylessComponentException(_activeComponent);
_activeComponent.addBody(component);
}
addTemplateBindings(component, token);
_stack[_stackx++] = _activeComponent;
_activeComponent = component;
}
private void checkForDuplicateId(String id, ILocation location)
{
if (id == null)
return;
IContainedComponent cc = _loadComponent.getSpecification().getComponent(id);
if (cc != null)
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponentTemplateLoader.dupe-component-id",
id,
location,
cc.getLocation()),
_loadComponent,
location,
null);
}
private IComponent createImplicitComponent(String id, String componentType, ILocation location)
{
IComponent result =
_pageLoader.createImplicitComponent(
_requestCycle,
_loadComponent,
id,
componentType,
location);
return result;
}
private IComponent getEmbeddedComponent(String id)
{
return _loadComponent.getComponent(id);
}
private void process(CloseToken token)
{
// Again, this is pretty much impossible to reach because
// the template parser does a great job.
if (_stackx <= 0)
throw new ApplicationRuntimeException(
Tapestry.getMessage("BaseComponent.unbalanced-close-tags"),
_loadComponent,
token.getLocation(),
null);
// Null and forget the top element on the stack.
_stack[_stackx--] = null;
_activeComponent = _stack[_stackx];
}
private void process(LocalizationToken token)
{
IRender render = new LocalizedStringRender(_loadComponent, token);
if (_activeComponent == null)
_loadComponent.addOuter(render);
else
_activeComponent.addBody(render);
}
/**
* Adds bindings based on attributes in the template.
*/
private void addTemplateBindings(IComponent component, OpenToken token)
{
IComponentSpecification spec = component.getSpecification();
Map attributes = token.getAttributesMap();
if (attributes != null)
{
Iterator i = attributes.entrySet().iterator();
while (i.hasNext())
{
Map.Entry entry = (Map.Entry) i.next();
String name = (String) entry.getKey();
TemplateAttribute attribute = (TemplateAttribute) entry.getValue();
AttributeType type = attribute.getType();
if (type == AttributeType.OGNL_EXPRESSION)
{
addExpressionBinding(
component,
spec,
name,
attribute.getValue(),
token.getLocation());
continue;
}
if (type == AttributeType.LOCALIZATION_KEY)
{
addStringBinding(
component,
spec,
name,
attribute.getValue(),
token.getLocation());
continue;
}
if (type == AttributeType.LITERAL)
addStaticBinding(
component,
spec,
name,
attribute.getValue(),
token.getLocation());
}
}
// if the component defines a templateTag parameter and
// there is no established binding for that parameter,
// add a static binding carrying the template tag
if (spec.getParameter(ITemplateSource.TEMPLATE_TAG_PARAMETER_NAME) != null
&& component.getBinding(ITemplateSource.TEMPLATE_TAG_PARAMETER_NAME) == null)
{
addStaticBinding(
component,
spec,
ITemplateSource.TEMPLATE_TAG_PARAMETER_NAME,
token.getTag(),
token.getLocation());
}
}
/**
* Adds an expression binding, checking for errors related
* to reserved and informal parameters.
*
* <p>It is an error to specify expression
* bindings in both the specification
* and the template.
*/
private void addExpressionBinding(
IComponent component,
IComponentSpecification spec,
String name,
String expression,
ILocation location)
{
// 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)
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.dupe-template-expression",
name,
component.getExtendedId(),
_loadComponent.getExtendedId()),
component,
location,
null);
}
else
{
if (!spec.getAllowInformalParameters())
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.template-expression-for-informal-parameter",
name,
component.getExtendedId(),
_loadComponent.getExtendedId()),
component,
location,
null);
// If the name is reserved (matches a formal parameter
// or reserved name, caselessly), then skip it.
if (spec.isReservedParameterName(name))
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.template-expression-for-reserved-parameter",
name,
component.getExtendedId(),
_loadComponent.getExtendedId()),
component,
location,
null);
}
IBinding binding =
new ExpressionBinding(
_pageSource.getResourceResolver(),
_loadComponent,
expression,
location);
component.setBinding(name, binding);
}
/**
* Adds an expression binding, checking for errors related
* to reserved and informal parameters.
*
* <p>It is an error to specify expression
* bindings in both the specification
* and the template.
*/
private void addStringBinding(
IComponent component,
IComponentSpecification spec,
String name,
String localizationKey,
ILocation location)
{
// 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)
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.dupe-string",
name,
component.getExtendedId(),
_loadComponent.getExtendedId()),
component,
location,
null);
}
else
{
if (!spec.getAllowInformalParameters())
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.template-expression-for-informal-parameter",
name,
component.getExtendedId(),
_loadComponent.getExtendedId()),
component,
location,
null);
// If the name is reserved (matches a formal parameter
// or reserved name, caselessly), then skip it.
if (spec.isReservedParameterName(name))
throw new ApplicationRuntimeException(
Tapestry.format(
"BaseComponent.template-expression-for-reserved-parameter",
name,
component.getExtendedId(),
_loadComponent.getExtendedId()),
component,
location,
null);
}
IBinding binding = new StringBinding(_loadComponent, localizationKey, location);
component.setBinding(name, binding);
}
/**
* Adds a static binding, checking for errors related
* to reserved and informal parameters.
*
* <p>
* Static bindings that conflict with bindings in the
* specification are quietly ignored.
*/
private void addStaticBinding(
IComponent component,
IComponentSpecification spec,
String name,
String staticValue,
ILocation location)
{
if (component.getBinding(name) != null)
return;
// 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)
{
// Skip informal parameters if the component doesn't allow them.
if (!spec.getAllowInformalParameters())
return;
// If the name is reserved (matches a formal parameter
// or reserved name, caselessly), then skip it.
if (spec.isReservedParameterName(name))
return;
}
IBinding binding = new StaticBinding(staticValue, location);
component.setBinding(name, binding);
}
private void checkAllComponentsReferenced()
{
// First, contruct a modifiable copy of the ids of all expected components
// (that is, components declared in the specification).
Map components = _loadComponent.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.format(key, _loadComponent.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.getMessage("BaseComponent.and"));
buffer.append(' ');
}
else
buffer.append(", ");
buffer.append(i.next());
j++;
}
buffer.append('.');
LOG.error(buffer.toString());
}
protected ApplicationRuntimeException createBodylessComponentException(IComponent component)
{
return new ApplicationRuntimeException(
Tapestry.getMessage("BaseComponentTemplateLoader.bodyless-component"),
component,
null,
null);
}
}