Clover coverage report - Code Coverage for tapestry release 4.0-alpha-3
Coverage timestamp: Mon May 16 2005 09:05:49 EDT
file stats: LOC: 1,436   Methods: 32
NCLOC: 744   Classes: 2
30 day Evaluation Version distributed via the Maven Jar Repository. Clover is not free. You have 30 days to evaluate it. Please visit http://www.thecortex.net/clover to obtain a licensed version of Clover
 
 Source file Conditionals Statements Methods TOTAL
TemplateParser.java 94% 96.6% 93.8% 95.7%
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  1491
         Tag(String tagName, int line)
 231   
         {
 232  1491
             _tagName = tagName;
 233  1491
             _line = line;
 234   
         }
 235   
 
 236  1406
         boolean match(String matchTagName)
 237   
         {
 238  1406
             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  133
     public TemplateParser()
 288   
     {
 289  133
         Perl5Compiler compiler = new Perl5Compiler();
 290   
 
 291  133
         try
 292   
         {
 293  133
             _simpleIdPattern = compiler.compile(SIMPLE_ID_PATTERN);
 294  133
             _implicitIdPattern = compiler.compile(IMPLICIT_ID_PATTERN);
 295   
         }
 296   
         catch (MalformedPatternException ex)
 297   
         {
 298  0
             throw new ApplicationRuntimeException(ex);
 299   
         }
 300   
 
 301  133
         _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  202
     public TemplateToken[] parse(char[] templateData, ITemplateParserDelegate delegate,
 319   
             Resource resourceLocation) throws TemplateParseException
 320   
     {
 321  202
         try
 322   
         {
 323  202
             beforeParse(templateData, delegate, resourceLocation);
 324   
 
 325  202
             parse();
 326   
 
 327  190
             return (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]);
 328   
         }
 329   
         finally
 330   
         {
 331  202
             afterParse();
 332   
         }
 333   
     }
 334   
 
 335   
     /**
 336   
      * perform default initialization of the parser.
 337   
      */
 338   
 
 339  202
     protected void beforeParse(char[] templateData, ITemplateParserDelegate delegate,
 340   
             Resource resourceLocation)
 341   
     {
 342  202
         _templateData = templateData;
 343  202
         _resourceLocation = resourceLocation;
 344  202
         _templateLocation = new LocationImpl(resourceLocation);
 345  202
         _delegate = delegate;
 346  202
         _ignoring = false;
 347  202
         _line = 1;
 348  202
         _componentAttributeName = delegate.getComponentAttributeName();
 349   
     }
 350   
 
 351   
     /**
 352   
      * Perform default cleanup after parsing completes.
 353   
      */
 354   
 
 355  202
     protected void afterParse()
 356   
     {
 357  202
         _delegate = null;
 358  202
         _templateData = null;
 359  202
         _resourceLocation = null;
 360  202
         _templateLocation = null;
 361  202
         _currentLocation = null;
 362  202
         _stack.clear();
 363  202
         _tokens.clear();
 364  202
         _attributes.clear();
 365  202
         _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  17489
     private boolean lookahead(char[] match)
 432   
     {
 433  17489
         try
 434   
         {
 435  17489
             for (int i = 0; i < match.length; i++)
 436   
             {
 437  23345
                 if (_templateData[_cursor + i] != match[i])
 438  15993
                     return false;
 439   
             }
 440   
 
 441   
             // Every character matched.
 442   
 
 443  1496
             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  202
     protected void parse() throws TemplateParseException
 461   
     {
 462  202
         _cursor = 0;
 463  202
         _blockStart = -1;
 464  202
         int length = _templateData.length;
 465   
 
 466  202
         while (_cursor < length)
 467   
         {
 468  22552
             if (_templateData[_cursor] != '<')
 469   
             {
 470  19278
                 if (_blockStart < 0 && !_ignoring)
 471  1220
                     _blockStart = _cursor;
 472   
 
 473  19278
                 advance();
 474  19278
                 continue;
 475   
             }
 476   
 
 477   
             // OK, start of something.
 478   
 
 479  3274
             if (lookahead(CLOSE_TAG))
 480   
             {
 481  1329
                 closeTag();
 482  1326
                 continue;
 483   
             }
 484   
 
 485  1945
             if (lookahead(COMMENT_START))
 486   
             {
 487  84
                 skipComment();
 488  83
                 continue;
 489   
             }
 490   
 
 491   
             // The start of some tag.
 492   
 
 493  1861
             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  190
         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  84
     private void skipComment() throws TemplateParseException
 510   
     {
 511  84
         int length = _templateData.length;
 512  84
         int startLine = _line;
 513   
 
 514  84
         if (_blockStart < 0 && !_ignoring)
 515  20
             _blockStart = _cursor;
 516   
 
 517  84
         while (true)
 518   
         {
 519  12271
             if (_cursor >= length)
 520  1
                 templateParseProblem(ParseMessages.commentNotEnded(startLine), new LocationImpl(
 521   
                         _resourceLocation, startLine), startLine, _cursor);
 522   
 
 523  12270
             if (lookahead(COMMENT_END))
 524  83
                 break;
 525   
 
 526   
             // Not the end of the comment, advance over it.
 527   
 
 528  12187
             advance();
 529   
         }
 530   
 
 531  83
         _cursor += COMMENT_END.length;
 532  83
         advanceOverWhitespace();
 533   
     }
 534   
 
 535  1794
     private void addTextToken(int end)
 536   
     {
 537   
         // No active block to add to.
 538   
 
 539  1794
         if (_blockStart < 0)
 540  456
             return;
 541   
 
 542  1338
         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  1338
             TemplateToken token = _factory.createTextToken(
 548   
                     _templateData,
 549   
                     _blockStart,
 550   
                     end,
 551   
                     _templateLocation);
 552   
 
 553  1338
             _tokens.add(token);
 554   
         }
 555   
 
 556  1338
         _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  1861
     private void startTag() throws TemplateParseException
 572   
     {
 573  1861
         int cursorStart = _cursor;
 574  1861
         int length = _templateData.length;
 575  1861
         String tagName = null;
 576  1861
         boolean endOfTag = false;
 577  1861
         boolean emptyTag = false;
 578  1861
         int startLine = _line;
 579  1861
         Location startLocation = new LocationImpl(_resourceLocation, startLine);
 580   
 
 581  1861
         tagBeginEvent(startLine, _cursor);
 582   
 
 583  1861
         advance();
 584   
 
 585   
         // Collect the element type
 586   
 
 587  7919
         while (_cursor < length)
 588   
         {
 589  7919
             char ch = _templateData[_cursor];
 590   
 
 591  7919
             if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
 592   
             {
 593  1861
                 tagName = new String(_templateData, cursorStart + 1, _cursor - cursorStart - 1);
 594   
 
 595  1861
                 break;
 596   
             }
 597   
 
 598  6058
             advance();
 599   
         }
 600   
 
 601  1861
         String attributeName = null;
 602  1861
         int attributeNameStart = -1;
 603  1861
         int attributeValueStart = -1;
 604  1861
         int state = WAIT_FOR_ATTRIBUTE_NAME;
 605  1861
         char quoteChar = 0;
 606   
 
 607  1861
         _attributes.clear();
 608   
 
 609   
         // Collect each attribute
 610   
 
 611  1861
         while (!endOfTag)
 612   
         {
 613  46786
             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  46785
             char ch = _templateData[_cursor];
 622   
 
 623  46785
             switch (state)
 624   
             {
 625   
                 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  6745
                     if (ch == '/')
 631   
                     {
 632  527
                         emptyTag = true;
 633  527
                         advance();
 634  527
                         break;
 635   
                     }
 636   
 
 637  6218
                     if (ch == '>')
 638   
                     {
 639  1859
                         endOfTag = true;
 640  1859
                         break;
 641   
                     }
 642   
 
 643  4359
                     if (Character.isWhitespace(ch))
 644   
                     {
 645  2089
                         advance();
 646  2089
                         break;
 647   
                     }
 648   
 
 649   
                     // Found non-whitespace, assume its the attribute name.
 650   
                     // Note: could use a check here for non-alpha.
 651   
 
 652  2270
                     attributeNameStart = _cursor;
 653  2270
                     state = COLLECT_ATTRIBUTE_NAME;
 654  2270
                     advance();
 655  2270
                     break;
 656   
 
 657   
                 case COLLECT_ATTRIBUTE_NAME:
 658   
 
 659   
                     // Looking for end of attribute name.
 660   
 
 661  12118
                     if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
 662   
                     {
 663  2270
                         attributeName = new String(_templateData, attributeNameStart, _cursor
 664   
                                 - attributeNameStart);
 665   
 
 666  2270
                         state = ADVANCE_PAST_EQUALS;
 667  2270
                         break;
 668   
                     }
 669   
 
 670   
                     // Part of the attribute name
 671   
 
 672  9848
                     advance();
 673  9848
                     break;
 674   
 
 675   
                 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  2416
                     if (ch == '/' || ch == '>')
 682   
                     {
 683   
                         // A bare attribute, which is not interesting to
 684   
                         // us.
 685   
 
 686  147
                         state = WAIT_FOR_ATTRIBUTE_NAME;
 687  147
                         break;
 688   
                     }
 689   
 
 690  2269
                     if (Character.isWhitespace(ch))
 691   
                     {
 692  146
                         advance();
 693  146
                         break;
 694   
                     }
 695   
 
 696  2123
                     if (ch == '=')
 697   
                     {
 698  2030
                         state = WAIT_FOR_ATTRIBUTE_VALUE;
 699  2030
                         quoteChar = 0;
 700  2030
                         attributeValueStart = -1;
 701  2030
                         advance();
 702  2030
                         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  93
                     state = WAIT_FOR_ATTRIBUTE_NAME;
 710  93
                     break;
 711   
 
 712   
                 case WAIT_FOR_ATTRIBUTE_VALUE:
 713   
 
 714  2033
                     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  2032
                     if (Character.isWhitespace(ch))
 724   
                     {
 725  3
                         advance();
 726  3
                         break;
 727   
                     }
 728   
 
 729  2029
                     if (ch == '\'' || ch == '"')
 730   
                     {
 731  1998
                         quoteChar = ch;
 732   
 
 733  1998
                         state = COLLECT_QUOTED_VALUE;
 734  1998
                         advance();
 735  1998
                         attributeValueStart = _cursor;
 736  1998
                         attributeBeginEvent(attributeName, _line, attributeValueStart);
 737  1998
                         break;
 738   
                     }
 739   
 
 740   
                     // Not whitespace or quote, must be start of unquoted attribute.
 741   
 
 742  31
                     state = COLLECT_UNQUOTED_VALUE;
 743  31
                     attributeValueStart = _cursor;
 744  31
                     attributeBeginEvent(attributeName, _line, attributeValueStart);
 745  31
                     break;
 746   
 
 747   
                 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  23407
                     if (ch == quoteChar)
 754   
                     {
 755  1998
                         String attributeValue = new String(_templateData, attributeValueStart,
 756   
                                 _cursor - attributeValueStart);
 757   
 
 758  1998
                         _attributes.put(attributeName, attributeValue);
 759  1998
                         attributeEndEvent(_cursor);
 760   
 
 761   
                         // Advance over the quote.
 762  1998
                         advance();
 763  1998
                         state = WAIT_FOR_ATTRIBUTE_NAME;
 764  1998
                         break;
 765   
                     }
 766   
 
 767  21409
                     advance();
 768  21409
                     break;
 769   
 
 770   
                 case COLLECT_UNQUOTED_VALUE:
 771   
 
 772   
                     // An unquoted attribute value ends with whitespace
 773   
                     // or the end of the enclosing tag.
 774   
 
 775  66
                     if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
 776   
                     {
 777  31
                         String attributeValue = new String(_templateData, attributeValueStart,
 778   
                                 _cursor - attributeValueStart);
 779   
 
 780  31
                         _attributes.put(attributeName, attributeValue);
 781  31
                         attributeEndEvent(_cursor);
 782   
 
 783  31
                         state = WAIT_FOR_ATTRIBUTE_NAME;
 784  31
                         break;
 785   
                     }
 786   
 
 787  35
                     advance();
 788  35
                     break;
 789   
             }
 790   
         }
 791   
 
 792  1859
         tagEndEvent(_cursor);
 793   
 
 794   
         // Check for invisible localizations
 795   
 
 796  1859
         String localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes);
 797  1859
         String jwcId = findValueCaselessly(_componentAttributeName, _attributes);
 798   
 
 799  1859
         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  1843
         if (jwcId != null)
 855   
         {
 856  1032
             processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation);
 857  1027
             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  811
         if (!emptyTag)
 864   
         {
 865  727
             Tag tag = new Tag(tagName, startLine);
 866  727
             _stack.add(tag);
 867   
         }
 868   
 
 869   
         // If there wasn't an active block, then start one.
 870   
 
 871  811
         if (_blockStart < 0 && !_ignoring)
 872  44
             _blockStart = cursorStart;
 873   
 
 874  811
         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  1861
     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  1859
     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  2029
     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  2029
     protected void attributeEndEvent(int cursorPosition)
 915   
     {
 916   
     }
 917   
 
 918  1032
     private void processComponentStart(String tagName, String jwcId, boolean emptyTag,
 919   
             int startLine, int cursorStart, Location startLocation) throws TemplateParseException
 920   
     {
 921  1032
         if (jwcId.equalsIgnoreCase(CONTENT_ID))
 922   
         {
 923  85
             processContentTag(tagName, startLine, cursorStart, emptyTag);
 924   
 
 925  84
             return;
 926   
         }
 927   
 
 928  947
         boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
 929   
 
 930  947
         if (_ignoring && !isRemoveId)
 931  2
             templateParseProblem(
 932   
                     ParseMessages.componentMayNotBeIgnored(tagName, startLine),
 933   
                     startLocation,
 934   
                     startLine,
 935   
                     cursorStart);
 936   
 
 937  945
         String type = null;
 938  945
         boolean allowBody = false;
 939   
 
 940  945
         if (_patternMatcher.matches(jwcId, _implicitIdPattern))
 941   
         {
 942  515
             MatchResult match = _patternMatcher.getMatch();
 943   
 
 944  515
             jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
 945  515
             type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);
 946   
 
 947  515
             String libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
 948  515
             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  515
             if (jwcId == null)
 959  445
                 jwcId = _idAllocator.allocateId("$" + simpleType);
 960   
 
 961  515
             try
 962   
             {
 963  515
                 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  430
             if (!isRemoveId)
 975   
             {
 976  319
                 if (!_patternMatcher.matches(jwcId, _simpleIdPattern))
 977  0
                     templateParseProblem(
 978   
                             ParseMessages.componentIdInvalid(tagName, startLine, jwcId),
 979   
                             startLocation,
 980   
                             startLine,
 981   
                             cursorStart);
 982   
 
 983  319
                 if (!_delegate.getKnownComponent(jwcId))
 984  1
                     templateParseProblem(
 985   
                             ParseMessages.unknownComponentId(tagName, startLine, jwcId),
 986   
                             startLocation,
 987   
                             startLine,
 988   
                             cursorStart);
 989   
 
 990  318
                 try
 991   
                 {
 992  318
                     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  944
         boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody);
 1007   
 
 1008  944
         if (_ignoring && ignoreBody)
 1009  1
             templateParseProblem(ParseMessages.nestedIgnore(tagName, startLine), new LocationImpl(
 1010   
                     _resourceLocation, startLine), startLine, cursorStart);
 1011   
 
 1012  943
         if (!emptyTag)
 1013  677
             pushNewTag(tagName, startLine, isRemoveId, ignoreBody);
 1014   
 
 1015   
         // End any open block.
 1016   
 
 1017  943
         addTextToken(cursorStart - 1);
 1018   
 
 1019  943
         if (!isRemoveId)
 1020   
         {
 1021  833
             addOpenToken(tagName, jwcId, type, startLocation);
 1022   
 
 1023  833
             if (emptyTag)
 1024  266
                 _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
 1025   
         }
 1026   
 
 1027  943
         advance();
 1028   
     }
 1029   
 
 1030  677
     private void pushNewTag(String tagName, int startLine, boolean isRemoveId, boolean ignoreBody)
 1031   
     {
 1032  677
         Tag tag = new Tag(tagName, startLine);
 1033   
 
 1034  677
         tag._component = !isRemoveId;
 1035  677
         tag._removeTag = isRemoveId;
 1036   
 
 1037  677
         tag._ignoringBody = ignoreBody;
 1038   
 
 1039  677
         _ignoring = tag._ignoringBody;
 1040   
 
 1041  677
         tag._mustBalance = true;
 1042   
 
 1043  677
         _stack.add(tag);
 1044   
     }
 1045   
 
 1046  85
     private void processContentTag(String tagName, int startLine, int cursorStart, boolean emptyTag)
 1047   
             throws TemplateParseException
 1048   
     {
 1049  85
         if (_ignoring)
 1050  1
             templateParseProblem(
 1051   
                     ParseMessages.contentBlockMayNotBeIgnored(tagName, startLine),
 1052   
                     new LocationImpl(_resourceLocation, startLine),
 1053   
                     startLine,
 1054   
                     cursorStart);
 1055   
 
 1056  84
         if (emptyTag)
 1057  0
             templateParseProblem(
 1058   
                     ParseMessages.contentBlockMayNotBeEmpty(tagName, startLine),
 1059   
                     new LocationImpl(_resourceLocation, startLine),
 1060   
                     startLine,
 1061   
                     cursorStart);
 1062   
 
 1063  84
         _tokens.clear();
 1064  84
         _blockStart = -1;
 1065   
 
 1066  84
         Tag tag = new Tag(tagName, startLine);
 1067   
 
 1068  84
         tag._mustBalance = true;
 1069  84
         tag._content = true;
 1070   
 
 1071  84
         _stack.clear();
 1072  84
         _stack.add(tag);
 1073   
 
 1074  84
         advance();
 1075   
     }
 1076   
 
 1077  833
     private void addOpenToken(String tagName, String jwcId, String type, Location location)
 1078   
     {
 1079  833
         OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
 1080  833
         _tokens.add(token);
 1081   
 
 1082  833
         if (_attributes.isEmpty())
 1083  0
             return;
 1084   
 
 1085  833
         Iterator i = _attributes.entrySet().iterator();
 1086  833
         while (i.hasNext())
 1087   
         {
 1088  1461
             Map.Entry entry = (Map.Entry) i.next();
 1089   
 
 1090  1461
             String key = (String) entry.getKey();
 1091   
 
 1092  1461
             if (key.equalsIgnoreCase(_componentAttributeName))
 1093  833
                 continue;
 1094   
 
 1095  628
             String value = (String) entry.getValue();
 1096   
 
 1097  628
             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  628
     private void addAttributeToToken(OpenToken token, String name, String attributeValue)
 1108   
     {
 1109  628
         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  1329
     private void closeTag() throws TemplateParseException
 1127   
     {
 1128  1329
         int cursorStart = _cursor;
 1129  1329
         int length = _templateData.length;
 1130  1329
         int startLine = _line;
 1131   
 
 1132  1329
         Location startLocation = getCurrentLocation();
 1133   
 
 1134  1329
         _cursor += CLOSE_TAG.length;
 1135   
 
 1136  1329
         int tagStart = _cursor;
 1137   
 
 1138  1329
         while (true)
 1139   
         {
 1140  5358
             if (_cursor >= length)
 1141  1
                 templateParseProblem(
 1142   
                         ParseMessages.incompleteCloseTag(startLine),
 1143   
                         startLocation,
 1144   
                         startLine,
 1145   
                         cursorStart);
 1146   
 
 1147  5357
             char ch = _templateData[_cursor];
 1148   
 
 1149  5357
             if (ch == '>')
 1150  1328
                 break;
 1151   
 
 1152  4029
             advance();
 1153   
         }
 1154   
 
 1155  1328
         String tagName = new String(_templateData, tagStart, _cursor - tagStart);
 1156   
 
 1157  1328
         int stackPos = _stack.size() - 1;
 1158  1328
         Tag tag = null;
 1159   
 
 1160  1328
         while (stackPos >= 0)
 1161   
         {
 1162  1406
             tag = (Tag) _stack.get(stackPos);
 1163   
 
 1164  1406
             if (tag.match(tagName))
 1165  1326
                 break;
 1166   
 
 1167  80
             if (tag._mustBalance)
 1168  1
                 templateParseProblem(ParseMessages.improperlyNestedCloseTag(
 1169   
                         tagName,
 1170   
                         startLine,
 1171   
                         tag._tagName,
 1172   
                         tag._line), startLocation, startLine, cursorStart);
 1173   
 
 1174  79
             stackPos--;
 1175   
         }
 1176   
 
 1177  1327
         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  1326
         if (tag._content)
 1187   
         {
 1188  83
             addTextToken(cursorStart - 1);
 1189   
 
 1190   
             // Advance the cursor right to the end.
 1191   
 
 1192  83
             _cursor = length;
 1193  83
             _stack.clear();
 1194  83
             return;
 1195   
         }
 1196   
 
 1197   
         // When a component closes, add a CLOSE tag.
 1198  1243
         if (tag._component)
 1199   
         {
 1200  563
             addTextToken(cursorStart - 1);
 1201   
 
 1202  563
             _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  680
             if (_blockStart < 0 && !tag._removeTag && !_ignoring)
 1210  114
                 _blockStart = cursorStart;
 1211   
         }
 1212   
 
 1213   
         // Remove all elements at stackPos or above.
 1214   
 
 1215  1243
         for (int i = _stack.size() - 1; i >= stackPos; i--)
 1216  1304
             _stack.remove(i);
 1217   
 
 1218   
         // Advance cursor past '>'
 1219   
 
 1220  1243
         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  1243
         if (tag._removeTag)
 1227  108
             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  1243
         if (tag._ignoringBody)
 1233  241
             _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  89648
     private void advance()
 1242   
     {
 1243  89648
         int length = _templateData.length;
 1244   
 
 1245  89648
         if (_cursor >= length)
 1246  0
             return;
 1247   
 
 1248  89648
         char ch = _templateData[_cursor];
 1249   
 
 1250  89648
         _cursor++;
 1251   
 
 1252  89648
         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  89379
         if (ch == '\r')
 1262   
         {
 1263  3215
             _line++;
 1264  3215
             _currentLocation = null;
 1265   
 
 1266  3215
             if (_cursor < length && _templateData[_cursor] == '\n')
 1267  3214
                 _cursor++;
 1268   
 
 1269  3215
             return;
 1270   
         }
 1271   
 
 1272   
         // Not an end-of-line character.
 1273   
 
 1274   
     }
 1275   
 
 1276  203
     private void advanceOverWhitespace()
 1277   
     {
 1278  203
         int length = _templateData.length;
 1279   
 
 1280  203
         while (_cursor < length)
 1281   
         {
 1282  987
             char ch = _templateData[_cursor];
 1283  987
             if (!Character.isWhitespace(ch))
 1284  198
                 return;
 1285   
 
 1286  789
             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  3733
     protected String findValueCaselessly(String key, Map map)
 1333   
     {
 1334  3733
         String result = (String) map.get(key);
 1335   
 
 1336  3733
         if (result != null)
 1337  1047
             return result;
 1338   
 
 1339  2686
         Iterator i = map.entrySet().iterator();
 1340  2686
         while (i.hasNext())
 1341   
         {
 1342  2350
             Map.Entry entry = (Map.Entry) i.next();
 1343   
 
 1344  2350
             String entryKey = (String) entry.getKey();
 1345   
 
 1346  2350
             if (entryKey.equalsIgnoreCase(key))
 1347  3
                 return (String) entry.getValue();
 1348   
         }
 1349   
 
 1350  2683
         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  628
     private String convertEntitiesToPlain(String input)
 1368   
     {
 1369  628
         int inputLength = input.length();
 1370   
 
 1371  628
         StringBuffer buffer = new StringBuffer(inputLength);
 1372   
 
 1373  628
         int cursor = 0;
 1374   
 
 1375  628
         outer: while (cursor < inputLength)
 1376   
         {
 1377  8772
             for (int i = 0; i < CONVERSIONS.length; i += 2)
 1378   
             {
 1379  35071
                 String entity = CONVERSIONS[i];
 1380  35071
                 int entityLength = entity.length();
 1381  35071
                 String value = CONVERSIONS[i + 1];
 1382   
 
 1383  35071
                 if (cursor + entityLength > inputLength)
 1384  8855
                     continue;
 1385   
 
 1386  26216
                 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  8757
             buffer.append(input.charAt(cursor));
 1395  8757
             cursor++;
 1396   
         }
 1397   
 
 1398  628
         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  2159
     protected Location getCurrentLocation()
 1424   
     {
 1425  2159
         if (_currentLocation == null)
 1426  1435
             _currentLocation = new LocationImpl(_resourceLocation, _line);
 1427   
 
 1428  2159
         return _currentLocation;
 1429   
     }
 1430   
 
 1431  133
     public void setFactory(TemplateTokenFactory factory)
 1432   
     {
 1433  133
         _factory = factory;
 1434   
     }
 1435   
 
 1436   
 }