| // 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.parse; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.oro.text.regex.MalformedPatternException; |
| import org.apache.oro.text.regex.MatchResult; |
| import org.apache.oro.text.regex.Pattern; |
| import org.apache.oro.text.regex.PatternMatcher; |
| import org.apache.oro.text.regex.Perl5Compiler; |
| import org.apache.oro.text.regex.Perl5Matcher; |
| import org.apache.tapestry.ApplicationRuntimeException; |
| import org.apache.tapestry.ILocation; |
| import org.apache.tapestry.IResourceLocation; |
| import org.apache.tapestry.Location; |
| import org.apache.tapestry.Tapestry; |
| import org.apache.tapestry.util.IdAllocator; |
| |
| /** |
| * Parses Tapestry templates, breaking them into a series of |
| * {@link org.apache.tapestry.parse.TemplateToken tokens}. |
| * Although often referred to as an "HTML template", there is no real |
| * requirement that the template be HTML. This parser can handle |
| * any reasonable SGML derived markup (including XML), |
| * but specifically works around the ambiguities |
| * of HTML reasonably. |
| * |
| * <p>Dynamic markup in Tapestry attempts to be invisible. |
| * Components are arbitrary tags containing a <code>jwcid</code> attribute. |
| * Such components must be well balanced (have a matching close tag, or |
| * end the tag with "<code>/></code>". |
| * |
| * <p>Generally, the id specified in the template is matched against |
| * an component defined in the specification. However, implicit |
| * components are also possible. The jwcid attribute uses |
| * the syntax "<code>@Type</code>" for implicit components. |
| * Type is the component type, and may include a library id prefix. Such |
| * a component is anonymous (but is given a unique id). |
| * |
| * <p> |
| * (The unique ids assigned start with a dollar sign, which is normally |
| * no allowed for component ids ... this helps to make them stand out |
| * and assures that they do not conflict with |
| * user-defined component ids. These ids tend to propagate |
| * into URLs and become HTML element names and even JavaScript |
| * variable names ... the dollar sign is acceptible in these contexts as |
| * well). |
| * |
| * <p>Implicit component may also be given a name using the syntax |
| * "<code>componentId:@Type</code>". Such a component should |
| * <b>not</b> be defined in the specification, but may still be |
| * accessed via {@link org.apache.tapestry.IComponent#getComponent(String)}. |
| * |
| * <p> |
| * Both defined and implicit components may have additional attributes |
| * defined, simply by including them in the template. They set formal or |
| * informal parameters of the component to static strings. |
| * {@link org.apache.tapestry.spec.IComponentSpecification#getAllowInformalParameters()}, |
| * if false, will cause such attributes to be simply ignored. For defined |
| * components, conflicting values defined in the template are ignored. |
| * |
| * <p>Attributes in component tags will become formal and informal parameters |
| * of the corresponding component. Most attributes will be |
| * |
| * <p>The parser removes |
| * the body of some tags (when the corresponding component doesn't |
| * {@link org.apache.tapestry.spec.IComponentSpecification#getAllowBody() allow a body}, |
| * and allows |
| * portions of the template to be completely removed. |
| * |
| * <p>The parser does a pretty thorough lexical analysis of the template, |
| * and reports a great number of errors, including improper nesting |
| * of tags. |
| * |
| * <p>The parser supports <em>invisible localization</em>: |
| * The parser recognizes HTML of the form: |
| * <code><span key="<i>value</i>"> ... </span></code> |
| * and converts them into a {@link TokenType#LOCALIZATION} |
| * token. You may also specifify a <code>raw</code> attribute ... if the value |
| * is <code>true</code>, then the localized value is |
| * sent to the client without filtering, which is appropriate if the |
| * value has any markup that should not be escaped. |
| * |
| * @author Howard Lewis Ship, Geoff Longman |
| * @version $Id$ |
| * |
| **/ |
| |
| public class TemplateParser |
| { |
| /** |
| * A Factory used by {@link org.apache.tapestry.parse.TemplateParser} to create |
| * {@link org.apache.tapestry.parse.TemplateToken} objects. |
| * |
| * <p> |
| * This class is extended by Spindle - the Eclipse Plugin for Tapestry. |
| * <p> |
| * @author glongman@intelligentworks.com |
| * @since 3.0 |
| */ |
| protected static class TemplateTokenFactory |
| { |
| |
| public OpenToken createOpenToken(String tagName, String jwcId, String type, ILocation location) |
| { |
| return new OpenToken(tagName, jwcId, type, location); |
| } |
| |
| public CloseToken createCloseToken(String tagName, ILocation location) |
| { |
| return new CloseToken(tagName, location); |
| } |
| |
| public TextToken createTextToken(char[] templateData, int blockStart, int end, ILocation templateLocation) |
| { |
| return new TextToken(templateData, blockStart, end, templateLocation); |
| } |
| |
| public LocalizationToken createLocalizationToken( |
| String tagName, |
| String localizationKey, |
| boolean raw, |
| Map attributes, |
| ILocation startLocation) |
| { |
| return new LocalizationToken(tagName, localizationKey, raw, attributes, startLocation); |
| } |
| } |
| |
| /** |
| * Attribute value prefix indicating that the attribute is an OGNL expression. |
| * |
| * @since 3.0 |
| **/ |
| |
| public static final String OGNL_EXPRESSION_PREFIX = "ognl:"; |
| |
| /** |
| * Attribute value prefix indicating that the attribute is a localization |
| * key. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| public static final String LOCALIZATION_KEY_PREFIX = "message:"; |
| |
| /** |
| * A "magic" component id that causes the tag with the id and its entire |
| * body to be ignored during parsing. |
| * |
| **/ |
| |
| private static final String REMOVE_ID = "$remove$"; |
| |
| /** |
| * A "magic" component id that causes the tag to represent the true |
| * content of the template. Any content prior to the tag is discarded, |
| * and any content after the tag is ignored. The tag itself is not |
| * included. |
| * |
| **/ |
| |
| private static final String CONTENT_ID = "$content$"; |
| |
| /** |
| * |
| * The attribute, checked for in <span> tags, that signfies |
| * that the span is being used as an invisible localization. |
| * |
| * @since 2.0.4 |
| * |
| **/ |
| |
| public static final String LOCALIZATION_KEY_ATTRIBUTE_NAME = "key"; |
| |
| /** |
| * Used with {@link #LOCALIZATION_KEY_ATTRIBUTE_NAME} to indicate a string |
| * that should be rendered "raw" (without escaping HTML). If not specified, |
| * defaults to "false". The value must equal "true" (caselessly). |
| * |
| * @since 2.3 |
| * |
| **/ |
| |
| public static final String RAW_ATTRIBUTE_NAME = "raw"; |
| |
| /** |
| * Attribute used to identify components. |
| * |
| * @since 2.3 |
| * |
| **/ |
| |
| public static final String JWCID_ATTRIBUTE_NAME = "jwcid"; |
| |
| private static final String PROPERTY_NAME_PATTERN = "_?[a-zA-Z]\\w*"; |
| |
| /** |
| * Pattern used to recognize ordinary components (defined in the specification). |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| public static final String SIMPLE_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")$"; |
| |
| /** |
| * Pattern used to recognize implicit components (whose type is defined in |
| * the template). Subgroup 1 is the id (which may be null) and subgroup 2 |
| * is the type (which may be qualified with a library prefix). |
| * Subgroup 4 is the library id, Subgroup 5 is the simple component type. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| public static final String IMPLICIT_ID_PATTERN = |
| "^(" + PROPERTY_NAME_PATTERN + ")?@(((" + PROPERTY_NAME_PATTERN + "):)?(" + PROPERTY_NAME_PATTERN + "))$"; |
| |
| private static final int IMPLICIT_ID_PATTERN_ID_GROUP = 1; |
| private static final int IMPLICIT_ID_PATTERN_TYPE_GROUP = 2; |
| private static final int IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP = 4; |
| private static final int IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP = 5; |
| |
| private Pattern _simpleIdPattern; |
| private Pattern _implicitIdPattern; |
| private PatternMatcher _patternMatcher; |
| |
| private IdAllocator _idAllocator = new IdAllocator(); |
| |
| private ITemplateParserDelegate _delegate; |
| |
| /** |
| * Identifies the template being parsed; used with error messages. |
| * |
| **/ |
| |
| private IResourceLocation _resourceLocation; |
| |
| /** |
| * Shared instance of {@link Location} used by |
| * all {@link TextToken} instances in the template. |
| * |
| **/ |
| |
| private ILocation _templateLocation; |
| |
| /** |
| * Location with in the resource for the current line. |
| * |
| **/ |
| |
| private ILocation _currentLocation; |
| |
| /** |
| * Local reference to the template data that is to be parsed. |
| * |
| **/ |
| |
| private char[] _templateData; |
| |
| /** |
| * List of Tag |
| * |
| **/ |
| |
| private List _stack = new ArrayList(); |
| |
| private static class Tag |
| { |
| // The element, i.e., <jwc> or virtually any other element (via jwcid attribute) |
| String _tagName; |
| // If true, the tag is a placeholder for a dynamic element |
| boolean _component; |
| // If true, the body of the tag is being ignored, and the |
| // ignore flag is cleared when the close tag is reached |
| boolean _ignoringBody; |
| // If true, then the entire tag (and its body) is being ignored |
| boolean _removeTag; |
| // If true, then the tag must have a balanced closing tag. |
| // This is always true for components. |
| boolean _mustBalance; |
| // The line on which the start tag exists |
| int _line; |
| // If true, then the parse ends when the closing tag is found. |
| boolean _content; |
| |
| Tag(String tagName, int line) |
| { |
| _tagName = tagName; |
| _line = line; |
| } |
| |
| boolean match(String matchTagName) |
| { |
| return _tagName.equalsIgnoreCase(matchTagName); |
| } |
| } |
| |
| /** |
| * List of {@link TemplateToken}, this forms the ultimate response. |
| * |
| **/ |
| |
| private List _tokens = new ArrayList(); |
| |
| /** |
| * The location of the 'cursor' within the template data. The |
| * advance() method moves this forward. |
| * |
| **/ |
| |
| private int _cursor; |
| |
| /** |
| * The start of the current block of static text, or -1 if no block |
| * is active. |
| * |
| **/ |
| |
| private int _blockStart; |
| |
| /** |
| * The current line number; tracked by advance(). Starts at 1. |
| * |
| **/ |
| |
| private int _line; |
| |
| /** |
| * Set to true when the body of a tag is being ignored. This is typically |
| * used to skip over the body of a tag when its corresponding |
| * component doesn't allow a body, or whe the special |
| * jwcid of $remove$ is used. |
| * |
| **/ |
| |
| private boolean _ignoring; |
| |
| /** |
| * A {@link Map} of {@link String}s, used to store attributes collected |
| * while parsing a tag. |
| * |
| **/ |
| |
| private Map _attributes = new HashMap(); |
| |
| /** |
| * A factory used to create template tokens. |
| * <p> |
| * author glongman@intelligentworks.com |
| */ |
| |
| protected TemplateTokenFactory _factory; |
| |
| public TemplateParser() |
| { |
| Perl5Compiler compiler = new Perl5Compiler(); |
| |
| try |
| { |
| _simpleIdPattern = compiler.compile(SIMPLE_ID_PATTERN); |
| _implicitIdPattern = compiler.compile(IMPLICIT_ID_PATTERN); |
| } catch (MalformedPatternException ex) |
| { |
| throw new ApplicationRuntimeException(ex); |
| } |
| |
| _patternMatcher = new Perl5Matcher(); |
| |
| _factory = new TemplateTokenFactory(); |
| } |
| |
| /** |
| * Parses the template data into an array of {@link TemplateToken}s. |
| * |
| * <p>The parser is <i>decidedly</i> not threadsafe, so care should be taken |
| * that only a single thread accesses it. |
| * |
| * @param templateData the HTML template to parse. Some tokens will hold |
| * a reference to this array. |
| * @param delegate object that "knows" about defined components |
| * @param resourceLocation a description of where the template originated from, |
| * used with error messages. |
| * |
| **/ |
| |
| public TemplateToken[] parse( |
| char[] templateData, |
| ITemplateParserDelegate delegate, |
| IResourceLocation resourceLocation) |
| throws TemplateParseException |
| { |
| TemplateToken[] result = null; |
| |
| try |
| { |
| beforeParse(templateData, delegate, resourceLocation); |
| |
| parse(); |
| |
| result = (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]); |
| } finally |
| { |
| afterParse(); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * perform default initialization of the parser. |
| * <p> |
| * author glongman@intelligentworks.com |
| */ |
| |
| protected void beforeParse( |
| char[] templateData, |
| ITemplateParserDelegate delegate, |
| IResourceLocation resourceLocation) |
| { |
| _templateData = templateData; |
| _resourceLocation = resourceLocation; |
| _templateLocation = new Location(resourceLocation); |
| _delegate = delegate; |
| _ignoring = false; |
| _line = 1; |
| } |
| |
| /** |
| * Perform default cleanup after parsing completes. |
| * <p> |
| * author glongman@intelligentworks.com |
| */ |
| |
| protected void afterParse() |
| { |
| _delegate = null; |
| _templateData = null; |
| _resourceLocation = null; |
| _templateLocation = null; |
| _currentLocation = null; |
| _stack.clear(); |
| _tokens.clear(); |
| _attributes.clear(); |
| _idAllocator.clear(); |
| } |
| |
| /** |
| * Used by the parser to report problems in the parse. |
| * Parsing <b>must</b> stop when a problem is reported. |
| * <p> |
| * The default implementation simply throws an exception that contains |
| * the message and location parameters. |
| * <p> |
| * Subclasses may override but <b>must</b> ensure they throw the required exception. |
| * <p> |
| * |
| * author glongman@intelligentworks.com |
| * |
| * @param message |
| * @param location |
| * @param line ignored by the default impl |
| * @param cursor ignored by the default impl |
| * @throws TemplateParseException always thrown in order to terminate the parse. |
| */ |
| |
| protected void templateParseProblem(String message, ILocation location, int line, int cursor) |
| throws TemplateParseException |
| { |
| throw new TemplateParseException(message, location); |
| } |
| |
| /** |
| * Used by the parser to report tapestry runtime specific problems in the parse. |
| * Parsing <b>must</b> stop when a problem is reported. |
| * <p> |
| * The default implementation simply rethrows the exception. |
| * <p> |
| * Subclasses may override but <b>must</b> ensure they rethrow the exception. |
| * <p> |
| * |
| * author glongman@intelligentworks.com |
| * |
| * @param exception |
| * @param line ignored by the default impl |
| * @param cursor ignored by the default impl |
| * @throws ApplicationRuntimeException always rethrown in order to terminate the parse. |
| */ |
| |
| protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor) |
| throws ApplicationRuntimeException |
| { |
| throw exception; |
| } |
| |
| /** |
| * Give subclasses access to the parse results. |
| * <p> |
| * |
| * author glongman@intelligentworks.com |
| */ |
| protected List getTokens() |
| { |
| if (_tokens == null) |
| return Collections.EMPTY_LIST; |
| |
| return _tokens; |
| } |
| |
| /** |
| * Checks to see if the next few characters match a given pattern. |
| * |
| **/ |
| |
| private boolean lookahead(char[] match) |
| { |
| try |
| { |
| for (int i = 0; i < match.length; i++) |
| { |
| if (_templateData[_cursor + i] != match[i]) |
| return false; |
| } |
| |
| // Every character matched. |
| |
| return true; |
| } catch (IndexOutOfBoundsException ex) |
| { |
| return false; |
| } |
| } |
| |
| private static final char[] COMMENT_START = new char[] { '<', '!', '-', '-' }; |
| private static final char[] COMMENT_END = new char[] { '-', '-', '>' }; |
| private static final char[] CLOSE_TAG = new char[] { '<', '/' }; |
| |
| protected void parse() throws TemplateParseException |
| { |
| _cursor = 0; |
| _blockStart = -1; |
| int length = _templateData.length; |
| |
| while (_cursor < length) |
| { |
| if (_templateData[_cursor] != '<') |
| { |
| if (_blockStart < 0 && !_ignoring) |
| _blockStart = _cursor; |
| |
| advance(); |
| continue; |
| } |
| |
| // OK, start of something. |
| |
| if (lookahead(CLOSE_TAG)) |
| { |
| closeTag(); |
| continue; |
| } |
| |
| if (lookahead(COMMENT_START)) |
| { |
| skipComment(); |
| continue; |
| } |
| |
| // The start of some tag. |
| |
| startTag(); |
| } |
| |
| // Usually there's some text at the end of the template (after the last closing tag) that should |
| // be added. Often the last few tags are static tags so we definately |
| // need to end the text block. |
| |
| addTextToken(_templateData.length - 1); |
| } |
| |
| /** |
| * Advance forward in the document until the end of the comment is reached. |
| * In addition, skip any whitespace following the comment. |
| * |
| **/ |
| |
| private void skipComment() throws TemplateParseException |
| { |
| int length = _templateData.length; |
| int startLine = _line; |
| |
| if (_blockStart < 0 && !_ignoring) |
| _blockStart = _cursor; |
| |
| while (true) |
| { |
| if (_cursor >= length) |
| templateParseProblem( |
| Tapestry.format("TemplateParser.comment-not-ended", Integer.toString(startLine)), |
| new Location(_resourceLocation, startLine), |
| startLine, |
| _cursor); |
| |
| if (lookahead(COMMENT_END)) |
| break; |
| |
| // Not the end of the comment, advance over it. |
| |
| advance(); |
| } |
| |
| _cursor += COMMENT_END.length; |
| advanceOverWhitespace(); |
| } |
| |
| private void addTextToken(int end) |
| { |
| // No active block to add to. |
| |
| if (_blockStart < 0) |
| return; |
| |
| if (_blockStart <= end) |
| { |
| TemplateToken token = _factory.createTextToken(_templateData, _blockStart, end, _templateLocation); |
| |
| _tokens.add(token); |
| } |
| |
| _blockStart = -1; |
| } |
| |
| private static final int WAIT_FOR_ATTRIBUTE_NAME = 0; |
| private static final int COLLECT_ATTRIBUTE_NAME = 1; |
| private static final int ADVANCE_PAST_EQUALS = 2; |
| private static final int WAIT_FOR_ATTRIBUTE_VALUE = 3; |
| private static final int COLLECT_QUOTED_VALUE = 4; |
| private static final int COLLECT_UNQUOTED_VALUE = 5; |
| |
| private void startTag() throws TemplateParseException |
| { |
| int cursorStart = _cursor; |
| int length = _templateData.length; |
| String tagName = null; |
| boolean endOfTag = false; |
| boolean emptyTag = false; |
| int startLine = _line; |
| ILocation startLocation = new Location(_resourceLocation, startLine); |
| |
| tagBeginEvent(startLine, _cursor); |
| |
| advance(); |
| |
| // Collect the element type |
| |
| while (_cursor < length) |
| { |
| char ch = _templateData[_cursor]; |
| |
| if (ch == '/' || ch == '>' || Character.isWhitespace(ch)) |
| { |
| tagName = new String(_templateData, cursorStart + 1, _cursor - cursorStart - 1); |
| |
| break; |
| } |
| |
| advance(); |
| } |
| |
| String attributeName = null; |
| int attributeNameStart = -1; |
| int attributeValueStart = -1; |
| int state = WAIT_FOR_ATTRIBUTE_NAME; |
| char quoteChar = 0; |
| |
| _attributes.clear(); |
| |
| // Collect each attribute |
| |
| while (!endOfTag) |
| { |
| if (_cursor >= length) |
| { |
| String key = (tagName == null) ? "TemplateParser.unclosed-unknown-tag" : "TemplateParser.unclosed-tag"; |
| |
| templateParseProblem( |
| Tapestry.format(key, tagName, Integer.toString(startLine)), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| } |
| |
| char ch = _templateData[_cursor]; |
| |
| switch (state) |
| { |
| case WAIT_FOR_ATTRIBUTE_NAME : |
| |
| // Ignore whitespace before the next attribute name, while |
| // looking for the end of the current tag. |
| |
| if (ch == '/') |
| { |
| emptyTag = true; |
| advance(); |
| break; |
| } |
| |
| if (ch == '>') |
| { |
| endOfTag = true; |
| break; |
| } |
| |
| if (Character.isWhitespace(ch)) |
| { |
| advance(); |
| break; |
| } |
| |
| // Found non-whitespace, assume its the attribute name. |
| // Note: could use a check here for non-alpha. |
| |
| attributeNameStart = _cursor; |
| state = COLLECT_ATTRIBUTE_NAME; |
| advance(); |
| break; |
| |
| case COLLECT_ATTRIBUTE_NAME : |
| |
| // Looking for end of attribute name. |
| |
| if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch)) |
| { |
| attributeName = new String(_templateData, attributeNameStart, _cursor - attributeNameStart); |
| |
| state = ADVANCE_PAST_EQUALS; |
| break; |
| } |
| |
| // Part of the attribute name |
| |
| advance(); |
| break; |
| |
| case ADVANCE_PAST_EQUALS : |
| |
| // Looking for the '=' sign. May hit the end of the tag, or (for bare attributes), |
| // the next attribute name. |
| |
| if (ch == '/' || ch == '>') |
| { |
| // A bare attribute, which is not interesting to |
| // us. |
| |
| state = WAIT_FOR_ATTRIBUTE_NAME; |
| break; |
| } |
| |
| if (Character.isWhitespace(ch)) |
| { |
| advance(); |
| break; |
| } |
| |
| if (ch == '=') |
| { |
| state = WAIT_FOR_ATTRIBUTE_VALUE; |
| quoteChar = 0; |
| attributeValueStart = -1; |
| advance(); |
| break; |
| } |
| |
| // Otherwise, an HTML style "bare" attribute (such as <select multiple>). |
| // We aren't interested in those (we're just looking for the id or jwcid attribute). |
| |
| state = WAIT_FOR_ATTRIBUTE_NAME; |
| break; |
| |
| case WAIT_FOR_ATTRIBUTE_VALUE : |
| |
| if (ch == '/' || ch == '>') |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.missing-attribute-value", |
| tagName, |
| Integer.toString(_line), |
| attributeName), |
| getCurrentLocation(), |
| _line, |
| _cursor); |
| |
| // Ignore whitespace between '=' and the attribute value. Also, look |
| // for initial quote. |
| |
| if (Character.isWhitespace(ch)) |
| { |
| advance(); |
| break; |
| } |
| |
| if (ch == '\'' || ch == '"') |
| { |
| quoteChar = ch; |
| |
| state = COLLECT_QUOTED_VALUE; |
| advance(); |
| attributeValueStart = _cursor; |
| attributeBeginEvent(attributeName, _line, attributeValueStart); |
| break; |
| } |
| |
| // Not whitespace or quote, must be start of unquoted attribute. |
| |
| state = COLLECT_UNQUOTED_VALUE; |
| attributeValueStart = _cursor; |
| attributeBeginEvent(attributeName, _line, attributeValueStart); |
| break; |
| |
| case COLLECT_QUOTED_VALUE : |
| |
| // Start collecting the quoted attribute value. Stop at the matching quote character, |
| // unless bare, in which case, stop at the next whitespace. |
| |
| if (ch == quoteChar) |
| { |
| String attributeValue = |
| new String(_templateData, attributeValueStart, _cursor - attributeValueStart); |
| |
| |
| attributeEndEvent(_cursor); |
| |
| if (_attributes.containsKey(attributeName)) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.duplicate-tag-attribute", |
| tagName, |
| Integer.toString(_line), |
| attributeName), |
| getCurrentLocation(), |
| _line, |
| _cursor); |
| |
| _attributes.put(attributeName, attributeValue); |
| |
| // Advance over the quote. |
| advance(); |
| state = WAIT_FOR_ATTRIBUTE_NAME; |
| break; |
| } |
| |
| advance(); |
| break; |
| |
| case COLLECT_UNQUOTED_VALUE : |
| |
| // An unquoted attribute value ends with whitespace |
| // or the end of the enclosing tag. |
| |
| if (ch == '/' || ch == '>' || Character.isWhitespace(ch)) |
| { |
| String attributeValue = |
| new String(_templateData, attributeValueStart, _cursor - attributeValueStart); |
| |
| attributeEndEvent(_cursor); |
| |
| if (_attributes.containsKey(attributeName)) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.duplicate-tag-attribute", |
| tagName, |
| Integer.toString(_line), |
| attributeName), |
| getCurrentLocation(), |
| _line, |
| _cursor); |
| |
| _attributes.put(attributeName, attributeValue); |
| |
| state = WAIT_FOR_ATTRIBUTE_NAME; |
| break; |
| } |
| |
| advance(); |
| break; |
| } |
| } |
| |
| tagEndEvent(_cursor); |
| |
| // Check for invisible localizations |
| |
| String localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes); |
| String jwcId = findValueCaselessly(JWCID_ATTRIBUTE_NAME, _attributes); |
| |
| if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null) |
| { |
| if (_ignoring) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.component-may-not-be-ignored", |
| tagName, |
| Integer.toString(startLine)), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| // If the tag isn't empty, then create a Tag instance to ignore the |
| // body of the tag. |
| |
| if (!emptyTag) |
| { |
| Tag tag = new Tag(tagName, startLine); |
| |
| tag._component = false; |
| tag._removeTag = true; |
| tag._ignoringBody = true; |
| tag._mustBalance = true; |
| |
| _stack.add(tag); |
| |
| // Start ignoring content until the close tag. |
| |
| _ignoring = true; |
| } else |
| { |
| // Cursor is at the closing carat, advance over it and any whitespace. |
| advance(); |
| advanceOverWhitespace(); |
| } |
| |
| // End any open block. |
| |
| addTextToken(cursorStart - 1); |
| |
| boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes); |
| |
| Map attributes = filter(_attributes, new String[] { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME }); |
| |
| TemplateToken token = |
| _factory.createLocalizationToken(tagName, localizationKey, raw, attributes, startLocation); |
| |
| _tokens.add(token); |
| |
| return; |
| } |
| |
| if (jwcId != null) |
| { |
| processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation); |
| return; |
| } |
| |
| // A static tag (not a tag without a jwcid attribute). |
| // We need to record this so that we can match close tags later. |
| |
| if (!emptyTag) |
| { |
| Tag tag = new Tag(tagName, startLine); |
| _stack.add(tag); |
| } |
| |
| // If there wasn't an active block, then start one. |
| |
| if (_blockStart < 0 && !_ignoring) |
| _blockStart = cursorStart; |
| |
| advance(); |
| } |
| |
| /** |
| * Processes a tag that is the open tag for a component (but also handles |
| * the $remove$ and $content$ tags). |
| * |
| **/ |
| |
| /** |
| * Notify that the beginning of a tag has been detected. |
| * <p> |
| * Default implementation does nothing. |
| * <p> |
| * |
| * author glongman@intelligentworks.com |
| */ |
| protected void tagBeginEvent(int startLine, int cursorPosition) |
| { |
| } |
| |
| /** |
| * Notify that the end of the current tag has been detected. |
| * <p> |
| * Default implementation does nothing. |
| * <p> |
| * author glongman@intelligentworks.com |
| */ |
| protected void tagEndEvent(int cursorPosition) |
| { |
| } |
| |
| /** |
| * Notify that the beginning of an attribute value has been detected. |
| * <p> |
| * Default implementation does nothing. |
| * <p> |
| * author glongman@intelligentworks.com |
| */ |
| protected void attributeBeginEvent(String attributeName, int startLine, int cursorPosition) |
| { |
| } |
| |
| /** |
| * Notify that the end of the current attribute value has been detected. |
| * <p> |
| * Default implementation does nothing. |
| * <p> |
| * author glongman@intelligentworks.com |
| */ |
| protected void attributeEndEvent(int cursorPosition) |
| { |
| } |
| |
| private void processComponentStart( |
| String tagName, |
| String jwcId, |
| boolean emptyTag, |
| int startLine, |
| int cursorStart, |
| ILocation startLocation) |
| throws TemplateParseException |
| { |
| if (jwcId.equalsIgnoreCase(CONTENT_ID)) |
| { |
| processContentTag(tagName, startLine, cursorStart, emptyTag); |
| |
| return; |
| } |
| |
| boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID); |
| |
| if (_ignoring && !isRemoveId) |
| templateParseProblem( |
| Tapestry.format("TemplateParser.component-may-not-be-ignored", tagName, Integer.toString(startLine)), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| String type = null; |
| boolean allowBody = false; |
| |
| if (_patternMatcher.matches(jwcId, _implicitIdPattern)) |
| { |
| MatchResult match = _patternMatcher.getMatch(); |
| |
| jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP); |
| type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP); |
| |
| String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP); |
| String simpleType = match.group(IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP); |
| |
| // If (and this is typical) no actual component id was specified, |
| // then generate one on the fly. |
| // The allocated id for anonymous components is |
| // based on the simple (unprefixed) type, but starts |
| // with a leading dollar sign to ensure no conflicts |
| // with user defined component ids (which don't allow dollar signs |
| // in the id). |
| |
| if (jwcId == null) |
| jwcId = _idAllocator.allocateId("$" + simpleType); |
| |
| try |
| { |
| allowBody = _delegate.getAllowBody(libraryId, simpleType, startLocation); |
| } catch (ApplicationRuntimeException e) |
| { |
| // give subclasses a chance to handle and rethrow |
| templateParseProblem(e, startLine, cursorStart); |
| } |
| |
| } else |
| { |
| if (!isRemoveId) |
| { |
| if (!_patternMatcher.matches(jwcId, _simpleIdPattern)) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.component-id-invalid", |
| tagName, |
| Integer.toString(startLine), |
| jwcId), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| if (!_delegate.getKnownComponent(jwcId)) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.unknown-component-id", |
| tagName, |
| Integer.toString(startLine), |
| jwcId), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| try |
| { |
| allowBody = _delegate.getAllowBody(jwcId, startLocation); |
| } catch (ApplicationRuntimeException e) |
| { |
| // give subclasses a chance to handle and rethrow |
| templateParseProblem(e, startLine, cursorStart); |
| } |
| } |
| } |
| |
| // Ignore the body if we're removing the entire tag, |
| // of if the corresponding component doesn't allow |
| // a body. |
| |
| boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody); |
| |
| if (_ignoring && ignoreBody) |
| templateParseProblem( |
| Tapestry.format("TemplateParser.nested-ignore", tagName, Integer.toString(startLine)), |
| new Location(_resourceLocation, startLine), |
| startLine, |
| cursorStart); |
| |
| if (!emptyTag) |
| pushNewTag(tagName, startLine, isRemoveId, ignoreBody); |
| |
| // End any open block. |
| |
| addTextToken(cursorStart - 1); |
| |
| if (!isRemoveId) |
| { |
| addOpenToken(tagName, jwcId, type, startLocation); |
| |
| if (emptyTag) |
| _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation())); |
| } |
| |
| advance(); |
| } |
| |
| private void pushNewTag(String tagName, int startLine, boolean isRemoveId, boolean ignoreBody) |
| { |
| Tag tag = new Tag(tagName, startLine); |
| |
| tag._component = !isRemoveId; |
| tag._removeTag = isRemoveId; |
| |
| tag._ignoringBody = ignoreBody; |
| |
| _ignoring = tag._ignoringBody; |
| |
| tag._mustBalance = true; |
| |
| _stack.add(tag); |
| } |
| |
| private void processContentTag(String tagName, int startLine, int cursorStart, boolean emptyTag) |
| throws TemplateParseException |
| { |
| if (_ignoring) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.content-block-may-not-be-ignored", |
| tagName, |
| Integer.toString(startLine)), |
| new Location(_resourceLocation, startLine), |
| startLine, |
| cursorStart); |
| |
| if (emptyTag) |
| templateParseProblem( |
| Tapestry.format("TemplateParser.content-block-may-not-be-empty", tagName, Integer.toString(startLine)), |
| new Location(_resourceLocation, startLine), |
| startLine, |
| cursorStart); |
| |
| _tokens.clear(); |
| _blockStart = -1; |
| |
| Tag tag = new Tag(tagName, startLine); |
| |
| tag._mustBalance = true; |
| tag._content = true; |
| |
| _stack.clear(); |
| _stack.add(tag); |
| |
| advance(); |
| } |
| |
| private void addOpenToken(String tagName, String jwcId, String type, ILocation location) |
| { |
| OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location); |
| _tokens.add(token); |
| |
| if (_attributes.isEmpty()) |
| return; |
| |
| Iterator i = _attributes.entrySet().iterator(); |
| while (i.hasNext()) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| |
| String key = (String) entry.getKey(); |
| |
| if (key.equalsIgnoreCase(JWCID_ATTRIBUTE_NAME)) |
| continue; |
| |
| String value = (String) entry.getValue(); |
| |
| addAttributeToToken(token, key, value); |
| } |
| } |
| |
| /** |
| * Analyzes the attribute value, looking for possible prefixes that indicate |
| * the value is not a literal. Adds the attribute to the |
| * token. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| private void addAttributeToToken(OpenToken token, String name, String attributeValue) |
| { |
| int pos = attributeValue.indexOf(":"); |
| |
| if (pos > 0) |
| { |
| |
| String prefix = attributeValue.substring(0, pos + 1); |
| |
| if (prefix.equals(OGNL_EXPRESSION_PREFIX)) |
| { |
| token.addAttribute( |
| name, |
| AttributeType.OGNL_EXPRESSION, |
| extractExpression(attributeValue.substring(pos + 1))); |
| return; |
| } |
| |
| if (prefix.equals(LOCALIZATION_KEY_PREFIX)) |
| { |
| token.addAttribute(name, AttributeType.LOCALIZATION_KEY, attributeValue.substring(pos + 1).trim()); |
| return; |
| |
| } |
| } |
| |
| token.addAttribute(name, AttributeType.LITERAL, attributeValue); |
| } |
| |
| /** |
| * Invoked to handle a closing tag, i.e., </foo>. When a tag closes, it will match against |
| * a tag on the open tag start. Preferably the top tag on the stack (if everything is well balanced), but this |
| * is HTML, not XML, so many tags won't balance. |
| * |
| * <p>Once the matching tag is located, the question is ... is the tag dynamic or static? If static, then |
| * the current text block is extended to include this close tag. If dynamic, then the current text block |
| * is ended (before the '<' that starts the tag) and a close token is added. |
| * |
| * <p>In either case, the matching static element and anything above it is removed, and the cursor is left |
| * on the character following the '>'. |
| * |
| **/ |
| |
| private void closeTag() throws TemplateParseException |
| { |
| int cursorStart = _cursor; |
| int length = _templateData.length; |
| int startLine = _line; |
| |
| ILocation startLocation = getCurrentLocation(); |
| |
| _cursor += CLOSE_TAG.length; |
| |
| int tagStart = _cursor; |
| |
| while (true) |
| { |
| if (_cursor >= length) |
| templateParseProblem( |
| Tapestry.format("TemplateParser.incomplete-close-tag", Integer.toString(startLine)), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| char ch = _templateData[_cursor]; |
| |
| if (ch == '>') |
| break; |
| |
| advance(); |
| } |
| |
| String tagName = new String(_templateData, tagStart, _cursor - tagStart); |
| |
| int stackPos = _stack.size() - 1; |
| Tag tag = null; |
| |
| while (stackPos >= 0) |
| { |
| tag = (Tag) _stack.get(stackPos); |
| |
| if (tag.match(tagName)) |
| break; |
| |
| if (tag._mustBalance) |
| templateParseProblem( |
| Tapestry.format( |
| "TemplateParser.improperly-nested-close-tag", |
| new Object[] { |
| tagName, |
| Integer.toString(startLine), |
| tag._tagName, |
| Integer.toString(tag._line)}), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| stackPos--; |
| } |
| |
| if (stackPos < 0) |
| templateParseProblem( |
| Tapestry.format("TemplateParser.unmatched-close-tag", tagName, Integer.toString(startLine)), |
| startLocation, |
| startLine, |
| cursorStart); |
| |
| // Special case for the content tag |
| |
| if (tag._content) |
| { |
| addTextToken(cursorStart - 1); |
| |
| // Advance the cursor right to the end. |
| |
| _cursor = length; |
| _stack.clear(); |
| return; |
| } |
| |
| // When a component closes, add a CLOSE tag. |
| if (tag._component) |
| { |
| addTextToken(cursorStart - 1); |
| |
| _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation())); |
| } else |
| { |
| // The close of a static tag. Unless removing the tag |
| // entirely, make sure the block tag is part of a text block. |
| |
| if (_blockStart < 0 && !tag._removeTag && !_ignoring) |
| _blockStart = cursorStart; |
| } |
| |
| // Remove all elements at stackPos or above. |
| |
| for (int i = _stack.size() - 1; i >= stackPos; i--) |
| _stack.remove(i); |
| |
| // Advance cursor past '>' |
| |
| advance(); |
| |
| // If editting out the tag (i.e., $remove$) then kill any whitespace. |
| // For components that simply don't contain a body, removeTag will |
| // be false. |
| |
| if (tag._removeTag) |
| advanceOverWhitespace(); |
| |
| // If we were ignoring the body of the tag, then clear the ignoring |
| // flag, since we're out of the body. |
| |
| if (tag._ignoringBody) |
| _ignoring = false; |
| } |
| |
| /** |
| * Advances the cursor to the next character. |
| * If the end-of-line is reached, then increments |
| * the line counter. |
| * |
| **/ |
| |
| private void advance() |
| { |
| int length = _templateData.length; |
| |
| if (_cursor >= length) |
| return; |
| |
| char ch = _templateData[_cursor]; |
| |
| _cursor++; |
| |
| if (ch == '\n') |
| { |
| _line++; |
| _currentLocation = null; |
| return; |
| } |
| |
| // A \r, or a \r\n also counts as a new line. |
| |
| if (ch == '\r') |
| { |
| _line++; |
| _currentLocation = null; |
| |
| if (_cursor < length && _templateData[_cursor] == '\n') |
| _cursor++; |
| |
| return; |
| } |
| |
| // Not an end-of-line character. |
| |
| } |
| |
| private void advanceOverWhitespace() |
| { |
| int length = _templateData.length; |
| |
| while (_cursor < length) |
| { |
| char ch = _templateData[_cursor]; |
| if (!Character.isWhitespace(ch)) |
| return; |
| |
| advance(); |
| } |
| } |
| |
| /** |
| * Returns a new Map that is a copy of the input Map with some |
| * key/value pairs removed. A list of keys is passed in |
| * and matching keys (caseless comparison) from the input |
| * Map are excluded from the output map. May return null |
| * (rather than return an empty Map). |
| * |
| **/ |
| |
| private Map filter(Map input, String[] removeKeys) |
| { |
| if (input == null || input.isEmpty()) |
| return null; |
| |
| Map result = null; |
| |
| Iterator i = input.entrySet().iterator(); |
| |
| nextkey : while (i.hasNext()) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| |
| String key = (String) entry.getKey(); |
| |
| for (int j = 0; j < removeKeys.length; j++) |
| { |
| if (key.equalsIgnoreCase(removeKeys[j])) |
| continue nextkey; |
| } |
| |
| if (result == null) |
| result = new HashMap(input.size()); |
| |
| result.put(key, entry.getValue()); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Searches a Map for given key, caselessly. The Map is expected to consist of Strings for keys and |
| * values. Returns the value for the first key found that matches (caselessly) the input key. Returns null |
| * if no value found. |
| * |
| **/ |
| |
| protected String findValueCaselessly(String key, Map map) |
| { |
| String result = (String) map.get(key); |
| |
| if (result != null) |
| return result; |
| |
| Iterator i = map.entrySet().iterator(); |
| while (i.hasNext()) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| |
| String entryKey = (String) entry.getKey(); |
| |
| if (entryKey.equalsIgnoreCase(key)) |
| return (String) entry.getValue(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Conversions needed by {@link #extractExpression(String)} |
| * |
| **/ |
| |
| private static final String[] CONVERSIONS = { "<", "<", ">", ">", """, "\"", "&", "&" }; |
| |
| /** |
| * Provided a raw input string that has been recognized to be an expression, |
| * this removes excess white space and converts &amp;, &quot; &lt; and &gt; |
| * to their normal character values (otherwise its impossible to specify |
| * those values in expressions in the template). |
| * |
| **/ |
| |
| private String extractExpression(String input) |
| { |
| int inputLength = input.length(); |
| |
| StringBuffer buffer = new StringBuffer(inputLength); |
| |
| int cursor = 0; |
| |
| outer : while (cursor < inputLength) |
| { |
| for (int i = 0; i < CONVERSIONS.length; i += 2) |
| { |
| String entity = CONVERSIONS[i]; |
| int entityLength = entity.length(); |
| String value = CONVERSIONS[i + 1]; |
| |
| if (cursor + entityLength > inputLength) |
| continue; |
| |
| if (input.substring(cursor, cursor + entityLength).equals(entity)) |
| { |
| buffer.append(value); |
| cursor += entityLength; |
| continue outer; |
| } |
| } |
| |
| buffer.append(input.charAt(cursor)); |
| cursor++; |
| } |
| |
| return buffer.toString().trim(); |
| } |
| |
| /** |
| * Returns true if the map contains the given key (caseless search) and the value |
| * is "true" (caseless comparison). |
| * |
| **/ |
| |
| private boolean checkBoolean(String key, Map map) |
| { |
| String value = findValueCaselessly(key, map); |
| |
| if (value == null) |
| return false; |
| |
| return value.equalsIgnoreCase("true"); |
| } |
| |
| /** |
| * Gets the current location within the file. This allows the location to be |
| * created only as needed, and multiple objects on the same line can share |
| * the same Location instance. |
| * |
| * @since 3.0 |
| * |
| **/ |
| |
| protected ILocation getCurrentLocation() |
| { |
| if (_currentLocation == null) |
| _currentLocation = new Location(_resourceLocation, _line); |
| |
| return _currentLocation; |
| } |
| } |