//  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>/&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).
 * 
 *  <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>&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
 *  @version $Id: TemplateParser.java 244412 2005-07-23 03:32:35Z glongman $
 * 
 **/

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 &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";

    /**
     *  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., &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;

        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 = { "&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 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;
    }
}