Clover coverage report - Code Coverage for tapestry release 4.0.1
Coverage timestamp: Fri Mar 31 2006 09:12:14 EST
file stats: LOC: 1,459   Methods: 33
NCLOC: 755   Classes: 2
 
 Source file Conditionals Statements Methods TOTAL
TemplateParser.java 93.5% 96.7% 93.9% 95.6%
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, which may (as of 4.0) have slashes to delinate folders containing the component.
 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    + PROPERTY_NAME_PATTERN + "))$";
 157   
 158    private static final int IMPLICIT_ID_PATTERN_ID_GROUP = 1;
 159   
 160    private static final int IMPLICIT_ID_PATTERN_TYPE_GROUP = 2;
 161   
 162    private static final int IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP = 4;
 163   
 164    private static final int IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP = 5;
 165   
 166    private Pattern _simpleIdPattern;
 167   
 168    private Pattern _implicitIdPattern;
 169   
 170    private PatternMatcher _patternMatcher;
 171   
 172    private IdAllocator _idAllocator = new IdAllocator();
 173   
 174    private ITemplateParserDelegate _delegate;
 175   
 176    /**
 177    * Identifies the template being parsed; used with error messages.
 178    */
 179   
 180    private Resource _resourceLocation;
 181   
 182    /**
 183    * Shared instance of {@link Location} used by all {@link TextToken} instances in the template.
 184    */
 185   
 186    private Location _templateLocation;
 187   
 188    /**
 189    * Location with in the resource for the current line.
 190    */
 191   
 192    private Location _currentLocation;
 193   
 194    /**
 195    * Local reference to the template data that is to be parsed.
 196    */
 197   
 198    private char[] _templateData;
 199   
 200    /**
 201    * List of Tag
 202    */
 203   
 204    private List _stack = new ArrayList();
 205   
 206    private static class Tag
 207    {
 208    // The element, i.e., <jwc> or virtually any other element (via jwcid attribute)
 209    String _tagName;
 210   
 211    // If true, the tag is a placeholder for a dynamic element
 212    boolean _component;
 213   
 214    // If true, the body of the tag is being ignored, and the
 215    // ignore flag is cleared when the close tag is reached
 216    boolean _ignoringBody;
 217   
 218    // If true, then the entire tag (and its body) is being ignored
 219    boolean _removeTag;
 220   
 221    // If true, then the tag must have a balanced closing tag.
 222    // This is always true for components.
 223    boolean _mustBalance;
 224   
 225    // The line on which the start tag exists
 226    int _line;
 227   
 228    // If true, then the parse ends when the closing tag is found.
 229    boolean _content;
 230   
 231  2524 Tag(String tagName, int line)
 232    {
 233  2524 _tagName = tagName;
 234  2524 _line = line;
 235    }
 236   
 237  2372 boolean match(String matchTagName)
 238    {
 239  2372 return _tagName.equalsIgnoreCase(matchTagName);
 240    }
 241    }
 242   
 243    /**
 244    * List of {@link TemplateToken}, this forms the ultimate response.
 245    */
 246   
 247    private List _tokens = new ArrayList();
 248   
 249    /**
 250    * The location of the 'cursor' within the template data. The advance() method moves this
 251    * forward.
 252    */
 253   
 254    private int _cursor;
 255   
 256    /**
 257    * The start of the current block of static text, or -1 if no block is active.
 258    */
 259   
 260    private int _blockStart;
 261   
 262    /**
 263    * The current line number; tracked by advance(). Starts at 1.
 264    */
 265   
 266    private int _line;
 267   
 268    /**
 269    * Set to true when the body of a tag is being ignored. This is typically used to skip over the
 270    * body of a tag when its corresponding component doesn't allow a body, or whe the special jwcid
 271    * of $remove$ is used.
 272    */
 273   
 274    private boolean _ignoring;
 275   
 276    /**
 277    * A {@link Map}of {@link String}s, used to store attributes collected while parsing a tag.
 278    */
 279   
 280    private Map _attributes = new HashMap();
 281   
 282    /**
 283    * A factory used to create template tokens.
 284    */
 285   
 286    private TemplateTokenFactory _factory;
 287   
 288  244 public TemplateParser()
 289    {
 290  244 Perl5Compiler compiler = new Perl5Compiler();
 291   
 292  244 try
 293    {
 294  244 _simpleIdPattern = compiler.compile(SIMPLE_ID_PATTERN);
 295  244 _implicitIdPattern = compiler.compile(IMPLICIT_ID_PATTERN);
 296    }
 297    catch (MalformedPatternException ex)
 298    {
 299  0 throw new ApplicationRuntimeException(ex);
 300    }
 301   
 302  244 _patternMatcher = new Perl5Matcher();
 303    }
 304   
 305    /**
 306    * Parses the template data into an array of {@link TemplateToken}s.
 307    * <p>
 308    * The parser is <i>decidedly </i> not threadsafe, so care should be taken that only a single
 309    * thread accesses it.
 310    *
 311    * @param templateData
 312    * the HTML template to parse. Some tokens will hold a reference to this array.
 313    * @param delegate
 314    * object that "knows" about defined components
 315    * @param resourceLocation
 316    * a description of where the template originated from, used with error messages.
 317    */
 318   
 319  362 public TemplateToken[] parse(char[] templateData, ITemplateParserDelegate delegate,
 320    Resource resourceLocation) throws TemplateParseException
 321    {
 322  362 try
 323    {
 324  362 beforeParse(templateData, delegate, resourceLocation);
 325   
 326  362 parse();
 327   
 328  334 return (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]);
 329    }
 330    finally
 331    {
 332  362 afterParse();
 333    }
 334    }
 335   
 336    /**
 337    * perform default initialization of the parser.
 338    */
 339   
 340  362 protected void beforeParse(char[] templateData, ITemplateParserDelegate delegate,
 341    Resource resourceLocation)
 342    {
 343  362 _templateData = templateData;
 344  362 _resourceLocation = resourceLocation;
 345  362 _templateLocation = new LocationImpl(resourceLocation);
 346  362 _delegate = delegate;
 347  362 _ignoring = false;
 348  362 _line = 1;
 349  362 _componentAttributeName = delegate.getComponentAttributeName();
 350    }
 351   
 352    /**
 353    * Perform default cleanup after parsing completes.
 354    */
 355   
 356  362 protected void afterParse()
 357    {
 358  362 _delegate = null;
 359  362 _templateData = null;
 360  362 _resourceLocation = null;
 361  362 _templateLocation = null;
 362  362 _currentLocation = null;
 363  362 _stack.clear();
 364  362 _tokens.clear();
 365  362 _attributes.clear();
 366  362 _idAllocator.clear();
 367    }
 368   
 369    /**
 370    * Used by the parser to report problems in the parse. Parsing <b>must </b> stop when a problem
 371    * is reported.
 372    * <p>
 373    * The default implementation simply throws an exception that contains the message and location
 374    * parameters.
 375    * <p>
 376    * Subclasses may override but <b>must </b> ensure they throw the required exception.
 377    *
 378    * @param message
 379    * @param location
 380    * @param line
 381    * ignored by the default impl
 382    * @param cursor
 383    * ignored by the default impl
 384    * @throws TemplateParseException
 385    * always thrown in order to terminate the parse.
 386    */
 387   
 388  28 protected void templateParseProblem(String message, Location location, int line, int cursor)
 389    throws TemplateParseException
 390    {
 391  28 throw new TemplateParseException(message, location);
 392    }
 393   
 394    /**
 395    * Used by the parser to report tapestry runtime specific problems in the parse. Parsing <b>must
 396    * </b> stop when a problem is reported.
 397    * <p>
 398    * The default implementation simply rethrows the exception.
 399    * <p>
 400    * Subclasses may override but <b>must </b> ensure they rethrow the exception.
 401    *
 402    * @param exception
 403    * @param line
 404    * ignored by the default impl
 405    * @param cursor
 406    * ignored by the default impl
 407    * @throws ApplicationRuntimeException
 408    * always rethrown in order to terminate the parse.
 409    */
 410   
 411  0 protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor)
 412    throws ApplicationRuntimeException
 413    {
 414  0 throw exception;
 415    }
 416   
 417    /**
 418    * Give subclasses access to the parse results.
 419    */
 420  0 protected List getTokens()
 421    {
 422  0 if (_tokens == null)
 423  0 return Collections.EMPTY_LIST;
 424   
 425  0 return _tokens;
 426    }
 427   
 428    /**
 429    * Checks to see if the next few characters match a given pattern.
 430    */
 431   
 432  28806 private boolean lookahead(char[] match)
 433    {
 434  28806 try
 435    {
 436  28806 for (int i = 0; i < match.length; i++)
 437    {
 438  38776 if (_templateData[_cursor + i] != match[i])
 439  26284 return false;
 440    }
 441   
 442    // Every character matched.
 443   
 444  2522 return true;
 445    }
 446    catch (IndexOutOfBoundsException ex)
 447    {
 448  0 return false;
 449    }
 450    }
 451   
 452    private static final char[] COMMENT_START = new char[]
 453    { '<', '!', '-', '-' };
 454   
 455    private static final char[] COMMENT_END = new char[]
 456    { '-', '-', '>' };
 457   
 458    private static final char[] CLOSE_TAG = new char[]
 459    { '<', '/' };
 460   
 461  362 protected void parse() throws TemplateParseException
 462    {
 463  362 _cursor = 0;
 464  362 _blockStart = -1;
 465  362 int length = _templateData.length;
 466   
 467  362 while (_cursor < length)
 468    {
 469  41026 if (_templateData[_cursor] != '<')
 470    {
 471  35452 if (_blockStart < 0 && !_ignoring)
 472  2168 _blockStart = _cursor;
 473   
 474  35452 advance();
 475  35452 continue;
 476    }
 477   
 478    // OK, start of something.
 479   
 480  5574 if (lookahead(CLOSE_TAG))
 481    {
 482  2244 closeTag();
 483  2238 continue;
 484    }
 485   
 486  3330 if (lookahead(COMMENT_START))
 487    {
 488  140 skipComment();
 489  138 continue;
 490    }
 491   
 492    // The start of some tag.
 493   
 494  3190 startTag();
 495    }
 496   
 497    // Usually there's some text at the end of the template (after the last closing tag) that
 498    // should
 499    // be added. Often the last few tags are static tags so we definately
 500    // need to end the text block.
 501   
 502  334 addTextToken(_templateData.length - 1);
 503    }
 504   
 505    /**
 506    * Advance forward in the document until the end of the comment is reached. In addition, skip
 507    * any whitespace following the comment.
 508    */
 509   
 510  140 private void skipComment() throws TemplateParseException
 511    {
 512  140 int length = _templateData.length;
 513  140 int startLine = _line;
 514   
 515  140 if (_blockStart < 0 && !_ignoring)
 516  34 _blockStart = _cursor;
 517   
 518  140 while (true)
 519    {
 520  19904 if (_cursor >= length)
 521  2 templateParseProblem(ParseMessages.commentNotEnded(startLine), new LocationImpl(
 522    _resourceLocation, startLine), startLine, _cursor);
 523   
 524  19902 if (lookahead(COMMENT_END))
 525  138 break;
 526   
 527    // Not the end of the comment, advance over it.
 528   
 529  19764 advance();
 530    }
 531   
 532  138 _cursor += COMMENT_END.length;
 533  138 advanceOverWhitespace();
 534    }
 535   
 536  3120 private void addTextToken(int end)
 537    {
 538    // No active block to add to.
 539   
 540  3120 if (_blockStart < 0)
 541  756 return;
 542   
 543  2364 if (_blockStart <= end)
 544    {
 545    // This seems odd, shouldn't the location be the current location? I guess
 546    // no errors are ever reported for a text token.
 547   
 548  2364 TemplateToken token = _factory.createTextToken(
 549    _templateData,
 550    _blockStart,
 551    end,
 552    _templateLocation);
 553   
 554  2364 _tokens.add(token);
 555    }
 556   
 557  2364 _blockStart = -1;
 558    }
 559   
 560    private static final int WAIT_FOR_ATTRIBUTE_NAME = 0;
 561   
 562    private static final int COLLECT_ATTRIBUTE_NAME = 1;
 563   
 564    private static final int ADVANCE_PAST_EQUALS = 2;
 565   
 566    private static final int WAIT_FOR_ATTRIBUTE_VALUE = 3;
 567   
 568    private static final int COLLECT_QUOTED_VALUE = 4;
 569   
 570    private static final int COLLECT_UNQUOTED_VALUE = 5;
 571   
 572  3190 private void startTag() throws TemplateParseException
 573    {
 574  3190 int cursorStart = _cursor;
 575  3190 int length = _templateData.length;
 576  3190 String tagName = null;
 577  3190 boolean endOfTag = false;
 578  3190 boolean emptyTag = false;
 579  3190 int startLine = _line;
 580  3190 Location startLocation = new LocationImpl(_resourceLocation, startLine);
 581   
 582  3190 tagBeginEvent(startLine, _cursor);
 583   
 584  3190 advance();
 585   
 586    // Collect the element type
 587   
 588  13802 while (_cursor < length)
 589    {
 590  13802 char ch = _templateData[_cursor];
 591   
 592  13802 if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
 593    {
 594  3190 tagName = new String(_templateData, cursorStart + 1, _cursor - cursorStart - 1);
 595   
 596  3190 break;
 597    }
 598   
 599  10612 advance();
 600    }
 601   
 602  3190 String attributeName = null;
 603  3190 int attributeNameStart = -1;
 604  3190 int attributeValueStart = -1;
 605  3190 int state = WAIT_FOR_ATTRIBUTE_NAME;
 606  3190 char quoteChar = 0;
 607   
 608  3190 _attributes.clear();
 609   
 610    // Collect each attribute
 611   
 612  3190 while (!endOfTag)
 613    {
 614  84588 if (_cursor >= length)
 615    {
 616  2 String message = (tagName == null) ? ParseMessages.unclosedUnknownTag(startLine)
 617    : ParseMessages.unclosedTag(tagName, startLine);
 618   
 619  2 templateParseProblem(message, startLocation, startLine, cursorStart);
 620    }
 621   
 622  84586 char ch = _templateData[_cursor];
 623   
 624  84586 switch (state)
 625    {
 626  12148 case WAIT_FOR_ATTRIBUTE_NAME:
 627   
 628    // Ignore whitespace before the next attribute name, while
 629    // looking for the end of the current tag.
 630   
 631  12148 if (ch == '/')
 632    {
 633  976 emptyTag = true;
 634  976 advance();
 635  976 break;
 636    }
 637   
 638  11172 if (ch == '>')
 639    {
 640  3182 endOfTag = true;
 641  3182 break;
 642    }
 643   
 644  7990 if (Character.isWhitespace(ch))
 645    {
 646  3884 advance();
 647  3884 break;
 648    }
 649   
 650    // Found non-whitespace, assume its the attribute name.
 651    // Note: could use a check here for non-alpha.
 652   
 653  4106 attributeNameStart = _cursor;
 654  4106 state = COLLECT_ATTRIBUTE_NAME;
 655  4106 advance();
 656  4106 break;
 657   
 658  21900 case COLLECT_ATTRIBUTE_NAME:
 659   
 660    // Looking for end of attribute name.
 661   
 662  21900 if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
 663    {
 664  4106 attributeName = new String(_templateData, attributeNameStart, _cursor
 665    - attributeNameStart);
 666   
 667  4106 state = ADVANCE_PAST_EQUALS;
 668  4106 break;
 669    }
 670   
 671    // Part of the attribute name
 672   
 673  17794 advance();
 674  17794 break;
 675   
 676  4392 case ADVANCE_PAST_EQUALS:
 677   
 678    // Looking for the '=' sign. May hit the end of the tag, or (for bare
 679    // attributes),
 680    // the next attribute name.
 681   
 682  4392 if (ch == '/' || ch == '>')
 683    {
 684    // A bare attribute, which is not interesting to
 685    // us.
 686   
 687  290 state = WAIT_FOR_ATTRIBUTE_NAME;
 688  290 break;
 689    }
 690   
 691  4102 if (Character.isWhitespace(ch))
 692    {
 693  286 advance();
 694  286 break;
 695    }
 696   
 697  3816 if (ch == '=')
 698    {
 699  3636 state = WAIT_FOR_ATTRIBUTE_VALUE;
 700  3636 quoteChar = 0;
 701  3636 attributeValueStart = -1;
 702  3636 advance();
 703  3636 break;
 704    }
 705   
 706    // Otherwise, an HTML style "bare" attribute (such as <select multiple>).
 707    // We aren't interested in those (we're just looking for the id or jwcid
 708    // attribute).
 709   
 710  180 state = WAIT_FOR_ATTRIBUTE_NAME;
 711  180 break;
 712   
 713  3642 case WAIT_FOR_ATTRIBUTE_VALUE:
 714   
 715  3642 if (ch == '/' || ch == '>')
 716  2 templateParseProblem(ParseMessages.missingAttributeValue(
 717    tagName,
 718    _line,
 719    attributeName), getCurrentLocation(), _line, _cursor);
 720   
 721    // Ignore whitespace between '=' and the attribute value. Also, look
 722    // for initial quote.
 723   
 724  3640 if (Character.isWhitespace(ch))
 725    {
 726  6 advance();
 727  6 break;
 728    }
 729   
 730  3634 if (ch == '\'' || ch == '"')
 731    {
 732  3632 quoteChar = ch;
 733   
 734  3632 state = COLLECT_QUOTED_VALUE;
 735  3632 advance();
 736  3632 attributeValueStart = _cursor;
 737  3632 attributeBeginEvent(attributeName, _line, attributeValueStart);
 738  3632 break;
 739    }
 740   
 741    // Not whitespace or quote, must be start of unquoted attribute.
 742   
 743  2 state = COLLECT_UNQUOTED_VALUE;
 744  2 attributeValueStart = _cursor;
 745  2 attributeBeginEvent(attributeName, _line, attributeValueStart);
 746  2 break;
 747   
 748  42492 case COLLECT_QUOTED_VALUE:
 749   
 750    // Start collecting the quoted attribute value. Stop at the matching quote
 751    // character,
 752    // unless bare, in which case, stop at the next whitespace.
 753   
 754  42492 if (ch == quoteChar)
 755    {
 756  3632 String attributeValue = new String(_templateData, attributeValueStart,
 757    _cursor - attributeValueStart);
 758   
 759  3632 attributeEndEvent(_cursor);
 760   
 761  3632 addAttributeIfUnique(tagName, attributeName, attributeValue);
 762   
 763    // Advance over the quote.
 764  3628 advance();
 765  3628 state = WAIT_FOR_ATTRIBUTE_NAME;
 766  3628 break;
 767    }
 768   
 769  38860 advance();
 770  38860 break;
 771   
 772  12 case COLLECT_UNQUOTED_VALUE:
 773   
 774    // An unquoted attribute value ends with whitespace
 775    // or the end of the enclosing tag.
 776   
 777  12 if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
 778    {
 779  2 String attributeValue = new String(_templateData, attributeValueStart,
 780    _cursor - attributeValueStart);
 781   
 782  2 attributeEndEvent(_cursor);
 783  2 addAttributeIfUnique(tagName, attributeName, attributeValue);
 784   
 785  2 state = WAIT_FOR_ATTRIBUTE_NAME;
 786  2 break;
 787    }
 788   
 789  10 advance();
 790  10 break;
 791    }
 792    }
 793   
 794  3182 tagEndEvent(_cursor);
 795   
 796    // Check for invisible localizations
 797   
 798  3182 String localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes);
 799  3182 String jwcId = findValueCaselessly(_componentAttributeName, _attributes);
 800   
 801  3182 if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null)
 802    {
 803  32 if (_ignoring)
 804  2 templateParseProblem(
 805    ParseMessages.componentMayNotBeIgnored(tagName, startLine),
 806    startLocation,
 807    startLine,
 808    cursorStart);
 809   
 810    // If the tag isn't empty, then create a Tag instance to ignore the
 811    // body of the tag.
 812   
 813  30 if (!emptyTag)
 814    {
 815  6 Tag tag = new Tag(tagName, startLine);
 816   
 817  6 tag._component = false;
 818  6 tag._removeTag = true;
 819  6 tag._ignoringBody = true;
 820  6 tag._mustBalance = true;
 821   
 822  6 _stack.add(tag);
 823   
 824    // Start ignoring content until the close tag.
 825   
 826  6 _ignoring = true;
 827    }
 828    else
 829    {
 830    // Cursor is at the closing carat, advance over it.
 831  24 advance();
 832    // TAPESTRY-359: *don't* skip whitespace
 833    }
 834   
 835    // End any open block.
 836   
 837  30 addTextToken(cursorStart - 1);
 838   
 839  30 boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes);
 840   
 841  30 Map attributes = filter(_attributes, new String[]
 842    { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME });
 843   
 844  30 TemplateToken token = _factory.createLocalizationToken(
 845    tagName,
 846    localizationKey,
 847    raw,
 848    attributes,
 849    startLocation);
 850   
 851  30 _tokens.add(token);
 852   
 853  30 return;
 854    }
 855   
 856  3150 if (jwcId != null)
 857    {
 858  1800 processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation);
 859  1790 return;
 860    }
 861   
 862    // A static tag (not a tag without a jwcid attribute).
 863    // We need to record this so that we can match close tags later.
 864   
 865  1350 if (!emptyTag)
 866    {
 867  1222 Tag tag = new Tag(tagName, startLine);
 868  1222 _stack.add(tag);
 869    }
 870   
 871    // If there wasn't an active block, then start one.
 872   
 873  1350 if (_blockStart < 0 && !_ignoring)
 874  80 _blockStart = cursorStart;
 875   
 876  1350 advance();
 877    }
 878   
 879    /**
 880    * @throws TemplateParseException
 881    * @since 4.0
 882    */
 883   
 884  3634 private void addAttributeIfUnique(String tagName, String attributeName, String attributeValue)
 885    throws TemplateParseException
 886    {
 887   
 888  3634 if (_attributes.containsKey(attributeName))
 889  4 templateParseProblem(
 890    ParseMessages.duplicateTagAttribute(tagName, _line, attributeName),
 891    getCurrentLocation(),
 892    _line,
 893    _cursor);
 894   
 895  3630 _attributes.put(attributeName, attributeValue);
 896    }
 897   
 898    /**
 899    * Processes a tag that is the open tag for a component (but also handles the $remove$ and
 900    * $content$ tags).
 901    */
 902   
 903    /**
 904    * Notify that the beginning of a tag has been detected.
 905    * <p>
 906    * Default implementation does nothing.
 907    */
 908  3190 protected void tagBeginEvent(int startLine, int cursorPosition)
 909    {
 910    }
 911   
 912    /**
 913    * Notify that the end of the current tag has been detected.
 914    * <p>
 915    * Default implementation does nothing.
 916    */
 917  3182 protected void tagEndEvent(int cursorPosition)
 918    {
 919    }
 920   
 921    /**
 922    * Notify that the beginning of an attribute value has been detected.
 923    * <p>
 924    * Default implementation does nothing.
 925    */
 926  3634 protected void attributeBeginEvent(String attributeName, int startLine, int cursorPosition)
 927    {
 928    }
 929   
 930    /**
 931    * Notify that the end of the current attribute value has been detected.
 932    * <p>
 933    * Default implementation does nothing.
 934    */
 935  3634 protected void attributeEndEvent(int cursorPosition)
 936    {
 937    }
 938   
 939  1800 private void processComponentStart(String tagName, String jwcId, boolean emptyTag,
 940    int startLine, int cursorStart, Location startLocation) throws TemplateParseException
 941    {
 942  1800 if (jwcId.equalsIgnoreCase(CONTENT_ID))
 943    {
 944  144 processContentTag(tagName, startLine, cursorStart, emptyTag);
 945   
 946  142 return;
 947    }
 948   
 949  1656 boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
 950   
 951  1656 if (_ignoring && !isRemoveId)
 952  4 templateParseProblem(
 953    ParseMessages.componentMayNotBeIgnored(tagName, startLine),
 954    startLocation,
 955    startLine,
 956    cursorStart);
 957   
 958  1652 String type = null;
 959  1652 boolean allowBody = false;
 960   
 961  1652 if (_patternMatcher.matches(jwcId, _implicitIdPattern))
 962    {
 963  958 MatchResult match = _patternMatcher.getMatch();
 964   
 965  958 jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
 966  958 type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);
 967   
 968  958 String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
 969  958 String simpleType = match.group(IMPLICIT_ID_PATTERN_SIMPLE_TYPE_GROUP);
 970   
 971    // If (and this is typical) no actual component id was specified,
 972    // then generate one on the fly.
 973    // The allocated id for anonymous components is
 974    // based on the simple (unprefixed) type, but starts
 975    // with a leading dollar sign to ensure no conflicts
 976    // with user defined component ids (which don't allow dollar signs
 977    // in the id).
 978    // New for 4.0: the component type may included slashes ('/'), but these
 979    // are not valid identifiers, so we convert them to '$'.
 980   
 981  958 if (jwcId == null)
 982  842 jwcId = _idAllocator.allocateId("$" + simpleType.replace('/', '$'));
 983   
 984  958 try
 985    {
 986  958 allowBody = _delegate.getAllowBody(libraryId, simpleType, startLocation);
 987    }
 988    catch (ApplicationRuntimeException e)
 989    {
 990    // give subclasses a chance to handle and rethrow
 991  0 templateParseProblem(e, startLine, cursorStart);
 992    }
 993   
 994    }
 995    else
 996    {
 997  694 if (!isRemoveId)
 998    {
 999  514 if (!_patternMatcher.matches(jwcId, _simpleIdPattern))
 1000  0 templateParseProblem(
 1001    ParseMessages.componentIdInvalid(tagName, startLine, jwcId),
 1002    startLocation,
 1003    startLine,
 1004    cursorStart);
 1005   
 1006  514 if (!_delegate.getKnownComponent(jwcId))
 1007  2 templateParseProblem(
 1008    ParseMessages.unknownComponentId(tagName, startLine, jwcId),
 1009    startLocation,
 1010    startLine,
 1011    cursorStart);
 1012   
 1013  512 try
 1014    {
 1015  512 allowBody = _delegate.getAllowBody(jwcId, startLocation);
 1016    }
 1017    catch (ApplicationRuntimeException e)
 1018    {
 1019    // give subclasses a chance to handle and rethrow
 1020  0 templateParseProblem(e, startLine, cursorStart);
 1021    }
 1022    }
 1023    }
 1024   
 1025    // Ignore the body if we're removing the entire tag,
 1026    // of if the corresponding component doesn't allow
 1027    // a body.
 1028   
 1029  1650 boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody);
 1030   
 1031  1650 if (_ignoring && ignoreBody)
 1032  2 templateParseProblem(ParseMessages.nestedIgnore(tagName, startLine), new LocationImpl(
 1033    _resourceLocation, startLine), startLine, cursorStart);
 1034   
 1035  1648 if (!emptyTag)
 1036  1154 pushNewTag(tagName, startLine, isRemoveId, ignoreBody);
 1037   
 1038    // End any open block.
 1039   
 1040  1648 addTextToken(cursorStart - 1);
 1041   
 1042  1648 if (!isRemoveId)
 1043    {
 1044  1470 addOpenToken(tagName, jwcId, type, startLocation);
 1045   
 1046  1470 if (emptyTag)
 1047  494 _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
 1048    }
 1049   
 1050  1648 advance();
 1051    }
 1052   
 1053  1154 private void pushNewTag(String tagName, int startLine, boolean isRemoveId, boolean ignoreBody)
 1054    {
 1055  1154 Tag tag = new Tag(tagName, startLine);
 1056   
 1057  1154 tag._component = !isRemoveId;
 1058  1154 tag._removeTag = isRemoveId;
 1059   
 1060  1154 tag._ignoringBody = ignoreBody;
 1061   
 1062  1154 _ignoring = tag._ignoringBody;
 1063   
 1064  1154 tag._mustBalance = true;
 1065   
 1066  1154 _stack.add(tag);
 1067    }
 1068   
 1069  144 private void processContentTag(String tagName, int startLine, int cursorStart, boolean emptyTag)
 1070    throws TemplateParseException
 1071    {
 1072  144 if (_ignoring)
 1073  2 templateParseProblem(
 1074    ParseMessages.contentBlockMayNotBeIgnored(tagName, startLine),
 1075    new LocationImpl(_resourceLocation, startLine),
 1076    startLine,
 1077    cursorStart);
 1078   
 1079  142 if (emptyTag)
 1080  0 templateParseProblem(
 1081    ParseMessages.contentBlockMayNotBeEmpty(tagName, startLine),
 1082    new LocationImpl(_resourceLocation, startLine),
 1083    startLine,
 1084    cursorStart);
 1085   
 1086  142 _tokens.clear();
 1087  142 _blockStart = -1;
 1088   
 1089  142 Tag tag = new Tag(tagName, startLine);
 1090   
 1091  142 tag._mustBalance = true;
 1092  142 tag._content = true;
 1093   
 1094  142 _stack.clear();
 1095  142 _stack.add(tag);
 1096   
 1097  142 advance();
 1098    }
 1099   
 1100  1470 private void addOpenToken(String tagName, String jwcId, String type, Location location)
 1101    {
 1102  1470 OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
 1103  1470 _tokens.add(token);
 1104   
 1105  1470 if (_attributes.isEmpty())
 1106  0 return;
 1107   
 1108  1470 Iterator i = _attributes.entrySet().iterator();
 1109  1470 while (i.hasNext())
 1110    {
 1111  2644 Map.Entry entry = (Map.Entry) i.next();
 1112   
 1113  2644 String key = (String) entry.getKey();
 1114   
 1115  2644 if (key.equalsIgnoreCase(_componentAttributeName))
 1116  1470 continue;
 1117   
 1118  1174 String value = (String) entry.getValue();
 1119   
 1120  1174 addAttributeToToken(token, key, value);
 1121    }
 1122    }
 1123   
 1124    /**
 1125    * Adds the attribute to the token (identifying prefixes and whatnot is now done downstream).
 1126    *
 1127    * @since 3.0
 1128    */
 1129   
 1130  1174 private void addAttributeToToken(OpenToken token, String name, String attributeValue)
 1131    {
 1132  1174 token.addAttribute(name, convertEntitiesToPlain(attributeValue));
 1133    }
 1134   
 1135    /**
 1136    * Invoked to handle a closing tag, i.e., &lt;/foo&gt;. When a tag closes, it will match against
 1137    * a tag on the open tag start. Preferably the top tag on the stack (if everything is well
 1138    * balanced), but this is HTML, not XML, so many tags won't balance.
 1139    * <p>
 1140    * Once the matching tag is located, the question is ... is the tag dynamic or static? If
 1141    * static, then the current text block is extended to include this close tag. If dynamic, then
 1142    * the current text block is ended (before the '&lt;' that starts the tag) and a close token is
 1143    * added.
 1144    * <p>
 1145    * In either case, the matching static element and anything above it is removed, and the cursor
 1146    * is left on the character following the '&gt;'.
 1147    */
 1148   
 1149  2244 private void closeTag() throws TemplateParseException
 1150    {
 1151  2244 int cursorStart = _cursor;
 1152  2244 int length = _templateData.length;
 1153  2244 int startLine = _line;
 1154   
 1155  2244 Location startLocation = getCurrentLocation();
 1156   
 1157  2244 _cursor += CLOSE_TAG.length;
 1158   
 1159  2244 int tagStart = _cursor;
 1160   
 1161  2244 while (true)
 1162    {
 1163  9162 if (_cursor >= length)
 1164  2 templateParseProblem(
 1165    ParseMessages.incompleteCloseTag(startLine),
 1166    startLocation,
 1167    startLine,
 1168    cursorStart);
 1169   
 1170  9160 char ch = _templateData[_cursor];
 1171   
 1172  9160 if (ch == '>')
 1173  2242 break;
 1174   
 1175  6918 advance();
 1176    }
 1177   
 1178  2242 String tagName = new String(_templateData, tagStart, _cursor - tagStart);
 1179   
 1180  2242 int stackPos = _stack.size() - 1;
 1181  2242 Tag tag = null;
 1182   
 1183  2242 while (stackPos >= 0)
 1184    {
 1185  2372 tag = (Tag) _stack.get(stackPos);
 1186   
 1187  2372 if (tag.match(tagName))
 1188  2238 break;
 1189   
 1190  134 if (tag._mustBalance)
 1191  2 templateParseProblem(ParseMessages.improperlyNestedCloseTag(
 1192    tagName,
 1193    startLine,
 1194    tag._tagName,
 1195    tag._line), startLocation, startLine, cursorStart);
 1196   
 1197  132 stackPos--;
 1198    }
 1199   
 1200  2240 if (stackPos < 0)
 1201  2 templateParseProblem(
 1202    ParseMessages.unmatchedCloseTag(tagName, startLine),
 1203    startLocation,
 1204    startLine,
 1205    cursorStart);
 1206   
 1207    // Special case for the content tag
 1208   
 1209  2238 if (tag._content)
 1210    {
 1211  140 addTextToken(cursorStart - 1);
 1212   
 1213    // Advance the cursor right to the end.
 1214   
 1215  140 _cursor = length;
 1216  140 _stack.clear();
 1217  140 return;
 1218    }
 1219   
 1220    // When a component closes, add a CLOSE tag.
 1221  2098 if (tag._component)
 1222    {
 1223  968 addTextToken(cursorStart - 1);
 1224   
 1225  968 _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
 1226    }
 1227    else
 1228    {
 1229    // The close of a static tag. Unless removing the tag
 1230    // entirely, make sure the block tag is part of a text block.
 1231   
 1232  1130 if (_blockStart < 0 && !tag._removeTag && !_ignoring)
 1233  188 _blockStart = cursorStart;
 1234    }
 1235   
 1236    // Remove all elements at stackPos or above.
 1237   
 1238  2098 for (int i = _stack.size() - 1; i >= stackPos; i--)
 1239  2200 _stack.remove(i);
 1240   
 1241    // Advance cursor past '>'
 1242   
 1243  2098 advance();
 1244   
 1245    // If editting out the tag (i.e., $remove$) then kill any whitespace.
 1246    // For components that simply don't contain a body, removeTag will
 1247    // be false.
 1248   
 1249  2098 if (tag._removeTag)
 1250  174 advanceOverWhitespace();
 1251   
 1252    // If we were ignoring the body of the tag, then clear the ignoring
 1253    // flag, since we're out of the body.
 1254   
 1255  2098 if (tag._ignoringBody)
 1256  396 _ignoring = false;
 1257    }
 1258   
 1259    /**
 1260    * Advances the cursor to the next character. If the end-of-line is reached, then increments the
 1261    * line counter.
 1262    */
 1263   
 1264  159290 private void advance()
 1265    {
 1266  159290 int length = _templateData.length;
 1267   
 1268  159290 if (_cursor >= length)
 1269  0 return;
 1270   
 1271  159290 char ch = _templateData[_cursor];
 1272   
 1273  159290 _cursor++;
 1274   
 1275  159290 if (ch == '\n')
 1276    {
 1277  5170 _line++;
 1278  5170 _currentLocation = null;
 1279  5170 return;
 1280    }
 1281   
 1282    // A \r, or a \r\n also counts as a new line.
 1283   
 1284  154120 if (ch == '\r')
 1285    {
 1286  806 _line++;
 1287  806 _currentLocation = null;
 1288   
 1289  806 if (_cursor < length && _templateData[_cursor] == '\n')
 1290  806 _cursor++;
 1291   
 1292  806 return;
 1293    }
 1294   
 1295    // Not an end-of-line character.
 1296   
 1297    }
 1298   
 1299  312 private void advanceOverWhitespace()
 1300    {
 1301  312 int length = _templateData.length;
 1302   
 1303  312 while (_cursor < length)
 1304    {
 1305  1584 char ch = _templateData[_cursor];
 1306  1584 if (!Character.isWhitespace(ch))
 1307  310 return;
 1308   
 1309  1274 advance();
 1310    }
 1311    }
 1312   
 1313    /**
 1314    * Returns a new Map that is a copy of the input Map with some key/value pairs removed. A list
 1315    * of keys is passed in and matching keys (caseless comparison) from the input Map are excluded
 1316    * from the output map. May return null (rather than return an empty Map).
 1317    */
 1318   
 1319  30 private Map filter(Map input, String[] removeKeys)
 1320    {
 1321  30 if (input == null || input.isEmpty())
 1322  0 return null;
 1323   
 1324  30 Map result = null;
 1325   
 1326  30 Iterator i = input.entrySet().iterator();
 1327   
 1328  30 nextkey: while (i.hasNext())
 1329    {
 1330  40 Map.Entry entry = (Map.Entry) i.next();
 1331   
 1332  40 String key = (String) entry.getKey();
 1333   
 1334  40 for (int j = 0; j < removeKeys.length; j++)
 1335    {
 1336  50 if (key.equalsIgnoreCase(removeKeys[j]))
 1337  34 continue nextkey;
 1338    }
 1339   
 1340  6 if (result == null)
 1341  4 result = new HashMap(input.size());
 1342   
 1343  6 result.put(key, entry.getValue());
 1344    }
 1345   
 1346  30 return result;
 1347    }
 1348   
 1349    /**
 1350    * Searches a Map for given key, caselessly. The Map is expected to consist of Strings for keys
 1351    * and values. Returns the value for the first key found that matches (caselessly) the input
 1352    * key. Returns null if no value found.
 1353    */
 1354   
 1355  6394 protected String findValueCaselessly(String key, Map map)
 1356    {
 1357  6394 String result = (String) map.get(key);
 1358   
 1359  6394 if (result != null)
 1360  1830 return result;
 1361   
 1362  4564 Iterator i = map.entrySet().iterator();
 1363  4564 while (i.hasNext())
 1364    {
 1365  4152 Map.Entry entry = (Map.Entry) i.next();
 1366   
 1367  4152 String entryKey = (String) entry.getKey();
 1368   
 1369  4152 if (entryKey.equalsIgnoreCase(key))
 1370  6 return (String) entry.getValue();
 1371    }
 1372   
 1373  4558 return null;
 1374    }
 1375   
 1376    /**
 1377    * Conversions needed by {@link #convertEntitiesToPlain(String)}
 1378    */
 1379   
 1380    private static final String[] CONVERSIONS =
 1381    { "&lt;", "<", "&gt;", ">", "&quot;", "\"", "&amp;", "&" };
 1382   
 1383    /**
 1384    * Provided a raw input string that has been recognized to be an expression, this removes excess
 1385    * white space and converts &amp;amp;;, &amp;quot;; &amp;lt;; and &amp;gt;; to their normal
 1386    * character values (otherwise its impossible to specify those values in expressions in the
 1387    * template).
 1388    */
 1389   
 1390  1174 private String convertEntitiesToPlain(String input)
 1391    {
 1392  1174 int inputLength = input.length();
 1393   
 1394  1174 StringBuffer buffer = new StringBuffer(inputLength);
 1395   
 1396  1174 int cursor = 0;
 1397   
 1398  1174 outer: while (cursor < inputLength)
 1399    {
 1400  17304 for (int i = 0; i < CONVERSIONS.length; i += 2)
 1401    {
 1402  69182 String entity = CONVERSIONS[i];
 1403  69182 int entityLength = entity.length();
 1404  69182 String value = CONVERSIONS[i + 1];
 1405   
 1406  69182 if (cursor + entityLength > inputLength)
 1407  16594 continue;
 1408   
 1409  52588 if (input.substring(cursor, cursor + entityLength).equals(entity))
 1410    {
 1411  30 buffer.append(value);
 1412  30 cursor += entityLength;
 1413  30 continue outer;
 1414    }
 1415    }
 1416   
 1417  17274 buffer.append(input.charAt(cursor));
 1418  17274 cursor++;
 1419    }
 1420   
 1421  1174 return buffer.toString().trim();
 1422    }
 1423   
 1424    /**
 1425    * Returns true if the map contains the given key (caseless search) and the value is "true"
 1426    * (caseless comparison).
 1427    */
 1428   
 1429  30 private boolean checkBoolean(String key, Map map)
 1430    {
 1431  30 String value = findValueCaselessly(key, map);
 1432   
 1433  30 if (value == null)
 1434  26 return false;
 1435   
 1436  4 return value.equalsIgnoreCase("true");
 1437    }
 1438   
 1439    /**
 1440    * Gets the current location within the file. This allows the location to be created only as
 1441    * needed, and multiple objects on the same line can share the same Location instance.
 1442    *
 1443    * @since 3.0
 1444    */
 1445   
 1446  3712 protected Location getCurrentLocation()
 1447    {
 1448  3712 if (_currentLocation == null)
 1449  2462 _currentLocation = new LocationImpl(_resourceLocation, _line);
 1450   
 1451  3712 return _currentLocation;
 1452    }
 1453   
 1454  244 public void setFactory(TemplateTokenFactory factory)
 1455    {
 1456  244 _factory = factory;
 1457    }
 1458   
 1459    }