You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2006/01/12 18:13:34 UTC

svn commit: r368410 [6/10] - in /jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry: ./ bean/ binding/ callback/ coerce/ components/ describe/ engine/ engine/state/ enhance/ error/ event/ form/ form/translator/ form/validator/ html/ link/ li...

Modified: jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TemplateParser.java
URL: http://svn.apache.org/viewcvs/jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TemplateParser.java?rev=368410&r1=368409&r2=368410&view=diff
==============================================================================
--- jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TemplateParser.java (original)
+++ jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TemplateParser.java Thu Jan 12 09:12:21 2006
@@ -1,4 +1,4 @@
-// Copyright 2004, 2005 The Apache Software Foundation
+// Copyright 2004, 2005, 2006 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.
@@ -35,198 +35,188 @@
 
 /**
  * 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.
+ * {@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>
- * Deployed as the tapestry.parse.TemplateParser service, using the threaded model.
+ * Deployed as the tapestry.parse.TemplateParser service, using the threaded
+ * model.
  * <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>/&gt;</code>".
+ * 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>/&gt;</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).
+ * 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).
+ *       (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
+ *       <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.
+ *       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
+ *       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.
+ *       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.
+ *       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>&lt;span key="<i>value</i>"&gt; ... &lt;/span&gt;</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.
+ *       The parser supports <em>invisible localization</em>: The parser
+ *       recognizes HTML of the form:
+ *       <code>&lt;span key="<i>value</i>"&gt; ... &lt;/span&gt;</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
  */
 
 public class TemplateParser implements ITemplateParser
 {
-    /**
-     * 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 &lt;span&gt; tags, that signfies that the span is being used as
-     * an invisible localization.
+     * The attribute, checked for in &lt;span&gt; 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";
+    public static final String PROPERTY_NAME_PATTERN = "_?[a-zA-Z]\\w*";
 
     /**
-     * Attribute name used to identify components.
+     * Pattern used to recognize ordinary components (defined in the
+     * specification).
      * 
-     * @since 4.0
+     * @since 3.0
      */
 
-    private String _componentAttributeName;
-
-    private static final String PROPERTY_NAME_PATTERN = "_?[a-zA-Z]\\w*";
+    public static final String SIMPLE_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")$";
 
     /**
-     * Pattern used to recognize ordinary components (defined in the specification).
+     * 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 3.0
+     * @since 2.3
      */
 
-    public static final String SIMPLE_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")$";
+    public static final String RAW_ATTRIBUTE_NAME = "raw";
 
     /**
-     * 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, which may (as of 4.0) have slashes to delinate folders containing the component.
+     * 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, which may (as of
+     * 4.0) have slashes to delinate folders containing the component.
      * 
      * @since 3.0
      */
 
-    public static final String IMPLICIT_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")?@((("
-            + PROPERTY_NAME_PATTERN + "):)?((" + PROPERTY_NAME_PATTERN + "/)*"
-            + PROPERTY_NAME_PATTERN + "))$";
+    public static final String IMPLICIT_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")?@(((" + PROPERTY_NAME_PATTERN
+            + "):)?((" + PROPERTY_NAME_PATTERN + "/)*" + PROPERTY_NAME_PATTERN + "))$";
 
-    private static final int IMPLICIT_ID_PATTERN_ID_GROUP = 1;
+    private static final int WAIT_FOR_ATTRIBUTE_NAME = 0;
 
-    private static final int IMPLICIT_ID_PATTERN_TYPE_GROUP = 2;
+    private static final int WAIT_FOR_ATTRIBUTE_VALUE = 3;
 
-    private static final int IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP = 4;
+    /**
+     * A "magic" component id that causes the tag with the id and its entire
+     * body to be ignored during parsing.
+     */
 
-    private static final int IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP = 5;
+    private static final String REMOVE_ID = "$remove$";
+    private static final int ADVANCE_PAST_EQUALS = 2;
 
-    private Pattern _simpleIdPattern;
+    private static final char[] CLOSE_TAG = new char[] { '<', '/' };
 
-    private Pattern _implicitIdPattern;
+    private static final int COLLECT_ATTRIBUTE_NAME = 1;
 
-    private PatternMatcher _patternMatcher;
+    private static final int COLLECT_QUOTED_VALUE = 4;
 
-    private IdAllocator _idAllocator = new IdAllocator();
+    private static final int COLLECT_UNQUOTED_VALUE = 5;
 
-    private ITemplateParserDelegate _delegate;
+    private static final char[] COMMENT_END = new char[] { '-', '-', '>' };
+
+    private static final char[] COMMENT_START = new char[] { '<', '!', '-', '-' };
 
     /**
-     * Identifies the template being parsed; used with error messages.
+     * 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 Resource _resourceLocation;
+    private static final String CONTENT_ID = "$content$";
 
     /**
-     * Shared instance of {@link Location} used by all {@link TextToken} instances in the template.
+     * Conversions needed by {@link #convertEntitiesToPlain(String)}.
      */
 
-    private Location _templateLocation;
+    private static final String[] CONVERSIONS = { "&lt;", "<", "&gt;", ">", "&quot;", "\"", "&amp;", "&" };
 
-    /**
-     * Location with in the resource for the current line.
-     */
+    private static final int IMPLICIT_ID_PATTERN_ID_GROUP = 1;
 
-    private Location _currentLocation;
+    private static final int IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP = 4;
 
-    /**
-     * Local reference to the template data that is to be parsed.
-     */
+    private static final int IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP = 5;
 
-    private char[] _templateData;
+    private static final int IMPLICIT_ID_PATTERN_TYPE_GROUP = 2;
 
     /**
-     * List of Tag
+     * Stores details about a parsed tag, used to match start and end tags and
+     * control other parser processing.
      */
-
-    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, then the parse ends when the closing tag is found.
+        boolean _content;
+
         // 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;
+        // The line on which the start tag exists
+        int _line;
 
         // 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 entire tag (and its body) is being ignored
+        boolean _removeTag;
 
-        // If true, then the parse ends when the closing tag is found.
-        boolean _content;
+        // The element, i.e., <jwc> or virtually any other element (via jwcid
+        // attribute)
+        String _tagName;
 
         Tag(String tagName, int line)
         {
@@ -241,23 +231,59 @@
     }
 
     /**
-     * List of {@link TemplateToken}, this forms the ultimate response.
+     * A {@link Map}of {@link String}s, used to store attributes collected
+     * while parsing a tag.
      */
 
-    private List _tokens = new ArrayList();
+    private Map _attributes = new HashMap();
+
+    /**
+     * The start of the current block of static text, or -1 if no block is
+     * active.
+     */
+
+    private int _blockStart;
+
+    /**
+     * Attribute name used to identify components.
+     * 
+     * @since 4.0
+     */
+
+    private String _componentAttributeName;
+
+    /**
+     * Location with in the resource for the current line.
+     */
+
+    private Location _currentLocation;
 
     /**
-     * The location of the 'cursor' within the template data. The advance() method moves this
-     * forward.
+     * The location of the 'cursor' within the template data. The advance()
+     * method moves this forward.
      */
 
     private int _cursor;
 
+    private ITemplateParserDelegate _delegate;
+
     /**
-     * The start of the current block of static text, or -1 if no block is active.
+     * A factory used to create template tokens.
      */
 
-    private int _blockStart;
+    private TemplateTokenFactory _factory;
+
+    private IdAllocator _idAllocator = new IdAllocator();
+
+    /**
+     * 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;
+
+    private Pattern _implicitIdPattern;
 
     /**
      * The current line number; tracked by advance(). Starts at 1.
@@ -265,25 +291,40 @@
 
     private int _line;
 
+    private PatternMatcher _patternMatcher;
+
     /**
-     * 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.
+     * Identifies the template being parsed; used with error messages.
      */
 
-    private boolean _ignoring;
+    private Resource _resourceLocation;
+
+    private Pattern _simpleIdPattern;
 
     /**
-     * A {@link Map}of {@link String}s, used to store attributes collected while parsing a tag.
+     * List of Tag.
      */
 
-    private Map _attributes = new HashMap();
+    private List _stack = new ArrayList();
 
     /**
-     * A factory used to create template tokens.
+     * Local reference to the template data that is to be parsed.
      */
 
-    private TemplateTokenFactory _factory;
+    private char[] _templateData;
+
+    /**
+     * Shared instance of {@link Location} used by all {@link TextToken}
+     * instances in the template.
+     */
+
+    private Location _templateLocation;
+
+    /**
+     * List of {@link TemplateToken}, this forms the ultimate response.
+     */
+
+    private List _tokens = new ArrayList();
 
     public TemplateParser()
     {
@@ -303,50 +344,124 @@
     }
 
     /**
-     * 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.
+     * @throws TemplateParseException
+     * @since 4.0
+     */
+
+    private void addAttributeIfUnique(String tagName, String attributeName, String attributeValue)
+        throws TemplateParseException
+    {
+
+        if (_attributes.containsKey(attributeName))
+            templateParseProblem(ParseMessages.duplicateTagAttribute(tagName, _line, attributeName),
+                    getCurrentLocation(), _line, _cursor);
+
+        _attributes.put(attributeName, attributeValue);
+    }
+
+    /**
+     * Adds the attribute to the token (identifying prefixes and whatnot is now
+     * done downstream).
      * 
-     * @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.
+     * @since 3.0
      */
 
-    public TemplateToken[] parse(char[] templateData, ITemplateParserDelegate delegate,
-            Resource resourceLocation) throws TemplateParseException
+    private void addAttributeToToken(OpenToken token, String name, String attributeValue)
     {
-        try
+        token.addAttribute(name, convertEntitiesToPlain(attributeValue));
+    }
+
+    private void addOpenToken(String tagName, String jwcId, String type, Location location)
+    {
+        OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
+        _tokens.add(token);
+
+        if (_attributes.isEmpty()) return;
+
+        Iterator i = _attributes.entrySet().iterator();
+        while(i.hasNext())
         {
-            beforeParse(templateData, delegate, resourceLocation);
+            Map.Entry entry = (Map.Entry)i.next();
 
-            parse();
+            String key = (String)entry.getKey();
 
-            return (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]);
+            if (key.equalsIgnoreCase(_componentAttributeName)) continue;
+
+            String value = (String)entry.getValue();
+
+            addAttributeToToken(token, key, value);
         }
-        finally
+    }
+
+    private void addTextToken(int end)
+    {
+        // No active block to add to.
+
+        if (_blockStart < 0) return;
+
+        if (_blockStart <= end)
         {
-            afterParse();
+            // This seems odd, shouldn't the location be the current location? I
+            // guess
+            // no errors are ever reported for a text token.
+
+            TemplateToken token = _factory.createTextToken(_templateData, _blockStart, end, _templateLocation);
+
+            _tokens.add(token);
         }
+
+        _blockStart = -1;
     }
 
     /**
-     * perform default initialization of the parser.
+     * Advances the cursor to the next character. If the end-of-line is reached,
+     * then increments the line counter.
      */
 
