Clover coverage report - Code Coverage for tapestry release 4.0-beta-2
Coverage timestamp: Sat Jul 9 2005 22:02:17 EDT
file stats: LOC: 1,436   Methods: 32
NCLOC: 744   Classes: 2
30 day Evaluation License registered to hlship@comcast.net Your 30 day evaluation period has expired. Please visit http://www.cenqua.com to obtain a licensed version of Clover
 
 Source file Conditionals Statements Methods TOTAL
TemplateParser.java 94% 96.7% 93.8% 95.8%
coverage coverage
 1    // Copyright 2004, 2005 The Apache Software Foundation
 2    //
 3    // Licensed under the Apache License, Version 2.0 (the "License");
 4    // you may not use this file except in compliance with the License.
 5    // You may obtain a copy of the License at
 6    //
 7    // http://www.apache.org/licenses/LICENSE-2.0
 8    //
 9    // Unless required by applicable law or agreed to in writing, software
 10    // distributed under the License is distributed on an "AS IS" BASIS,
 11    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12    // See the License for the specific language governing permissions and
 13    // limitations under the License.
 14   
 15    package org.apache.tapestry.parse;
 16   
 17    import java.util.ArrayList;
 18    import java.util.Collections;
 19    import java.util.HashMap;
 20    import java.util.Iterator;
 21    import java.util.List;
 22    import java.util.Map;
 23   
 24    import org.apache.hivemind.ApplicationRuntimeException;
 25    import org.apache.hivemind.Location;
 26    import org.apache.hivemind.Resource;
 27    import org.apache.hivemind.impl.LocationImpl;
 28    import org.apache.oro.text.regex.MalformedPatternException;
 29    import org.apache.oro.text.regex.MatchResult;
 30    import org.apache.oro.text.regex.Pattern;
 31    import org.apache.oro.text.regex.PatternMatcher;
 32    import org.apache.oro.text.regex.Perl5Compiler;
 33    import org.apache.oro.text.regex.Perl5Matcher;
 34    import org.apache.tapestry.util.IdAllocator;
 35   
 36    /**
 37    * Parses Tapestry templates, breaking them into a series of
 38    * {@link org.apache.tapestry.parse.TemplateToken tokens}. Although often referred to as an "HTML
 39    * template", there is no real requirement that the template be HTML. This parser can handle any
 40    * reasonable SGML derived markup (including XML), but specifically works around the ambiguities of
 41    * HTML reasonably.
 42    * <p>
 43    * Deployed as the tapestry.parse.TemplateParser service, using the threaded model.
 44    * <p>
 45    * Dynamic markup in Tapestry attempts to be invisible. Components are arbitrary tags containing a
 46    * <code>jwcid</code> attribute. Such components must be well balanced (have a matching close tag,
 47    * or end the tag with "<code>/&gt;</code>".
 48    * <p>
 49    * Generally, the id specified in the template is matched against an component defined in the
 50    * specification. However, implicit components are also possible. The jwcid attribute uses the
 51    * syntax "<code>@Type</code>" for implicit components. Type is the component type, and may include a library id
 52    * prefix. Such a component is anonymous (but is given a unique id).
 53    * <p>
 54    * (The unique ids assigned start with a dollar sign, which is normally no allowed for
 55    * component ids ... this helps to make them stand out and assures that they do not conflict
 56    * with user-defined component ids. These ids tend to propagate into URLs and become HTML
 57    * element names and even JavaScript variable names ... the dollar sign is acceptible in these
 58    * contexts as well).
 59    * <p>
 60    * Implicit component may also be given a name using the syntax "
 61    * <code>componentId:@Type</code>". Such a component should <b>not </b> be defined in the
 62    * specification, but may still be accessed via
 63    * {@link org.apache.tapestry.IComponent#getComponent(String)}.
 64    * <p>
 65    * Both defined and implicit components may have additional attributes defined, simply by
 66    * including them in the template. They set formal or informal parameters of the component to
 67    * static strings.
 68    * {@link org.apache.tapestry.spec.IComponentSpecification#getAllowInformalParameters()}, if
 69    * false, will cause such attributes to be simply ignored. For defined components, conflicting
 70    * values defined in the template are ignored.
 71    * <p>
 72    * Attributes in component tags will become formal and informal parameters of the
 73    * corresponding component. Most attributes will be
 74    * <p>
 75    * The parser removes the body of some tags (when the corresponding component doesn't
 76    * {@link org.apache.tapestry.spec.IComponentSpecification#getAllowBody() allow a body}, and
 77    * allows portions of the template to be completely removed.
 78    * <p>
 79    * The parser does a pretty thorough lexical analysis of the template, and reports a great
 80    * number of errors, including improper nesting of tags.
 81    * <p>
 82    * The parser supports <em>invisible localization</em>: The parser recognizes HTML of the
 83    * form: <code>&lt;span key="<i>value</i>"&gt; ... &lt;/span&gt;</code> and converts them
 84    * into a {@link TokenType#LOCALIZATION}token. You may also specifify a <code>raw</code>
 85    * attribute ... if the value is <code>true</code>, then the localized value is sent to the
 86    * client without filtering, which is appropriate if the value has any markup that should not
 87    * be escaped.
 88    * @author Howard Lewis Ship, Geoff Longman
 89    */
 90   
 91    public class TemplateParser implements ITemplateParser
 92    {
 93    /**
 94    * A "magic" component id that causes the tag with the id and its entire body to be ignored
 95    * during parsing.
 96    */
 97   
 98    private static final String REMOVE_ID = "$remove$";
 99   
 100    /**
 101    * A "magic" component id that causes the tag to represent the true content of the template. Any
 102    * content prior to the tag is discarded, and any content after the tag is ignored. The tag
 103    * itself is not included.
 104    */
 105   
 106    private static final String CONTENT_ID = "$content$";
 107   
 108    /**
 109    * The attribute, checked for in &lt;span&gt; tags, that signfies that the span is being used as
 110    * an invisible localization.
 111    *
 112    * @since 2.0.4
 113    */
 114   
 115    public static final String LOCALIZATION_KEY_ATTRIBUTE_NAME = "key";
 116   
 117    /**
 118    * Used with {@link #LOCALIZATION_KEY_ATTRIBUTE_NAME}to indicate a string that should be
 119    * rendered "raw" (without escaping HTML). If not specified, defaults to "false". The value must
 120    * equal "true" (caselessly).
 121    *
 122    * @since 2.3
 123    */
 124   
 125    public static final String RAW_ATTRIBUTE_NAME = "raw";
 126   
 127    /**
 128    * Attribute name used to identify components.
 129    *
 130    * @since 4.0
 131    */
 132   
 133    private String _componentAttributeName;
 134   
 135    private static final String PROPERTY_NAME_PATTERN = "_?[a-zA-Z]\\w*";
 136   
 137    /**
 138    * Pattern used to recognize ordinary components (defined in the specification).
 139    *
 140    * @since 3.0
 141    */
 142   
 143    public static final String SIMPLE_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")$";
 144   
 145    /**
 146    * Pattern used to recognize implicit components (whose type is defined in the template).
 147    * Subgroup 1 is the id (which may be null) and subgroup 2 is the type (which may be qualified
 148    * with a library prefix). Subgroup 4 is the library id, Subgroup 5 is the simple component
 149    * type.
 150    *
 151    * @since 3.0
 152    */
 153   
 154    public static final String IMPLICIT_ID_PATTERN = "^(" + PROPERTY_NAME_PATTERN + ")?@((("
 155    + PROPERTY_NAME_PATTERN + "):)?(" + PROPERTY_NAME_PATTERN + "))$";
 156   
 157    private static final int IMPLICIT_ID_PATTERN_ID_GROUP = 1;
 158   
 159    private static final int IMPLICIT_ID_PATTERN_TYPE_GROUP = 2;
 160   
 161    private static final int IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP = 4;
 162   
 163    private static final int IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP = 5;
 164   
 165    private Pattern _simpleIdPattern;
 166   
 167    private Pattern _implicitIdPattern;
 168   
 169    private PatternMatcher _patternMatcher;
 170   
 171    private IdAllocator _idAllocator = new IdAllocator();
 172   
 173    private ITemplateParserDelegate _delegate;
 174   
 175    /**
 176    * Identifies the template being parsed; used with error messages.
 177    */
 178   
 179    private Resource _resourceLocation;
 180   
 181    /**
 182    * Shared instance of {@link Location}used by all {@link TextToken}instances in the template.
 183    */
 184   
 185    private Location _templateLocation;
 186   
 187    /**
 188    * Location with in the resource for the current line.
 189    */
 190   
 191    private Location _currentLocation;
 192   
 193    /**
 194    * Local reference to the template data that is to be parsed.
 195    */
 196   
 197    private char[] _templateData;
 198   
 199    /**
 200    * List of Tag
 201    */
 202   
 203    private List _stack = new ArrayList();
 204   
 205    private static class Tag
 206    {
 207    // The element, i.e., <jwc> or virtually any other element (via jwcid attribute)
 208    String _tagName;
 209   
 210    // If true, the tag is a placeholder for a dynamic element
 211    boolean _component;
 212   
 213    // If true, the body of the tag is being ignored, and the
 214    // ignore flag is cleared when the close tag is reached
 215    boolean _ignoringBody;
 216   
 217    // If true, then the entire tag (and its body) is being ignored
 218    boolean _removeTag;
 219   
 220    // If true, then the tag must have a balanced closing tag.
 221    // This is always true for components.
 222    boolean _mustBalance;
 223   
 224    // The line on which the start tag exists
 225    int _line;
 226   
 227    // If true, then the parse ends when the closing tag is found.
 228    boolean _content;
 229   
 230  1388 Tag(String tagName, int line)
 231    {
 232  1388 _tagName = tagName;
 233  1388 _line = line;
 234    }
 235   
 236  1306 boolean match(String matchTagName)
 237    {
 238  1306 return _tagName.equalsIgnoreCase(matchTagName);
 239    }
 240    }
 241   
 242    /**
 243    * List of {@link TemplateToken}, this forms the ultimate response.
 244    */
 245   
 246    private List _tokens = new ArrayList();
 247   
 248    /**
 249    * The location of the 'cursor' within the template data. The advance() method moves this
 250    * forward.
 251    */
 252   
 253    private int _cursor;
 254   
 255    /**
 256    * The start of the current block of static text, or -1 if no block is active.
 257    */
 258   
 259    private int _blockStart;
 260   
 261    /**
 262    * The current line number; tracked by advance(). Starts at 1.
 263    */
 264   
 265    private int _line;
 266   
 267    /**
 268    * Set to true when the body of a tag is being ignored. This is typically used to skip over the
 269    * body of a tag when its corresponding component doesn't allow a body, or whe the special jwcid
 270    * of $remove$ is used.
 271    */
 272   
 273    private boolean _ignoring;
 274   
 275    /**
 276    * A {@link Map}of {@link String}s, used to store attributes collected while parsing a tag.
 277    */
 278   
 279    private Map _attributes = new HashMap();
 280   
 281    /**
 282    * A factory used to create template tokens.
 283    */
 284   
 285    private TemplateTokenFactory _factory;
 286   
 287  125 public TemplateParser()
 288    {
 289  125 Perl5Compiler compiler = new Perl5Compiler();
 290   
 291  125 try
 292    {
 293  125 _simpleIdPattern = compiler.compile(SIMPLE_ID_PATTERN);
 294  125 _implicitIdPattern = compiler.compile(IMPLICIT_ID_PATTERN);
 295    }
 296    catch (MalformedPatternException ex)
 297    {
 298  0 throw new ApplicationRuntimeException(ex);
 299    }
 300   
 301  125 _patternMatcher = new Perl5Matcher();
 302    }
 303   
 304    /**
 305    * Parses the template data into an array of {@link TemplateToken}s.
 306    * <p>
 307    * The parser is <i>decidedly </i> not threadsafe, so care should be taken that only a single
 308    * thread accesses it.
 309    *
 310    * @param templateData
 311    * the HTML template to parse. Some tokens will hold a reference to this array.
 312    * @param delegate
 313    * object that "knows" about defined components
 314    * @param resourceLocation
 315    * a description of where the template originated from, used with error messages.
 316    */
 317   
 318  189 public TemplateToken[] parse(char[] templateData, ITemplateParserDelegate delegate,
 319    Resource resourceLocation) throws TemplateParseException
 320    {
 321  189 try
 322    {
 323  189 beforeParse(templateData, delegate, resourceLocation);
 324   
 325  189 parse();
 326   
 327  177 return (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]);
 328    }
 329    finally
 330    {
 331  189 afterParse();
 332    }
 333    }
 334   
 335    /**
 336    * perform default initialization of the parser.
 337    */
 338   
 339  189 protected void beforeParse(char[] templateData, ITemplateParserDelegate delegate,
 340    Resource resourceLocation)
 341    {
 342  189 _templateData = templateData;
 343  189 _resourceLocation = resourceLocation;
 344  189 _templateLocation = new LocationImpl(resourceLocation);
 345  189 _delegate = delegate;
 346  189 _ignoring = false;
 347  189 _line = 1;
 348  189 _componentAttributeName = delegate.getComponentAttributeName();
 349    }
 350   
 351    /**
 352    * Perform default cleanup after parsing completes.
 353    */
 354   
 355  189 protected void afterParse()
 356    {
 357  189 _delegate = null;
 358  189 _templateData = null;
 359  189 _resourceLocation = null;
 360  189 _templateLocation = null;
 361  189 _currentLocation = null;
 362  189 _stack.clear();
 363  189 _tokens.clear();
 364  189 _attributes.clear();
 365  189 _idAllocator.clear();
 366    }
 367   
 368    /**
 369    * Used by the parser to report problems in the parse. Parsing <b>must </b> stop when a problem
 370    * is reported.
 371    * <p>
 372    * The default implementation simply throws an exception that contains the message and location
 373    * parameters.
 374    * <p>
 375    * Subclasses may override but <b>must </b> ensure they throw the required exception.
 376    *
 377    * @param message
 378    * @param location
 379    * @param line
 380    * ignored by the default impl
 381    * @param cursor
 382    * ignored by the default impl
 383    * @throws TemplateParseException
 384    * always thrown in order to terminate the parse.
 385    */
 386   
 387  12 protected void templateParseProblem(String message, Location location, int line, int cursor)
 388    throws TemplateParseException
 389    {
 390  12 throw new TemplateParseException(message, location);
 391    }
 392   
 393    /**
 394    * Used by the parser to report tapestry runtime specific problems in the parse. Parsing <b>must
 395    * </b> stop when a problem is reported.
 396    * <p>
 397    * The default implementation simply rethrows the exception.
 398    * <p>
 399    * Subclasses may override but <b>must </b> ensure they rethrow the exception.
 400    *
 401    * @param exception
 402    * @param line
 403    * ignored by the default impl
 404    * @param cursor
 405    * ignored by the default impl
 406    * @throws ApplicationRuntimeException
 407    * always rethrown in order to terminate the parse.
 408    */
 409   
 410  0 protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor)
 411    throws ApplicationRuntimeException
 412    {
 413  0 throw exception;
 414    }
 415   
 416    /**
 417    * Give subclasses access to the parse results.
 418    */
 419  0 protected List getTokens()
 420    {
 421  0 if (_tokens == null)
 422  0 return Collections.EMPTY_LIST;
 423   
 424  0 return _tokens;
 425    }
 426   
 427    /**
 428    * Checks to see if the next few characters match a given pattern.
 429    */
 430   
 431  16461 private boolean lookahead(char[] match)
 432    {
 433  16461 try
 434    {
 435  16461 for (int i = 0; i < match.length; i++)
 436    {
 437  21915 if (_templateData[_cursor + i] != match[i])
 438  15067 return false;
 439    }
 440   
 441    // Every character matched.
 442   
 443  1394 return true;
 444    }
 445    catch (IndexOutOfBoundsException ex)
 446    {
 447  0 return false;
 448    }
 449    }
 450   
 451    private static final char[] COMMENT_START = new char[]
 452    { '<', '!', '-', '-' };
 453   
 454    private static final char[] COMMENT_END = new char[]
 455    { '-', '-', '>' };
 456   
 457    private static final char[] CLOSE_TAG = new char[]
 458    { '<', '/' };
 459   
 460  189 protected void parse() throws TemplateParseException
 461    {
 462  189 _cursor = 0;
 463  189 _blockStart = -1;
 464  189 int length = _templateData.length;
 465   
 466  189 while (_cursor < length)
 467    {
 468  21727 if (_templateData[_cursor] != '<')
 469    {
 470  18686 if (_blockStart < 0 && !_ignoring)
 471  1153 _blockStart = _cursor;
 472   
 473  18686 advance();
 474  18686 continue;
 475    }
 476   
 477    // OK, start of something.
 478   
 479  3041 if (lookahead(CLOSE_TAG))
 480    {
 481  1235 closeTag();
 482  1232 continue;
 483    }
 484   
 485  1806 if (lookahead(COMMENT_START))
 486    {
 487  80 skipComment();
 488  79 continue;
 489    }
 490   
 491    // The start of some tag.
 492   
 493  1726 startTag();
 494    }
 495   
 496    // Usually there's some text at the end of the template (after the last closing tag) that
 497    // should
 498    // be added. Often the last few tags are static tags so we definately
 499    // need to end the text block.
 500   
 501  177 addTextToken(_templateData.length - 1);
 502    }
 503   
 504    /**
 505    * Advance forward in the document until the end of the comment is reached. In addition, skip
 506    * any whitespace following the comment.
 507    */
 508   
 509  80 private void skipComment() throws TemplateParseException
 510    {
 511  80 int length = _templateData.length;
 512  80 int startLine = _line;
 513   
 514  80 if (_blockStart < 0 && !_ignoring)
 515  19 _blockStart = _cursor;
 516   
 517  80 while (true)
 518    {
 519  11615 if (_cursor >= length)
 520  1 templateParseProblem(ParseMessages.commentNotEnded(startLine), new LocationImpl(
 521    _resourceLocation, startLine), startLine, _cursor);
 522   
 523  11614 if (lookahead(COMMENT_END))
 524  79 break;
 525   
 526    // Not the end of the comment, advance over it.
 527   
 528  11535 advance();
 529    }
 530   
 531  79 _cursor += COMMENT_END.length;
 532  79 advanceOverWhitespace();
 533    }
 534   
 535  1682 private void addTextToken(int end)
 536    {
 537    // No active block to add to.
 538   
 539  1682 if (_blockStart < 0)
 540  419 return;
 541   
 542  1263 if (_blockStart <= end)
 543    {
 544    // This seems odd, shouldn't the location be the current location? I guess
 545    // no errors are ever reported for a text token.
 546   
 547  1263 TemplateToken token = _factory.createTextToken(
 548    _templateData,
 549    _blockStart,
 550    end,
 551    _templateLocation);
 552   
 553  1263 _tokens.add(token);
 554    }
 555   
 556  1263 _blockStart = -1;
 557    }
 558   
 559    private static final int WAIT_FOR_ATTRIBUTE_NAME = 0;
 560   
 561    private static final int COLLECT_ATTRIBUTE_NAME = 1;
 562   
 563    private static final int ADVANCE_PAST_EQUALS = 2;
 564   
 565    private static final int WAIT_FOR_ATTRIBUTE_VALUE = 3;
 566   
 567    private static final int COLLECT_QUOTED_VALUE = 4;
 568   
 569    private static final int COLLECT_UNQUOTED_VALUE = 5;
 570   
 571  1726 private void startTag() throws TemplateParseException
 572    {
 573  1726 int cursorStart = _cursor;
 574  1726 int length = _templateData.length;
 575  1726 String tagName = null;
 576  1726 boolean endOfTag = false;
 577  1726 boolean emptyTag = false;
 578  1726 int startLine = _line;
 579  1726 Location startLocation = new LocationImpl(_resourceLocation, startLine);
 580   
 581  1726 tagBeginEvent(startLine, _cursor);
 582   
 583  1726 advance();
 584   
 585    // Collect the element type
 586   
 587  7359 while (_cursor < length)
 588    {
 589  7359 char ch = _templateData[_cursor];
 590   
 591  7359 if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
 592    {
 593  1726 tagName = new String(_templateData, cursorStart + 1, _cursor - cursorStart - 1);
 594   
 595  1726 break;
 596    }
 597   
 598  5633 advance();
 599    }
 600   
 601  1726 String attributeName = null;
 602  1726 int attributeNameStart = -1;
 603  1726 int attributeValueStart = -1;
 604  1726 int state = WAIT_FOR_ATTRIBUTE_NAME;
 605  1726 char quoteChar = 0;
 606   
 607  1726 _attributes.clear();
 608   
 609    // Collect each attribute
 610   
 611  1726 while (!endOfTag)
 612    {
 613  44344 if (_cursor >= length)
 614    {
 615  1 String message = (tagName == null) ? ParseMessages.unclosedUnknownTag(startLine)
 616    : ParseMessages.unclosedTag(tagName, startLine);
 617   
 618  1 templateParseProblem(message, startLocation, startLine, cursorStart);
 619    }
 620   
 621  44343 char ch = _templateData[_cursor];
 622   
 623  44343 switch (state)
 624    {
 625  6358 case WAIT_FOR_ATTRIBUTE_NAME:
 626   
 627    // Ignore whitespace before the next attribute name, while
 628    // looking for the end of the current tag.
 629   
 630  6358 if (ch == '/')
 631    {
 632  495 emptyTag = true;
 633  495 advance();
 634  495 break;
 635    }
 636   
 637  5863 if (ch == '>')
 638    {
 639  1724 endOfTag = true;
 640  1724 break;
 641    }
 642   
 643  4139 if (Character.isWhitespace(ch))
 644    {
 645  1988 advance();
 646  1988 break;
 647    }
 648   
 649    // Found non-whitespace, assume its the attribute name.
 650    // Note: could use a check here for non-alpha.
 651   
 652  2151 attributeNameStart = _cursor;
 653  2151 state = COLLECT_ATTRIBUTE_NAME;
 654  2151 advance();
 655  2151 break;
 656   
 657  11518 case COLLECT_ATTRIBUTE_NAME:
 658   
 659    // Looking for end of attribute name.
 660   
 661  11518 if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
 662    {
 663  2151 attributeName = new String(_templateData, attributeNameStart, _cursor
 664    - attributeNameStart);
 665   
 666  2151 state = ADVANCE_PAST_EQUALS;
 667  2151 break;
 668    }
 669   
 670    // Part of the attribute name
 671   
 672  9367 advance();
 673  9367 break;
 674   
 675  2294 case ADVANCE_PAST_EQUALS:
 676   
 677    // Looking for the '=' sign. May hit the end of the tag, or (for bare
 678    // attributes),
 679    // the next attribute name.
 680   
 681  2294 if (ch == '/' || ch == '>')
 682    {
 683    // A bare attribute, which is not interesting to
 684    // us.
 685   
 686  145 state = WAIT_FOR_ATTRIBUTE_NAME;
 687  145 break;
 688    }
 689   
 690  2149 if (Character.isWhitespace(ch))
 691    {
 692  143 advance();
 693  143 break;
 694    }
 695   
 696  2006 if (ch == '=')
 697    {
 698  1916 state = WAIT_FOR_ATTRIBUTE_VALUE;
 699  1916 quoteChar = 0;
 700  1916 attributeValueStart = -1;
 701  1916 advance();
 702  1916 break;
 703    }
 704   
 705    // Otherwise, an HTML style "bare" attribute (such as <select multiple>).
 706    // We aren't interested in those (we're just looking for the id or jwcid
 707    // attribute).
 708   
 709  90 state = WAIT_FOR_ATTRIBUTE_NAME;
 710  90 break;
 711   
 712  1919 case WAIT_FOR_ATTRIBUTE_VALUE:
 713   
 714  1919 if (ch == '/' || ch == '>')
 715  1 templateParseProblem(ParseMessages.missingAttributeValue(
 716    tagName,
 717    _line,
 718    attributeName), getCurrentLocation(), _line, _cursor);
 719   
 720    // Ignore whitespace between '=' and the attribute value. Also, look
 721    // for initial quote.
 722   
 723  1918 if (Character.isWhitespace(ch))
 724    {
 725  3 advance();
 726  3 break;
 727    }
 728   
 729  1915 if (ch == '\'' || ch == '"')
 730    {
 731  1886 quoteChar = ch;
 732   
 733  1886 state = COLLECT_QUOTED_VALUE;
 734  1886 advance();
 735  1886 attributeValueStart = _cursor;
 736  1886 attributeBeginEvent(attributeName, _line, attributeValueStart);
 737  1886 break;
 738    }
 739   
 740    // Not whitespace or quote, must be start of unquoted attribute.
 741   
 742  29 state = COLLECT_UNQUOTED_VALUE;
 743  29 attributeValueStart = _cursor;
 744  29 attributeBeginEvent(attributeName, _line, attributeValueStart);
 745  29 break;
 746   
 747  22192 case COLLECT_QUOTED_VALUE:
 748   
 749    // Start collecting the quoted attribute value. Stop at the matching quote
 750    // character,
 751    // unless bare, in which case, stop at the next whitespace.
 752   
 753  22192 if (ch == quoteChar)
 754    {
 755  1886 String attributeValue = new String(_templateData, attributeValueStart,
 756    _cursor - attributeValueStart);
 757   
 758  1886 _attributes.put(attributeName, attributeValue);
 759  1886 attributeEndEvent(_cursor);
 760   
 761    // Advance over the quote.
 762  1886 advance();
 763  1886 state = WAIT_FOR_ATTRIBUTE_NAME;
 764  1886 break;
 765    }
 766   
 767  20306 advance();
 768  20306 break;
 769   
 770  62 case COLLECT_UNQUOTED_VALUE:
 771   
 772    // An unquoted attribute value ends with whitespace
 773    // or the end of the enclosing tag.
 774   
 775  62 if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
 776    {
 777  29 String attributeValue = new String(_templateData, attributeValueStart,
 778    _cursor - attributeValueStart);
 779   
 780  29 _attributes.put(attributeName, attributeValue);
 781  29 attributeEndEvent(_cursor);
 782   
 783  29 state = WAIT_FOR_ATTRIBUTE_NAME;
 784  29 break;
 785    }
 786   
 787  33 advance();
 788  33 break;
 789    }
 790    }
 791   
 792  1724 tagEndEvent(_cursor);
 793   
 794    // Check for invisible localizations
 795   
 796  1724 String localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes);
 797  1724 String jwcId = findValueCaselessly(_componentAttributeName, _attributes);
 798   
 799  1724 if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null)
 800    {
 801  16 if (_ignoring)
 802  1 templateParseProblem(
 803    ParseMessages.componentMayNotBeIgnored(tagName, startLine),
 804    startLocation,
 805    startLine,
 806    cursorStart);
 807   
 808    // If the tag isn't empty, then create a Tag instance to ignore the
 809    // body of the tag.
 810   
 811  15 if (!emptyTag)
 812    {
 813  3 Tag tag = new Tag(tagName, startLine);
 814   
 815  3 tag._component = false;
 816  3 tag._removeTag = true;
 817  3 tag._ignoringBody = true;
 818  3 tag._mustBalance = true;
 819   
 820  3 _stack.add(tag);
 821   
 822    // Start ignoring content until the close tag.
 823   
 824  3 _ignoring = true;
 825    }
 826    else
 827    {
 828    // Cursor is at the closing carat, advance over it and any whitespace.
 829  12 advance();
 830  12 advanceOverWhitespace();
 831    }
 832   
 833    // End any open block.
 834   
 835  15 addTextToken(cursorStart - 1);
 836   
 837  15 boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes);
 838   
 839  15 Map attributes = filter(_attributes, new String[]
 840    { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME });
 841   
 842  15 TemplateToken token = _factory.createLocalizationToken(
 843    tagName,
 844    localizationKey,
 845    raw,
 846    attributes,
 847    startLocation);
 848   
 849  15 _tokens.add(token);
 850   
 851  15 return;
 852    }
 853   
 854  1708 if (jwcId != null)
 855    {
 856  964 processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation);
 857  959 return;
 858    }
 859   
 860    // A static tag (not a tag without a jwcid attribute).
 861    // We need to record this so that we can match close tags later.
 862   
 863  744 if (!emptyTag)
 864    {
 865  669 Tag tag = new Tag(tagName, startLine);
 866  669 _stack.add(tag);
 867    }
 868   
 869    // If there wasn't an active block, then start one.
 870   
 871  744 if (_blockStart < 0 && !_ignoring)
 872  44 _blockStart = cursorStart;
 873   
 874  744 advance();
 875    }
 876   
 877    /**
 878    * Processes a tag that is the open tag for a component (but also handles the $remove$ and
 879    * $content$ tags).
 880    */
 881   
 882    /**
 883    * Notify that the beginning of a tag has been detected.
 884    * <p>
 885    * Default implementation does nothing.
 886    */
 887  1726 protected void tagBeginEvent(int startLine, int cursorPosition)
 888    {
 889    }
 890   
 891    /**
 892    * Notify that the end of the current tag has been detected.
 893    * <p>
 894    * Default implementation does nothing.
 895    */
 896  1724 protected void tagEndEvent(int cursorPosition)
 897    {
 898    }
 899   
 900    /**
 901    * Notify that the beginning of an attribute value has been detected.
 902    * <p>
 903    * Default implementation does nothing.
 904    */
 905  1915 protected void attributeBeginEvent(String attributeName, int startLine, int cursorPosition)
 906    {
 907    }
 908   
 909    /**
 910    * Notify that the end of the current attribute value has been detected.
 911    * <p>
 912    * Default implementation does nothing.
 913    */
 914  1915 protected void attributeEndEvent(int cursorPosition)
 915    {
 916    }
 917   
 918  964 private void processComponentStart(String tagName, String jwcId, boolean emptyTag,
 919    int startLine, int cursorStart, Location startLocation) throws TemplateParseException
 920    {
 921  964 if (jwcId.equalsIgnoreCase(CONTENT_ID))
 922    {
 923  78 processContentTag(tagName, startLine, cursorStart, emptyTag);
 924   
 925  77 return;
 926    }
 927   
 928  886 boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
 929   
 930  886 if (_ignoring && !isRemoveId)
 931  2 templateParseProblem(
 932    ParseMessages.componentMayNotBeIgnored(tagName, startLine),
 933    startLocation,
 934    startLine,
 935    cursorStart);
 936   
 937  884 String type = null;
 938  884 boolean allowBody = false;
 939   
 940  884 if (_patternMatcher.matches(jwcId, _implicitIdPattern))
 941    {
 942  498 MatchResult match = _patternMatcher.getMatch();
 943   
 944  498 jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
 945  498 type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);
 946   
 947  498 String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
 948  498 String simpleType = match.group(IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP);
 949   
 950    // If (and this is typical) no actual component id was specified,
 951    // then generate one on the fly.
 952    // The allocated id for anonymous components is
 953    // based on the simple (unprefixed) type, but starts
 954    // with a leading dollar sign to ensure no conflicts
 955    // with user defined component ids (which don't allow dollar signs
 956    // in the id).
 957   
 958  498 if (jwcId == null)
 959  432 jwcId = _idAllocator.allocateId("$" + simpleType);
 960   
 961  498 try
 962    {
 963  498 allowBody = _delegate.getAllowBody(libraryId, simpleType, startLocation);
 964    }
 965    catch (ApplicationRuntimeException e)
 966    {
 967    // give subclasses a chance to handle and rethrow
 968  0 templateParseProblem(e, startLine, cursorStart);
 969    }
 970   
 971    }
 972    else
 973    {
 974  386 if (!isRemoveId)
 975    {
 976  282 if (!_patternMatcher.matches(jwcId, _simpleIdPattern))
 977  0 templateParseProblem(
 978    ParseMessages.componentIdInvalid(tagName, startLine, jwcId),
 979    startLocation,
 980    startLine,
 981    cursorStart);
 982   
 983  282 if (!_delegate.getKnownComponent(jwcId))
 984  1 templateParseProblem(
 985    ParseMessages.unknownComponentId(tagName, startLine, jwcId),
 986    startLocation,
 987    startLine,
 988    cursorStart);
 989   
 990  281 try
 991    {
 992  281 allowBody = _delegate.getAllowBody(jwcId, startLocation);
 993    }
 994    catch (ApplicationRuntimeException e)
 995    {
 996    // give subclasses a chance to handle and rethrow
 997  0 templateParseProblem(e, startLine, cursorStart);
 998    }
 999    }
 1000    }
 1001   
 1002    // Ignore the body if we're removing the entire tag,
 1003    // of if the corresponding component doesn't allow
 1004    // a body.
 1005   
 1006  883 boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody);
 1007   
 1008  883 if (_ignoring && ignoreBody)
 1009  1 templateParseProblem(ParseMessages.nestedIgnore(tagName, startLine), new LocationImpl(
 1010    _resourceLocation, startLine), startLine, cursorStart);
 1011   
 1012  882 if (!emptyTag)
 1013  639 pushNewTag(tagName, startLine, isRemoveId, ignoreBody);
 1014   
 1015    // End any open block.
 1016   
 1017  882 addTextToken(cursorStart - 1);
 1018   
 1019  882 if (!isRemoveId)
 1020    {
 1021  779 addOpenToken(tagName, jwcId, type, startLocation);
 1022   
 1023  779 if (emptyTag)
 1024  243 _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
 1025    }
 1026   
 1027  882 advance();
 1028    }
 1029   
 1030  639 private void pushNewTag(String tagName, int startLine, boolean isRemoveId, boolean ignoreBody)
 1031    {
 1032  639 Tag tag = new Tag(tagName, startLine);
 1033   
 1034  639 tag._component = !isRemoveId;
 1035  639 tag._removeTag = isRemoveId;
 1036   
 1037  639 tag._ignoringBody = ignoreBody;
 1038   
 1039  639 _ignoring = tag._ignoringBody;
 1040   
 1041  639 tag._mustBalance = true;
 1042   
 1043  639 _stack.add(tag);
 1044    }
 1045   
 1046  78 private void processContentTag(String tagName, int startLine, int cursorStart, boolean emptyTag)
 1047    throws TemplateParseException
 1048    {
 1049  78 if (_ignoring)
 1050  1 templateParseProblem(
 1051    ParseMessages.contentBlockMayNotBeIgnored(tagName, startLine),
 1052    new LocationImpl(_resourceLocation, startLine),
 1053    startLine,
 1054    cursorStart);
 1055   
 1056  77 if (emptyTag)
 1057  0 templateParseProblem(
 1058    ParseMessages.contentBlockMayNotBeEmpty(tagName, startLine),
 1059    new LocationImpl(_resourceLocation, startLine),
 1060    startLine,
 1061    cursorStart);
 1062   
 1063  77 _tokens.clear();
 1064  77 _blockStart = -1;
 1065   
 1066  77 Tag tag = new Tag(tagName, startLine);
 1067   
 1068  77 tag._mustBalance = true;
 1069  77 tag._content = true;
 1070   
 1071  77 _stack.clear();
 1072  77 _stack.add(tag);
 1073   
 1074  77 advance();
 1075    }
 1076   
 1077  779 private void addOpenToken(String tagName, String jwcId, String type, Location location)
 1078    {
 1079  779 OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
 1080  779 _tokens.add(token);
 1081   
 1082  779 if (_attributes.isEmpty())
 1083  0 return;
 1084   
 1085  779 Iterator i = _attributes.entrySet().iterator();
 1086  779 while (i.hasNext())
 1087    {
 1088  1382 Map.Entry entry = (Map.Entry) i.next();
 1089   
 1090  1382 String key = (String) entry.getKey();
 1091   
 1092  1382 if (key.equalsIgnoreCase(_componentAttributeName))
 1093  779 continue;
 1094   
 1095  603 String value = (String) entry.getValue();
 1096   
 1097  603 addAttributeToToken(token, key, value);
 1098    }
 1099    }
 1100   
 1101    /**
 1102    * Adds the attribute to the token (identifying prefixes and whatnot is now done downstream).
 1103    *
 1104    * @since 3.0
 1105    */
 1106   
 1107  603 private void addAttributeToToken(OpenToken token, String name, String attributeValue)
 1108    {
 1109  603 token.addAttribute(name, convertEntitiesToPlain(attributeValue));
 1110    }
 1111   
 1112    /**
 1113    * Invoked to handle a closing tag, i.e., &lt;/foo&gt;. When a tag closes, it will match against
 1114    * a tag on the open tag start. Preferably the top tag on the stack (if everything is well
 1115    * balanced), but this is HTML, not XML, so many tags won't balance.
 1116    * <p>
 1117    * Once the matching tag is located, the question is ... is the tag dynamic or static? If
 1118    * static, then the current text block is extended to include this close tag. If dynamic, then
 1119    * the current text block is ended (before the '&lt;' that starts the tag) and a close token is
 1120    * added.
 1121    * <p>
 1122    * In either case, the matching static element and anything above it is removed, and the cursor
 1123    * is left on the character following the '&gt;'.
 1124    */
 1125   
 1126  1235 private void closeTag() throws TemplateParseException
 1127    {
 1128  1235 int cursorStart = _cursor;
 1129  1235 int length = _templateData.length;
 1130  1235 int startLine = _line;
 1131   
 1132  1235 Location startLocation = getCurrentLocation();
 1133   
 1134  1235 _cursor += CLOSE_TAG.length;
 1135   
 1136  1235 int tagStart = _cursor;
 1137   
 1138  1235 while (true)
 1139    {
 1140  4994 if (_cursor >= length)
 1141  1 templateParseProblem(
 1142    ParseMessages.incompleteCloseTag(startLine),
 1143    startLocation,
 1144    startLine,
 1145    cursorStart);
 1146   
 1147  4993 char ch = _templateData[_cursor];
 1148   
 1149  4993 if (ch == '>')
 1150  1234 break;
 1151   
 1152  3759 advance();
 1153    }
 1154   
 1155  1234 String tagName = new String(_templateData, tagStart, _cursor - tagStart);
 1156   
 1157  1234 int stackPos = _stack.size() - 1;
 1158  1234 Tag tag = null;
 1159   
 1160  1234 while (stackPos >= 0)
 1161    {
 1162  1306 tag = (Tag) _stack.get(stackPos);
 1163   
 1164  1306 if (tag.match(tagName))
 1165  1232 break;
 1166   
 1167  74 if (tag._mustBalance)
 1168  1 templateParseProblem(ParseMessages.improperlyNestedCloseTag(
 1169    tagName,
 1170    startLine,
 1171    tag._tagName,
 1172    tag._line), startLocation, startLine, cursorStart);
 1173   
 1174  73 stackPos--;
 1175    }
 1176   
 1177  1233 if (stackPos < 0)
 1178  1 templateParseProblem(
 1179    ParseMessages.unmatchedCloseTag(tagName, startLine),
 1180    startLocation,
 1181    startLine,
 1182    cursorStart);
 1183   
 1184    // Special case for the content tag
 1185   
 1186  1232 if (tag._content)
 1187    {
 1188  76 addTextToken(cursorStart - 1);
 1189   
 1190    // Advance the cursor right to the end.
 1191   
 1192  76 _cursor = length;
 1193  76 _stack.clear();
 1194  76 return;
 1195    }
 1196   
 1197    // When a component closes, add a CLOSE tag.
 1198  1156 if (tag._component)
 1199    {
 1200  532 addTextToken(cursorStart - 1);
 1201   
 1202  532 _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
 1203    }
 1204    else
 1205    {
 1206    // The close of a static tag. Unless removing the tag
 1207    // entirely, make sure the block tag is part of a text block.
 1208   
 1209  624 if (_blockStart < 0 && !tag._removeTag && !_ignoring)
 1210  102 _blockStart = cursorStart;
 1211    }
 1212   
 1213    // Remove all elements at stackPos or above.
 1214   
 1215  1156 for (int i = _stack.size() - 1; i >= stackPos; i--)
 1216  1212 _stack.remove(i);
 1217   
 1218    // Advance cursor past '>'
 1219   
 1220  1156 advance();
 1221   
 1222    // If editting out the tag (i.e., $remove$) then kill any whitespace.
 1223    // For components that simply don't contain a body, removeTag will
 1224    // be false.
 1225   
 1226  1156 if (tag._removeTag)
 1227  101 advanceOverWhitespace();
 1228   
 1229    // If we were ignoring the body of the tag, then clear the ignoring
 1230    // flag, since we're out of the body.
 1231   
 1232  1156 if (tag._ignoringBody)
 1233  222 _ignoring = false;
 1234    }
 1235   
 1236    /**
 1237    * Advances the cursor to the next character. If the end-of-line is reached, then increments the
 1238    * line counter.
 1239    */
 1240   
 1241  85126 private void advance()
 1242    {
 1243  85126 int length = _templateData.length;
 1244   
 1245  85126 if (_cursor >= length)
 1246  0 return;
 1247   
 1248  85126 char ch = _templateData[_cursor];
 1249   
 1250  85126 _cursor++;
 1251   
 1252  85126 if (ch == '\n')
 1253    {
 1254  269 _line++;
 1255  269 _currentLocation = null;
 1256  269 return;
 1257    }
 1258   
 1259    // A \r, or a \r\n also counts as a new line.
 1260   
 1261  84857 if (ch == '\r')
 1262    {
 1263  2982 _line++;
 1264  2982 _currentLocation = null;
 1265   
 1266  2982 if (_cursor < length && _templateData[_cursor] == '\n')
 1267  2981 _cursor++;
 1268   
 1269  2982 return;
 1270    }
 1271   
 1272    // Not an end-of-line character.
 1273   
 1274    }
 1275   
 1276  192 private void advanceOverWhitespace()
 1277    {
 1278  192 int length = _templateData.length;
 1279   
 1280  192 while (_cursor < length)
 1281    {
 1282  929 char ch = _templateData[_cursor];
 1283  929 if (!Character.isWhitespace(ch))
 1284  187 return;
 1285   
 1286  742 advance();
 1287    }
 1288    }
 1289   
 1290    /**
 1291    * Returns a new Map that is a copy of the input Map with some key/value pairs removed. A list
 1292    * of keys is passed in and matching keys (caseless comparison) from the input Map are excluded
 1293    * from the output map. May return null (rather than return an empty Map).
 1294    */
 1295   
 1296  15 private Map filter(Map input, String[] removeKeys)
 1297    {
 1298  15 if (input == null || input.isEmpty())
 1299  0 return null;
 1300   
 1301  15 Map result = null;
 1302   
 1303  15 Iterator i = input.entrySet().iterator();
 1304   
 1305  15 nextkey: while (i.hasNext())
 1306    {
 1307  20 Map.Entry entry = (Map.Entry) i.next();
 1308   
 1309  20 String key = (String) entry.getKey();
 1310   
 1311  20 for (int j = 0; j < removeKeys.length; j++)
 1312    {
 1313  25 if (key.equalsIgnoreCase(removeKeys[j]))
 1314  17 continue nextkey;
 1315    }
 1316   
 1317  3 if (result == null)
 1318  2 result = new HashMap(input.size());
 1319   
 1320  3 result.put(key, entry.getValue());
 1321    }
 1322   
 1323  15 return result;
 1324    }
 1325   
 1326    /**
 1327    * Searches a Map for given key, caselessly. The Map is expected to consist of Strings for keys
 1328    * and values. Returns the value for the first key found that matches (caselessly) the input
 1329    * key. Returns null if no value found.
 1330    */
 1331   
 1332  3463 protected String findValueCaselessly(String key, Map map)
 1333    {
 1334  3463 String result = (String) map.get(key);
 1335   
 1336  3463 if (result != null)
 1337  979 return result;
 1338   
 1339  2484 Iterator i = map.entrySet().iterator();
 1340  2484 while (i.hasNext())
 1341    {
 1342  2218 Map.Entry entry = (Map.Entry) i.next();
 1343   
 1344  2218 String entryKey = (String) entry.getKey();
 1345   
 1346  2218 if (entryKey.equalsIgnoreCase(key))
 1347  3 return (String) entry.getValue();
 1348    }
 1349   
 1350  2481 return null;
 1351    }
 1352   
 1353    /**
 1354    * Conversions needed by {@link #convertEntitiesToPlain(String)}
 1355    */
 1356   
 1357    private static final String[] CONVERSIONS =
 1358    { "&lt;", "<", "&gt;", ">", "&quot;", "\"", "&amp;", "&" };
 1359   
 1360    /**
 1361    * Provided a raw input string that has been recognized to be an expression, this removes excess
 1362    * white space and converts &amp;amp;;, &amp;quot;; &amp;lt;; and &amp;gt;; to their normal
 1363    * character values (otherwise its impossible to specify those values in expressions in the
 1364    * template).
 1365    */
 1366   
 1367  603 private String convertEntitiesToPlain(String input)
 1368    {
 1369  603 int inputLength = input.length();
 1370   
 1371  603 StringBuffer buffer = new StringBuffer(inputLength);
 1372   
 1373  603 int cursor = 0;
 1374   
 1375  603 outer: while (cursor < inputLength)
 1376    {
 1377  8680 for (int i = 0; i < CONVERSIONS.length; i += 2)
 1378    {
 1379  34703 String entity = CONVERSIONS[i];
 1380  34703 int entityLength = entity.length();
 1381  34703 String value = CONVERSIONS[i + 1];
 1382   
 1383  34703 if (cursor + entityLength > inputLength)
 1384  8521 continue;
 1385   
 1386  26182 if (input.substring(cursor, cursor + entityLength).equals(entity))
 1387    {
 1388  15 buffer.append(value);
 1389  15 cursor += entityLength;
 1390  15 continue outer;
 1391    }
 1392    }
 1393   
 1394  8665 buffer.append(input.charAt(cursor));
 1395  8665 cursor++;
 1396    }
 1397   
 1398  603 return buffer.toString().trim();
 1399    }
 1400   
 1401    /**
 1402    * Returns true if the map contains the given key (caseless search) and the value is "true"
 1403    * (caseless comparison).
 1404    */
 1405   
 1406  15 private boolean checkBoolean(String key, Map map)
 1407    {
 1408  15 String value = findValueCaselessly(key, map);
 1409   
 1410  15 if (value == null)
 1411  13 return false;
 1412   
 1413  2 return value.equalsIgnoreCase("true");
 1414    }
 1415   
 1416    /**
 1417    * Gets the current location within the file. This allows the location to be created only as
 1418    * needed, and multiple objects on the same line can share the same Location instance.
 1419    *
 1420    * @since 3.0
 1421    */
 1422   
 1423  2011 protected Location getCurrentLocation()
 1424    {
 1425  2011 if (_currentLocation == null)
 1426  1330 _currentLocation = new LocationImpl(_resourceLocation, _line);
 1427   
 1428  2011 return _currentLocation;
 1429    }
 1430   
 1431  125 public void setFactory(TemplateTokenFactory factory)
 1432    {
 1433  125 _factory = factory;
 1434    }
 1435   
 1436    }