| // |
| // Tapestry Web Application Framework |
| // Copyright (c) 2000-2002 by Howard Lewis Ship |
| // |
| // Howard Lewis Ship |
| // http://sf.net/projects/tapestry |
| // mailto:hship@users.sf.net |
| // |
| // This library is free software. |
| // |
| // You may redistribute it and/or modify it under the terms of the GNU |
| // Lesser General Public License as published by the Free Software Foundation. |
| // |
| // Version 2.1 of the license should be included with this distribution in |
| // the file LICENSE, as well as License.html. If the license is not |
| // included with this distribution, you may find a copy at the FSF web |
| // site at 'www.gnu.org' or 'www.fsf.org', or you may write to the |
| // Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139 USA. |
| // |
| // This library is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied waranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| // Lesser General Public License for more details. |
| // |
| |
| package net.sf.tapestry; |
| |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.log4j.Category; |
| |
| import net.sf.tapestry.parse.ComponentTemplate; |
| import net.sf.tapestry.parse.TemplateToken; |
| import net.sf.tapestry.parse.TokenType; |
| import net.sf.tapestry.spec.ComponentSpecification; |
| |
| /** |
| * Base implementation for most components that use an HTML template. |
| * |
| * @author Howard Lewis Ship |
| * @version $Id$ |
| **/ |
| |
| public class BaseComponent extends AbstractComponent |
| { |
| /** |
| * @deprecated to be remoaved after 2.1 |
| * |
| **/ |
| |
| protected static final int OUTER_INIT_SIZE = 5; |
| |
| /** |
| * |
| * @deprecated to be removed after 2.1, use {@link IComponent#renderWrapped(IMarkupWriter, IRequestCycle)}. |
| * |
| **/ |
| |
| protected int outerCount = 0; |
| |
| /** |
| * |
| * @deprecated to be removed after 2.1, use {@link IComponent#renderWrapped(IMarkupWriter, IRequestCycle)}. |
| * |
| **/ |
| |
| protected IRender[] outer; |
| |
| private static final Category CAT = |
| Category.getInstance(BaseComponent.class); |
| |
| /** |
| * 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 LocalizedStringRender(String key, Map attributes) |
| { |
| this.key = key; |
| this.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); |
| } |
| } |
| |
| writer.print(getString(key)); |
| |
| 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. |
| * |
| * @deprecated to be marked private after 2.1 |
| * |
| **/ |
| |
| protected 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. |
| * |
| * @deprecated to be marked private after 2.1 |
| **/ |
| |
| protected void readTemplate(IPageLoader loader) throws PageLoaderException |
| { |
| ComponentTemplate componentTemplate; |
| Set seenIds = new HashSet(); |
| IPageSource pageSource = loader.getEngine().getPageSource(); |
| |
| if (CAT.isDebugEnabled()) |
| CAT.debug(this +" reading template"); |
| |
| try |
| { |
| ITemplateSource source = loader.getTemplateSource(); |
| componentTemplate = source.getTemplate(this); |
| } |
| catch (ResourceUnavailableException ex) |
| { |
| throw new PageLoaderException( |
| Tapestry.getString("BaseComponent.unable-to-obtain-template"), |
| this, |
| ex); |
| } |
| |
| 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 (CAT.isDebugEnabled()) |
| CAT.debug(this +" finished reading template"); |
| } |
| |
| /** @since 2.1-beta-2 **/ |
| |
| private void addStringLocalization( |
| IComponent activeComponent, |
| TemplateToken token) |
| { |
| IRender renderer = |
| new LocalizedStringRender(token.getId(), token.getAttributes()); |
| |
| if (activeComponent == null) |
| addOuter(renderer); |
| else |
| activeComponent.addWrapped(renderer); |
| } |
| |
| /** @since 2.1-beta-2 **/ |
| |
| 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.addWrapped(component); |
| } |
| |
| addStaticBindings(component, token.getAttributes(), pageSource); |
| |
| return component; |
| } |
| |
| /** @since 2.1-beta-2 **/ |
| |
| 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.addWrapped(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 |
| { |
| Set ids = null; |
| |
| // First, contruct a modifiable copy of the ids of all expected components |
| // (that is, components declared in the specification). |
| |
| Map components = getComponents(); |
| |
| // Occasionally, a component will have a template but no embedded components. |
| |
| if (components == null) |
| ids = Collections.EMPTY_SET; |
| else |
| 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('.'); |
| |
| throw new PageLoaderException(buffer.toString(), this); |
| } |
| |
| /** |
| * Renders the top level components contained by the receiver. |
| * |
| * @since 2.0.3 |
| **/ |
| |
| protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) |
| throws RequestCycleException |
| { |
| if (CAT.isDebugEnabled()) |
| CAT.debug("Begin render " + getExtendedId()); |
| |
| for (int i = 0; i < outerCount; i++) |
| outer[i].render(writer, cycle); |
| |
| if (CAT.isDebugEnabled()) |
| CAT.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( |
| IPageLoader loader, |
| ComponentSpecification specification) |
| throws PageLoaderException |
| { |
| readTemplate(loader); |
| |
| finishLoad(); |
| } |
| } |