-    protected void beforeParse(char[] templateData, ITemplateParserDelegate delegate,
-            Resource resourceLocation)
+    private void advance()
     {
-        _templateData = templateData;
-        _resourceLocation = resourceLocation;
-        _templateLocation = new LocationImpl(resourceLocation);
-        _delegate = delegate;
-        _ignoring = false;
-        _line = 1;
-        _componentAttributeName = delegate.getComponentAttributeName();
+        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();
+        }
     }
 
     /**
@@ -367,51 +482,281 @@
     }
 
     /**
-     * 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.
+     * Notify that the beginning of an attribute value has been detected.
      * <p>
-     * Subclasses may override but <b>must </b> ensure they throw the required exception.
-     * 
-     * @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.
+     * Default implementation does nothing.
      */
-
-    protected void templateParseProblem(String message, Location location, int line, int cursor)
-            throws TemplateParseException
+    protected void attributeBeginEvent(String attributeName, int startLine, int cursorPosition)
     {
-        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.
+     * Notify that the end of the current attribute value has been detected.
      * <p>
-     * The default implementation simply rethrows the exception.
+     * Default implementation does nothing.
+     */
+    protected void attributeEndEvent(int cursorPosition)
+    {
+    }
+
+    /**
+     * perform default initialization of the parser.
+     */
+
+    protected void beforeParse(char[] templateData, ITemplateParserDelegate delegate, Resource resourceLocation)
+    {
+        _templateData = templateData;
+        _resourceLocation = resourceLocation;
+        _templateLocation = new LocationImpl(resourceLocation);
+        _delegate = delegate;
+        _ignoring = false;
+        _line = 1;
+        _componentAttributeName = delegate.getComponentAttributeName();
+    }
+
+    /**
+     * 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");
+    }
+
+    /**
+     * Invoked to handle a closing tag, i.e., &lt;/foo&gt;. 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>
-     * Subclasses may override but <b>must </b> ensure they rethrow the exception.
+     * 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 '&lt;' 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 '&gt;'.
+     */
+
+    private void closeTag()
+        throws TemplateParseException
+    {
+        int cursorStart = _cursor;
+        int length = _templateData.length;
+        int startLine = _line;
+
+        Location startLocation = getCurrentLocation();
+
+        _cursor += CLOSE_TAG.length;
+
+        int tagStart = _cursor;
+
+        while(true)
+        {
+            if (_cursor >= length)
+                templateParseProblem(ParseMessages.incompleteCloseTag(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(ParseMessages
+                        .improperlyNestedCloseTag(tagName, startLine, tag._tagName, tag._line), startLocation,
+                        startLine, cursorStart);
+
+            stackPos--;
+        }
+
+        if (stackPos < 0)
+            templateParseProblem(ParseMessages.unmatchedCloseTag(tagName, 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;
+    }
+
+    /**
+     * Processes a tag that is the open tag for a component (but also handles
+     * the $remove$ and $content$ tags).
+     */
+
+    /**
+     * Provided a raw input string that has been recognized to be an expression,
+     * this removes excess white space and converts &amp;amp;;, &amp;quot;;
+     * &amp;lt;; and &amp;gt;; to their normal character values (otherwise its
+     * impossible to specify those values in expressions in the template).
+     */
+
+    private String convertEntitiesToPlain(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 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;
+    }
+
+    /**
+     * 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.
      * 
-     * @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.
+     * @since 3.0
      */
 
-    protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor)
-            throws ApplicationRuntimeException
+    protected Location getCurrentLocation()
     {
-        throw exception;
+        if (_currentLocation == null) _currentLocation = new LocationImpl(_resourceLocation, _line);
+
+        return _currentLocation;
     }
 
     /**
@@ -419,8 +764,7 @@
      */
     protected List getTokens()
     {
-        if (_tokens == null)
-            return Collections.EMPTY_LIST;
+        if (_tokens == null) return Collections.EMPTY_LIST;
 
         return _tokens;
     }
@@ -433,10 +777,9 @@
     {
         try
         {
-            for (int i = 0; i < match.length; i++)
+            for(int i = 0; i < match.length; i++)
             {
-                if (_templateData[_cursor + i] != match[i])
-                    return false;
+                if (_templateData[_cursor + i] != match[i]) return false;
             }
 
             // Every character matched.
@@ -449,27 +792,18 @@
         }
     }
 
-    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
+    protected void parse()
+        throws TemplateParseException
     {
         _cursor = 0;
         _blockStart = -1;
         int length = _templateData.length;
 
-        while (_cursor < length)
+        while(_cursor < length)
         {
             if (_templateData[_cursor] != '<')
             {
-                if (_blockStart < 0 && !_ignoring)
-                    _blockStart = _cursor;
+                if (_blockStart < 0 && !_ignoring) _blockStart = _cursor;
 
                 advance();
                 continue;
@@ -494,7 +828,8 @@
             startTag();
         }
 
-        // Usually there's some text at the end of the template (after the last closing tag) that
+        // 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.
@@ -503,73 +838,220 @@
     }
 
     /**
-     * Advance forward in the document until the end of the comment is reached. In addition, skip
-     * any whitespace following the comment.
+     * 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.
      */
 
-    private void skipComment() throws TemplateParseException
+    public TemplateToken[] parse(char[] templateData, ITemplateParserDelegate delegate, Resource resourceLocation)
+        throws TemplateParseException
     {
-        int length = _templateData.length;
-        int startLine = _line;
-
-        if (_blockStart < 0 && !_ignoring)
-            _blockStart = _cursor;
-
-        while (true)
+        try
         {
-            if (_cursor >= length)
-                templateParseProblem(ParseMessages.commentNotEnded(startLine), new LocationImpl(
-                        _resourceLocation, startLine), startLine, _cursor);
-
-            if (lookahead(COMMENT_END))
-                break;
+            beforeParse(templateData, delegate, resourceLocation);
 
-            // Not the end of the comment, advance over it.
+            parse();
 
-            advance();
+            return (TemplateToken[])_tokens.toArray(new TemplateToken[_tokens.size()]);
+        }
+        finally
+        {
+            afterParse();
         }
-
-        _cursor += COMMENT_END.length;
-        advanceOverWhitespace();
     }
 
-    private void addTextToken(int end)
+    private void processComponentStart(String tagName, String jwcId, boolean emptyTag, int startLine, int cursorStart,
+            Location startLocation)
+        throws TemplateParseException
     {
-        // No active block to add to.
-
-        if (_blockStart < 0)
-            return;
-
-        if (_blockStart <= end)
+        if (jwcId.equalsIgnoreCase(CONTENT_ID))
         {
-            // This seems odd, shouldn't the location be the current location? I guess
-            // no errors are ever reported for a text token.
-
-            TemplateToken token = _factory.createTextToken(
-                    _templateData,
-                    _blockStart,
-                    end,
-                    _templateLocation);
+            processContentTag(tagName, startLine, cursorStart, emptyTag);
 
-            _tokens.add(token);
+            return;
         }
 
-        _blockStart = -1;
-    }
+        boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
 
-    private static final int WAIT_FOR_ATTRIBUTE_NAME = 0;
+        if (_ignoring && !isRemoveId)
+            templateParseProblem(ParseMessages.componentMayNotBeIgnored(tagName, startLine), startLocation, startLine,
+                    cursorStart);
 
-    private static final int COLLECT_ATTRIBUTE_NAME = 1;
+        String type = null;
+        boolean allowBody = false;
 
-    private static final int ADVANCE_PAST_EQUALS = 2;
+        if (_patternMatcher.matches(jwcId, _implicitIdPattern))
+        {
+            MatchResult match = _patternMatcher.getMatch();
 
-    private static final int WAIT_FOR_ATTRIBUTE_VALUE = 3;
+            jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
+            type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);
 
-    private static final int COLLECT_QUOTED_VALUE = 4;
+            String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
+            String simpleType = match.group(IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP);
 
-    private static final int COLLECT_UNQUOTED_VALUE = 5;
+            // 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).
+            // New for 4.0: the component type may included slashes ('/'), but
+            // these
+            // are not valid identifiers, so we convert them to '$'.
+
+            if (jwcId == null) jwcId = _idAllocator.allocateId("$" + simpleType.replace('/', '$'));
+
+            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(ParseMessages.componentIdInvalid(tagName, startLine, jwcId), startLocation,
+                            startLine, cursorStart);
+
+                if (!_delegate.getKnownComponent(jwcId))
+                    templateParseProblem(ParseMessages.unknownComponentId(tagName, 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(ParseMessages.nestedIgnore(tagName, startLine), new LocationImpl(_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 processContentTag(String tagName, int startLine, int cursorStart, boolean emptyTag)
+        throws TemplateParseException
+    {
+        if (_ignoring)
+            templateParseProblem(ParseMessages.contentBlockMayNotBeIgnored(tagName, startLine), new LocationImpl(
+                    _resourceLocation, startLine), startLine, cursorStart);
+
+        if (emptyTag)
+            templateParseProblem(ParseMessages.contentBlockMayNotBeEmpty(tagName, startLine), new LocationImpl(
+                    _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 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);
+    }
+
+    public void setFactory(TemplateTokenFactory factory)
+    {
+        _factory = factory;
+    }
+
+    /**
+     * 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(ParseMessages.commentNotEnded(startLine), new LocationImpl(_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 startTag() throws TemplateParseException
+    private void startTag()
+        throws TemplateParseException
     {
         int cursorStart = _cursor;
         int length = _templateData.length;
@@ -585,7 +1067,7 @@
 
         // Collect the element type
 
-        while (_cursor < length)
+        while(_cursor < length)
         {
             char ch = _templateData[_cursor];
 
@@ -609,185 +1091,189 @@
 
         // Collect each attribute
 
-        while (!endOfTag)
+        while(!endOfTag)
         {
             if (_cursor >= length)
             {
-                String message = (tagName == null) ? ParseMessages.unclosedUnknownTag(startLine)
-                        : ParseMessages.unclosedTag(tagName, startLine);
+                String message = (tagName == null) ? ParseMessages.unclosedUnknownTag(startLine) : ParseMessages
+                        .unclosedTag(tagName, startLine);
 
                 templateParseProblem(message, startLocation, startLine, cursorStart);
             }
 
             char ch = _templateData[_cursor];
 
-            switch (state)
+            switch(state)
             {
-                case WAIT_FOR_ATTRIBUTE_NAME:
+            case WAIT_FOR_ATTRIBUTE_NAME:
 
-                    // Ignore whitespace before the next attribute name, while
-                    // looking for the end of the current tag.
+                // 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;
-                    }
+                if (ch == '/')
+                {
+                    emptyTag = true;
+                    advance();
+                    break;
+                }
 
-                    // Found non-whitespace, assume its the attribute name.
-                    // Note: could use a check here for non-alpha.
+                if (ch == '>')
+                {
+                    endOfTag = true;
+                    break;
+                }
 
-                    attributeNameStart = _cursor;
-                    state = COLLECT_ATTRIBUTE_NAME;
+                if (Character.isWhitespace(ch))
+                {
                     advance();
                     break;
+                }
 
-                case COLLECT_ATTRIBUTE_NAME:
+                // Found non-whitespace, assume its the attribute name.
+                // Note: could use a check here for non-alpha.
 
-                    // Looking for end of attribute name.
+                attributeNameStart = _cursor;
+                state = COLLECT_ATTRIBUTE_NAME;
+                advance();
+                break;
 
-                    if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
-                    {
-                        attributeName = new String(_templateData, attributeNameStart, _cursor
-                                - attributeNameStart);
+            case COLLECT_ATTRIBUTE_NAME:
 
-                        state = ADVANCE_PAST_EQUALS;
-                        break;
-                    }
+                // Looking for end of attribute name.
 
-                    // Part of the attribute name
+                if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
+                {
+                    attributeName = new String(_templateData, attributeNameStart, _cursor - attributeNameStart);
 
-                    advance();
+                    state = ADVANCE_PAST_EQUALS;
                     break;
+                }
+
+                // Part of the attribute name
+
+                advance();
+                break;
 
-                case ADVANCE_PAST_EQUALS:
+            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).
+                // 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).
 
-                case WAIT_FOR_ATTRIBUTE_VALUE:
+                state = WAIT_FOR_ATTRIBUTE_NAME;
+                break;
+
+            case WAIT_FOR_ATTRIBUTE_VALUE:
+
+                if (ch == '/' || ch == '>')
+                    templateParseProblem(ParseMessages.missingAttributeValue(tagName, _line, attributeName),
+                            getCurrentLocation(), _line, _cursor);
+
+                // Ignore whitespace between '=' and the attribute value. Also,
+                // look
+                // for initial quote.
 
-                    if (ch == '/' || ch == '>')
-                        templateParseProblem(ParseMessages.missingAttributeValue(
-                                tagName,
-                                _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;
-                    }
+                if (Character.isWhitespace(ch))
+                {
+                    advance();
+                    break;
+                }
 
-                    // Not whitespace or quote, must be start of unquoted attribute.
+                if (ch == '\'' || ch == '"')
+                {
+                    quoteChar = ch;
 
-                    state = COLLECT_UNQUOTED_VALUE;
+                    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.
 
-                case COLLECT_QUOTED_VALUE:
+                if (ch == quoteChar)
+                {
+                    String attributeValue = new String(_templateData, attributeValueStart, _cursor
+                            - attributeValueStart);
+
+                    attributeEndEvent(_cursor);
 
-                    // 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);
-
-                        addAttributeIfUnique(tagName, attributeName, attributeValue);
-
-                        // Advance over the quote.
-                        advance();
-                        state = WAIT_FOR_ATTRIBUTE_NAME;
-                        break;
-                    }
+                    addAttributeIfUnique(tagName, attributeName, attributeValue);
 
+                    // Advance over the quote.
                     advance();
+                    state = WAIT_FOR_ATTRIBUTE_NAME;
                     break;
+                }
 
-                case COLLECT_UNQUOTED_VALUE:
+                advance();
+                break;
 
-                    // An unquoted attribute value ends with whitespace
-                    // or the end of the enclosing tag.
+            case COLLECT_UNQUOTED_VALUE:
 
-                    if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
-                    {
-                        String attributeValue = new String(_templateData, attributeValueStart,
-                                _cursor - attributeValueStart);
-
-                        attributeEndEvent(_cursor);
-                        addAttributeIfUnique(tagName, attributeName, attributeValue);
-
-                        state = WAIT_FOR_ATTRIBUTE_NAME;
-                        break;
-                    }
+                // An unquoted attribute value ends with whitespace
+                // or the end of the enclosing tag.
 
-                    advance();
+                if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
+                {
+                    String attributeValue = new String(_templateData, attributeValueStart, _cursor
+                            - attributeValueStart);
+
+                    attributeEndEvent(_cursor);
+                    addAttributeIfUnique(tagName, attributeName, attributeValue);
+
+                    state = WAIT_FOR_ATTRIBUTE_NAME;
                     break;
+                }
+
+                advance();
+
+            default:
+                break;
             }
         }
 
@@ -801,11 +1287,8 @@
         if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null)
         {
             if (_ignoring)
-                templateParseProblem(
-                        ParseMessages.componentMayNotBeIgnored(tagName, startLine),
-                        startLocation,
-                        startLine,
-                        cursorStart);
+                templateParseProblem(ParseMessages.componentMayNotBeIgnored(tagName, startLine), startLocation,
+                        startLine, cursorStart);
 
             // If the tag isn't empty, then create a Tag instance to ignore the
             // body of the tag.
@@ -838,14 +1321,9 @@
 
             boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes);
 
-            Map attributes = filter(_attributes, new String[]
-            { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME });
+            Map attributes = filter(_attributes, new String[] { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME });
 
-            TemplateToken token = _factory.createLocalizationToken(
-                    tagName,
-                    localizationKey,
-                    raw,
-                    attributes,
+            TemplateToken token = _factory.createLocalizationToken(tagName, localizationKey, raw, attributes,
                     startLocation);
 
             _tokens.add(token);
@@ -870,37 +1348,12 @@
 
         // If there wasn't an active block, then start one.
 
-        if (_blockStart < 0 && !_ignoring)
-            _blockStart = cursorStart;
+        if (_blockStart < 0 && !_ignoring) _blockStart = cursorStart;
 
         advance();
     }
 
     /**
-     * @throws TemplateParseException
-     * @since 4.0
-     */
-
-    private void addAttributeIfUnique(String tagName, String attributeName, String attributeValue)
-            throws TemplateParseException
-    {
-
-        if (_attributes.containsKey(attributeName))
-            templateParseProblem(
-                    ParseMessages.duplicateTagAttribute(tagName, _line, attributeName),
-                    getCurrentLocation(),
-                    _line,
-                    _cursor);
-
-        _attributes.put(attributeName, attributeValue);
-    }
-
-    /**
-     * 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.
@@ -919,541 +1372,52 @@
     }
 
     /**
-     * Notify that the beginning of an attribute value has been detected.
+     * Used by the parser to report tapestry runtime specific problems in the
+     * parse. Parsing <b>must </b> stop when a problem is reported.
      * <p>
-     * Default implementation does nothing.
+     * The default implementation simply rethrows the exception.
+     * <p>
+     * Subclasses may override but <b>must </b> ensure they rethrow the
+     * exception.
+     * 
+     * @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 attributeBeginEvent(String attributeName, int startLine, int cursorPosition)
+
+    protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor)
     {
+        throw exception;
     }
 
     /**
-     * Notify that the end of the current attribute value has been detected.
+     * Used by the parser to report problems in the parse. Parsing <b>must </b>
+     * stop when a problem is reported.
      * <p>
-     * Default implementation does nothing.
+     * 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.
+     * 
+     * @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 attributeEndEvent(int cursorPosition)
-    {
-    }
 
-    private void processComponentStart(String tagName, String jwcId, boolean emptyTag,
-            int startLine, int cursorStart, Location startLocation) throws TemplateParseException
+    protected void templateParseProblem(String message, Location location, int line, int cursor)
+        throws TemplateParseException
     {
-        if (jwcId.equalsIgnoreCase(CONTENT_ID))
-        {
-            processContentTag(tagName, startLine, cursorStart, emptyTag);
-
-            return;
-        }
-
-        boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
-
-        if (_ignoring && !isRemoveId)
-            templateParseProblem(
-                    ParseMessages.componentMayNotBeIgnored(tagName, 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).
-            // New for 4.0: the component type may included slashes ('/'), but these
-            // are not valid identifiers, so we convert them to '$'.
-
-            if (jwcId == null)
-                jwcId = _idAllocator.allocateId("$" + simpleType.replace('/', '$'));
-
-            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(
-                            ParseMessages.componentIdInvalid(tagName, startLine, jwcId),
-                            startLocation,
-                            startLine,
-                            cursorStart);
-
-                if (!_delegate.getKnownComponent(jwcId))
-                    templateParseProblem(
-                            ParseMessages.unknownComponentId(tagName, 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(ParseMessages.nestedIgnore(tagName, startLine), new LocationImpl(
-                    _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(
-                    ParseMessages.contentBlockMayNotBeIgnored(tagName, startLine),
-                    new LocationImpl(_resourceLocation, startLine),
-                    startLine,
-                    cursorStart);
-
-        if (emptyTag)
-            templateParseProblem(
-                    ParseMessages.contentBlockMayNotBeEmpty(tagName, startLine),
-                    new LocationImpl(_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, Location 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(_componentAttributeName))
-                continue;
-
-            String value = (String) entry.getValue();
-
-            addAttributeToToken(token, key, value);
-        }
-    }
-
-    /**
-     * Adds the attribute to the token (identifying prefixes and whatnot is now done downstream).
-     * 
-     * @since 3.0
-     */
-
-    private void addAttributeToToken(OpenToken token, String name, String attributeValue)
-    {
-        token.addAttribute(name, convertEntitiesToPlain(attributeValue));
-    }
-
-    /**
-     * Invoked to handle a closing tag, i.e., &lt;/foo&gt;. 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 '&lt;' 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 '&gt;'.
-     */
-
-    private void closeTag() throws TemplateParseException
-    {
-        int cursorStart = _cursor;
-        int length = _templateData.length;
-        int startLine = _line;
-
-        Location startLocation = getCurrentLocation();
-
-        _cursor += CLOSE_TAG.length;
-
-        int tagStart = _cursor;
-
-        while (true)
-        {
-            if (_cursor >= length)
-                templateParseProblem(
-                        ParseMessages.incompleteCloseTag(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(ParseMessages.improperlyNestedCloseTag(
-                        tagName,
-                        startLine,
-                        tag._tagName,
-                        tag._line), startLocation, startLine, cursorStart);
-
-            stackPos--;
-        }
-
-        if (stackPos < 0)
-            templateParseProblem(
-                    ParseMessages.unmatchedCloseTag(tagName, 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 #convertEntitiesToPlain(String)}
-     */
-
-    private static final String[] CONVERSIONS =
-    { "&lt;", "<", "&gt;", ">", "&quot;", "\"", "&amp;", "&" };
-
-    /**
-     * Provided a raw input string that has been recognized to be an expression, this removes excess
-     * white space and converts &amp;amp;;, &amp;quot;; &amp;lt;; and &amp;gt;; to their normal
-     * character values (otherwise its impossible to specify those values in expressions in the
-     * template).
-     */
-
-    private String convertEntitiesToPlain(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 Location getCurrentLocation()
-    {
-        if (_currentLocation == null)
-            _currentLocation = new LocationImpl(_resourceLocation, _line);
-
-        return _currentLocation;
-    }
-
-    public void setFactory(TemplateTokenFactory factory)
-    {
-        _factory = factory;
+        throw new TemplateParseException(message, location);
     }
 
-}
\ No newline at end of file
+}

Modified: jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TextToken.java
URL: http://svn.apache.org/viewcvs/jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TextToken.java?rev=368410&r1=368409&r2=368410&view=diff
==============================================================================
--- jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TextToken.java (original)
+++ jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TextToken.java Thu Jan 12 09:12:21 2006
@@ -1,4 +1,4 @@
-// Copyright 2004, 2005 The Apache Software Foundation
+// Copyright 2004, 2005, 2006 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.
@@ -22,8 +22,8 @@
 import org.apache.tapestry.IRequestCycle;
 
 /**
- * Represents static text in the template that may be passed through to the client unchanged
- * (except, perhaps, for the removal of some whitespace).
+ * Represents static text in the template that may be passed through to the
+ * client unchanged (except, perhaps, for the removal of some whitespace).
  * 
  * @see TokenType#TEXT
  * @author Howard Lewis Ship
@@ -32,6 +32,7 @@
 
 public class TextToken extends TemplateToken implements IRender
 {
+
     private char[] _templateData;
 
     private int _offset;
@@ -42,11 +43,9 @@
     {
         super(TokenType.TEXT, location);
 
-        if (startIndex < 0 || endIndex < 0 || startIndex > templateData.length
-                || endIndex > templateData.length)
-            throw new ApplicationRuntimeException(ParseMessages.rangeError(
-                    this,
-                    templateData.length), this, getLocation(), null);
+        if (startIndex < 0 || endIndex < 0 || startIndex > templateData.length || endIndex > templateData.length)
+            throw new ApplicationRuntimeException(ParseMessages.rangeError(this, templateData.length), this,
+                    getLocation(), null);
 
         _templateData = templateData;
 
@@ -56,14 +55,14 @@
 
     public void render(IMarkupWriter writer, IRequestCycle cycle)
     {
-        if (_length == 0)
-            return;
+        if (_length == 0) return;
 
         // At one time, we would check to see if the cycle was rewinding and
         // only invoke printRaw() if it was. However, that slows down
         // normal rendering (microscopically) and, with the new
         // NullResponseWriter class, the "cost" of invoking cycle.isRewinding()
-        // is approximately the same as the "cost" of invoking writer.printRaw().
+        // is approximately the same as the "cost" of invoking
+        // writer.printRaw().
 
         writer.printRaw(_templateData, _offset, _length);
     }
@@ -88,4 +87,4 @@
     {
         return _offset;
     }
-}
\ No newline at end of file
+}

Modified: jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TokenType.java
URL: http://svn.apache.org/viewcvs/jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TokenType.java?rev=368410&r1=368409&r2=368410&view=diff
==============================================================================
--- jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TokenType.java (original)
+++ jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/parse/TokenType.java Thu Jan 12 09:12:21 2006
@@ -1,4 +1,4 @@
-// Copyright 2004, 2005 The Apache Software Foundation
+// Copyright 2004, 2005, 2006 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.
@@ -21,8 +21,9 @@
  * @author Howard Lewis Ship
  */
 
-public class TokenType
+public final class TokenType
 {
+
     /**
      * Raw HTML text.
      * 
@@ -71,4 +72,4 @@
     {
         return _name;
     }
-}
\ No newline at end of file
+}

Modified: jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/record/RecordMessages.java
URL: http://svn.apache.org/viewcvs/jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/record/RecordMessages.java?rev=368410&r1=368409&r2=368410&view=diff
==============================================================================
--- jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/record/RecordMessages.java (original)
+++ jakarta/tapestry/trunk/framework/src/java/org/apache/tapestry/record/RecordMessages.java Thu Jan 12 09:12:21 2006
@@ -1,4 +1,4 @@
-// Copyright 2005 The Apache Software Foundation
+// Copyright 2005, 2006 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.
@@ -14,6 +14,7 @@
 
 package org.apache.tapestry.record;
 
+import org.apache.hivemind.Messages;
 import org.apache.hivemind.impl.MessageFormatter;
 import org.apache.tapestry.IComponent;
 
@@ -21,38 +22,44 @@
  * @author Howard M. Lewis Ship
  * @since 4.0
  */
-class RecordMessages
+final class RecordMessages
 {
-    private final static MessageFormatter _formatter = new MessageFormatter(RecordMessages.class);
+
+    private final static Messages MESSAGES = new MessageFormatter(RecordMessages.class);
+
+    /** @since 4.1 */
+    private RecordMessages()
+    {
+    }
 
     static String unknownPersistenceStrategy(String name)
     {
-        return _formatter.format("unknown-persistence-strategy", name);
+        return MESSAGES.format("unknown-persistence-strategy", name);
     }
 
     static String missingPropertySpecification(String propertyName, IComponent component)
     {
-        return _formatter.format("missing-property-specification", propertyName, component
-                .getExtendedId(), component.getSpecification().getSpecificationLocation());
+        return MESSAGES.format("missing-property-specification", propertyName, component.getExtendedId(), component
+                .getSpecification().getSpecificationLocation());
     }
 
     static String recorderLocked(String propertyName, IComponent component)
     {
-        return _formatter.format("recorder-locked", propertyName, component.getExtendedId());
+        return MESSAGES.format("recorder-locked", propertyName, component.getExtendedId());
     }
 
     static String decodeFailure(Throwable cause)
     {
-        return _formatter.format("decode-failure", cause);
+        return MESSAGES.format("decode-failure", cause);
     }
 
     static String encodeFailure(Throwable cause)
     {
-        return _formatter.format("encode-failure", cause);
+        return MESSAGES.format("encode-failure", cause);
     }
 
     static String unknownPrefix(String prefix)
     {
-        return _formatter.format("unknown-prefix", prefix);
+        return MESSAGES.format("unknown-prefix", prefix);
     }
-}
\ No newline at end of file
+}



---------------------------------------------------------------------
To unsubscribe, e-mail: tapestry-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: tapestry-dev-help@jakarta.apache.org