KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > tapestry > parse > TemplateParser


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 JavaDoc;
18 import java.util.Collections JavaDoc;
19 import java.util.HashMap JavaDoc;
20 import java.util.Iterator JavaDoc;
21 import java.util.List JavaDoc;
22 import java.util.Map JavaDoc;
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 JavaDoc 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 JavaDoc 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 JavaDoc 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 JavaDoc RAW_ATTRIBUTE_NAME = "raw";
126
127     /**
128      * Attribute name used to identify components.
129      *
130      * @since 4.0
131      */

132
133     private String JavaDoc _componentAttributeName;
134
135     private static final String JavaDoc 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 JavaDoc 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 JavaDoc 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 JavaDoc _stack = new ArrayList JavaDoc();
204
205     private static class Tag
206     {
207         // The element, i.e., <jwc> or virtually any other element (via jwcid attribute)
208
String JavaDoc _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         Tag(String JavaDoc tagName, int line)
231         {
232             _tagName = tagName;
233             _line = line;
234         }
235
236         boolean match(String JavaDoc matchTagName)
237         {
238             return _tagName.equalsIgnoreCase(matchTagName);
239         }
240     }
241
242     /**
243      * List of {@link TemplateToken}, this forms the ultimate response.
244      */

245
246     private List JavaDoc _tokens = new ArrayList JavaDoc();
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 JavaDoc _attributes = new HashMap JavaDoc();
280
281     /**
282      * A factory used to create template tokens.
283      */

284
285     private TemplateTokenFactory _factory;
286
287     public TemplateParser()
288     {
289         Perl5Compiler compiler = new Perl5Compiler();
290
291         try
292         {
293             _simpleIdPattern = compiler.compile(SIMPLE_ID_PATTERN);
294             _implicitIdPattern = compiler.compile(IMPLICIT_ID_PATTERN);
295         }
296         catch (MalformedPatternException ex)
297         {
298             throw new ApplicationRuntimeException(ex);
299         }
300
301         _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     public TemplateToken[] parse(char[] templateData, ITemplateParserDelegate delegate,
319             Resource resourceLocation) throws TemplateParseException
320     {
321         try
322         {
323             beforeParse(templateData, delegate, resourceLocation);
324
325             parse();
326
327             return (TemplateToken[]) _tokens.toArray(new TemplateToken[_tokens.size()]);
328         }
329         finally
330         {
331             afterParse();
332         }
333     }
334
335     /**
336      * perform default initialization of the parser.
337      */

338
339     protected void beforeParse(char[] templateData, ITemplateParserDelegate delegate,
340             Resource resourceLocation)
341     {
342         _templateData = templateData;
343         _resourceLocation = resourceLocation;
344         _templateLocation = new LocationImpl(resourceLocation);
345         _delegate = delegate;
346         _ignoring = false;
347         _line = 1;
348         _componentAttributeName = delegate.getComponentAttributeName();
349     }
350
351     /**
352      * Perform default cleanup after parsing completes.
353      */

354
355     protected void afterParse()
356     {
357         _delegate = null;
358         _templateData = null;
359         _resourceLocation = null;
360         _templateLocation = null;
361         _currentLocation = null;
362         _stack.clear();
363         _tokens.clear();
364         _attributes.clear();
365         _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     protected void templateParseProblem(String JavaDoc message, Location location, int line, int cursor)
388             throws TemplateParseException
389     {
390         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     protected void templateParseProblem(ApplicationRuntimeException exception, int line, int cursor)
411             throws ApplicationRuntimeException
412     {
413         throw exception;
414     }
415
416     /**
417      * Give subclasses access to the parse results.
418      */

419     protected List JavaDoc getTokens()
420     {
421         if (_tokens == null)
422             return Collections.EMPTY_LIST;
423
424         return _tokens;
425     }
426
427     /**
428      * Checks to see if the next few characters match a given pattern.
429      */

430
431     private boolean lookahead(char[] match)
432     {
433         try
434         {
435             for (int i = 0; i < match.length; i++)
436             {
437                 if (_templateData[_cursor + i] != match[i])
438                     return false;
439             }
440
441             // Every character matched.
442

443             return true;
444         }
445         catch (IndexOutOfBoundsException JavaDoc ex)
446         {
447             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     protected void parse() throws TemplateParseException
461     {
462         _cursor = 0;
463         _blockStart = -1;
464         int length = _templateData.length;
465
466         while (_cursor < length)
467         {
468             if (_templateData[_cursor] != '<')
469             {
470                 if (_blockStart < 0 && !_ignoring)
471                     _blockStart = _cursor;
472
473                 advance();
474                 continue;
475             }
476
477             // OK, start of something.
478

479             if (lookahead(CLOSE_TAG))
480             {
481                 closeTag();
482                 continue;
483             }
484
485             if (lookahead(COMMENT_START))
486             {
487                 skipComment();
488                 continue;
489             }
490
491             // The start of some tag.
492

493             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         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     private void skipComment() throws TemplateParseException
510     {
511         int length = _templateData.length;
512         int startLine = _line;
513
514         if (_blockStart < 0 && !_ignoring)
515             _blockStart = _cursor;
516
517         while (true)
518         {
519             if (_cursor >= length)
520                 templateParseProblem(ParseMessages.commentNotEnded(startLine), new LocationImpl(
521                         _resourceLocation, startLine), startLine, _cursor);
522
523             if (lookahead(COMMENT_END))
524                 break;
525
526             // Not the end of the comment, advance over it.
527

528             advance();
529         }
530
531         _cursor += COMMENT_END.length;
532         advanceOverWhitespace();
533     }
534
535     private void addTextToken(int end)
536     {
537         // No active block to add to.
538

539         if (_blockStart < 0)
540             return;
541
542         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             TemplateToken token = _factory.createTextToken(
548                     _templateData,
549                     _blockStart,
550                     end,
551                     _templateLocation);
552
553             _tokens.add(token);
554         }
555
556         _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     private void startTag() throws TemplateParseException
572     {
573         int cursorStart = _cursor;
574         int length = _templateData.length;
575         String JavaDoc tagName = null;
576         boolean endOfTag = false;
577         boolean emptyTag = false;
578         int startLine = _line;
579         Location startLocation = new LocationImpl(_resourceLocation, startLine);
580
581         tagBeginEvent(startLine, _cursor);
582
583         advance();
584
585         // Collect the element type
586

587         while (_cursor < length)
588         {
589             char ch = _templateData[_cursor];
590
591             if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
592             {
593                 tagName = new String JavaDoc(_templateData, cursorStart + 1, _cursor - cursorStart - 1);
594
595                 break;
596             }
597
598             advance();
599         }
600
601         String JavaDoc attributeName = null;
602         int attributeNameStart = -1;
603         int attributeValueStart = -1;
604         int state = WAIT_FOR_ATTRIBUTE_NAME;
605         char quoteChar = 0;
606
607         _attributes.clear();
608
609         // Collect each attribute
610

611         while (!endOfTag)
612         {
613             if (_cursor >= length)
614             {
615                 String JavaDoc message = (tagName == null) ? ParseMessages.unclosedUnknownTag(startLine)
616                         : ParseMessages.unclosedTag(tagName, startLine);
617
618                 templateParseProblem(message, startLocation, startLine, cursorStart);
619             }
620
621             char ch = _templateData[_cursor];
622
623             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                     if (ch == '/')
631                     {
632                         emptyTag = true;
633                         advance();
634                         break;
635                     }
636
637                     if (ch == '>')
638                     {
639                         endOfTag = true;
640                         break;
641                     }
642
643                     if (Character.isWhitespace(ch))
644                     {
645                         advance();
646                         break;
647                     }
648
649                     // Found non-whitespace, assume its the attribute name.
650
// Note: could use a check here for non-alpha.
651

652                     attributeNameStart = _cursor;
653                     state = COLLECT_ATTRIBUTE_NAME;
654                     advance();
655                     break;
656
657                 case COLLECT_ATTRIBUTE_NAME:
658
659                     // Looking for end of attribute name.
660

661                     if (ch == '=' || ch == '/' || ch == '>' || Character.isWhitespace(ch))
662                     {
663                         attributeName = new String JavaDoc(_templateData, attributeNameStart, _cursor
664                                 - attributeNameStart);
665
666                         state = ADVANCE_PAST_EQUALS;
667                         break;
668                     }
669
670                     // Part of the attribute name
671

672                     advance();
673                     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                     if (ch == '/' || ch == '>')
682                     {
683                         // A bare attribute, which is not interesting to
684
// us.
685

686                         state = WAIT_FOR_ATTRIBUTE_NAME;
687                         break;
688                     }
689
690                     if (Character.isWhitespace(ch))
691                     {
692                         advance();
693                         break;
694                     }
695
696                     if (ch == '=')
697                     {
698                         state = WAIT_FOR_ATTRIBUTE_VALUE;
699                         quoteChar = 0;
700                         attributeValueStart = -1;
701                         advance();
702                         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                     state = WAIT_FOR_ATTRIBUTE_NAME;
710                     break;
711
712                 case WAIT_FOR_ATTRIBUTE_VALUE:
713
714                     if (ch == '/' || ch == '>')
715                         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                     if (Character.isWhitespace(ch))
724                     {
725                         advance();
726                         break;
727                     }
728
729                     if (ch == '\'' || ch == '"')
730                     {
731                         quoteChar = ch;
732
733                         state = COLLECT_QUOTED_VALUE;
734                         advance();
735                         attributeValueStart = _cursor;
736                         attributeBeginEvent(attributeName, _line, attributeValueStart);
737                         break;
738                     }
739
740                     // Not whitespace or quote, must be start of unquoted attribute.
741

742                     state = COLLECT_UNQUOTED_VALUE;
743                     attributeValueStart = _cursor;
744                     attributeBeginEvent(attributeName, _line, attributeValueStart);
745                     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                     if (ch == quoteChar)
754                     {
755                         String JavaDoc attributeValue = new String JavaDoc(_templateData, attributeValueStart,
756                                 _cursor - attributeValueStart);
757
758                         _attributes.put(attributeName, attributeValue);
759                         attributeEndEvent(_cursor);
760
761                         // Advance over the quote.
762
advance();
763                         state = WAIT_FOR_ATTRIBUTE_NAME;
764                         break;
765                     }
766
767                     advance();
768                     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                     if (ch == '/' || ch == '>' || Character.isWhitespace(ch))
776                     {
777                         String JavaDoc attributeValue = new String JavaDoc(_templateData, attributeValueStart,
778                                 _cursor - attributeValueStart);
779
780                         _attributes.put(attributeName, attributeValue);
781                         attributeEndEvent(_cursor);
782
783                         state = WAIT_FOR_ATTRIBUTE_NAME;
784                         break;
785                     }
786
787                     advance();
788                     break;
789             }
790         }
791
792         tagEndEvent(_cursor);
793
794         // Check for invisible localizations
795

796         String JavaDoc localizationKey = findValueCaselessly(LOCALIZATION_KEY_ATTRIBUTE_NAME, _attributes);
797         String JavaDoc jwcId = findValueCaselessly(_componentAttributeName, _attributes);
798
799         if (localizationKey != null && tagName.equalsIgnoreCase("span") && jwcId == null)
800         {
801             if (_ignoring)
802                 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             if (!emptyTag)
812             {
813                 Tag tag = new Tag(tagName, startLine);
814
815                 tag._component = false;
816                 tag._removeTag = true;
817                 tag._ignoringBody = true;
818                 tag._mustBalance = true;
819
820                 _stack.add(tag);
821
822                 // Start ignoring content until the close tag.
823

824                 _ignoring = true;
825             }
826             else
827             {
828                 // Cursor is at the closing carat, advance over it and any whitespace.
829
advance();
830                 advanceOverWhitespace();
831             }
832
833             // End any open block.
834

835             addTextToken(cursorStart - 1);
836
837             boolean raw = checkBoolean(RAW_ATTRIBUTE_NAME, _attributes);
838
839             Map JavaDoc attributes = filter(_attributes, new String JavaDoc[]
840             { LOCALIZATION_KEY_ATTRIBUTE_NAME, RAW_ATTRIBUTE_NAME });
841
842             TemplateToken token = _factory.createLocalizationToken(
843                     tagName,
844                     localizationKey,
845                     raw,
846                     attributes,
847                     startLocation);
848
849             _tokens.add(token);
850
851             return;
852         }
853
854         if (jwcId != null)
855         {
856             processComponentStart(tagName, jwcId, emptyTag, startLine, cursorStart, startLocation);
857             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         if (!emptyTag)
864         {
865             Tag tag = new Tag(tagName, startLine);
866             _stack.add(tag);
867         }
868
869         // If there wasn't an active block, then start one.
870

871         if (_blockStart < 0 && !_ignoring)
872             _blockStart = cursorStart;
873
874         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     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     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     protected void attributeBeginEvent(String JavaDoc 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     protected void attributeEndEvent(int cursorPosition)
915     {
916     }
917
918     private void processComponentStart(String JavaDoc tagName, String JavaDoc jwcId, boolean emptyTag,
919             int startLine, int cursorStart, Location startLocation) throws TemplateParseException
920     {
921         if (jwcId.equalsIgnoreCase(CONTENT_ID))
922         {
923             processContentTag(tagName, startLine, cursorStart, emptyTag);
924
925             return;
926         }
927
928         boolean isRemoveId = jwcId.equalsIgnoreCase(REMOVE_ID);
929
930         if (_ignoring && !isRemoveId)
931             templateParseProblem(
932                     ParseMessages.componentMayNotBeIgnored(tagName, startLine),
933                     startLocation,
934                     startLine,
935                     cursorStart);
936
937         String JavaDoc type = null;
938         boolean allowBody = false;
939
940         if (_patternMatcher.matches(jwcId, _implicitIdPattern))
941         {
942             MatchResult match = _patternMatcher.getMatch();
943
944             jwcId = match.group(IMPLICIT_ID_PATTERN_ID_GROUP);
945             type = match.group(IMPLICIT_ID_PATTERN_TYPE_GROUP);
946
947             String JavaDoc libraryId = match.group(IMPLICIT_ID_PATTERN_LIBRARY_ID_GROUP);
948             String JavaDoc 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             if (jwcId == null)
959                 jwcId = _idAllocator.allocateId("$" + simpleType);
960
961             try
962             {
963                 allowBody = _delegate.getAllowBody(libraryId, simpleType, startLocation);
964             }
965             catch (ApplicationRuntimeException e)
966             {
967                 // give subclasses a chance to handle and rethrow
968
templateParseProblem(e, startLine, cursorStart);
969             }
970
971         }
972         else
973         {
974             if (!isRemoveId)
975             {
976                 if (!_patternMatcher.matches(jwcId, _simpleIdPattern))
977                     templateParseProblem(
978                             ParseMessages.componentIdInvalid(tagName, startLine, jwcId),
979                             startLocation,
980                             startLine,
981                             cursorStart);
982
983                 if (!_delegate.getKnownComponent(jwcId))
984                     templateParseProblem(
985                             ParseMessages.unknownComponentId(tagName, startLine, jwcId),
986                             startLocation,
987                             startLine,
988                             cursorStart);
989
990                 try
991                 {
992                     allowBody = _delegate.getAllowBody(jwcId, startLocation);
993                 }
994                 catch (ApplicationRuntimeException e)
995                 {
996                     // give subclasses a chance to handle and rethrow
997
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        boolean ignoreBody = !emptyTag && (isRemoveId || !allowBody);
1007
1008        if (_ignoring && ignoreBody)
1009            templateParseProblem(ParseMessages.nestedIgnore(tagName, startLine), new LocationImpl(
1010                    _resourceLocation, startLine), startLine, cursorStart);
1011
1012        if (!emptyTag)
1013            pushNewTag(tagName, startLine, isRemoveId, ignoreBody);
1014
1015        // End any open block.
1016

1017        addTextToken(cursorStart - 1);
1018
1019        if (!isRemoveId)
1020        {
1021            addOpenToken(tagName, jwcId, type, startLocation);
1022
1023            if (emptyTag)
1024                _tokens.add(_factory.createCloseToken(tagName, getCurrentLocation()));
1025        }
1026
1027        advance();
1028    }
1029
1030    private void pushNewTag(String JavaDoc tagName, int startLine, boolean isRemoveId, boolean ignoreBody)
1031    {
1032        Tag tag = new Tag(tagName, startLine);
1033
1034        tag._component = !isRemoveId;
1035        tag._removeTag = isRemoveId;
1036
1037        tag._ignoringBody = ignoreBody;
1038
1039        _ignoring = tag._ignoringBody;
1040
1041        tag._mustBalance = true;
1042
1043        _stack.add(tag);
1044    }
1045
1046    private void processContentTag(String JavaDoc tagName, int startLine, int cursorStart, boolean emptyTag)
1047            throws TemplateParseException
1048    {
1049        if (_ignoring)
1050            templateParseProblem(
1051                    ParseMessages.contentBlockMayNotBeIgnored(tagName, startLine),
1052                    new LocationImpl(_resourceLocation, startLine),
1053                    startLine,
1054                    cursorStart);
1055
1056        if (emptyTag)
1057            templateParseProblem(
1058                    ParseMessages.contentBlockMayNotBeEmpty(tagName, startLine),
1059                    new LocationImpl(_resourceLocation, startLine),
1060                    startLine,
1061                    cursorStart);
1062
1063        _tokens.clear();
1064        _blockStart = -1;
1065
1066        Tag tag = new Tag(tagName, startLine);
1067
1068        tag._mustBalance = true;
1069        tag._content = true;
1070
1071        _stack.clear();
1072        _stack.add(tag);
1073
1074        advance();
1075    }
1076
1077    private void addOpenToken(String JavaDoc tagName, String JavaDoc jwcId, String JavaDoc type, Location location)
1078    {
1079        OpenToken token = _factory.createOpenToken(tagName, jwcId, type, location);
1080        _tokens.add(token);
1081
1082        if (_attributes.isEmpty())
1083            return;
1084
1085        Iterator JavaDoc i = _attributes.entrySet().iterator();
1086        while (i.hasNext())
1087        {
1088            Map.Entry JavaDoc entry = (Map.Entry JavaDoc) i.next();
1089
1090            String JavaDoc key = (String JavaDoc) entry.getKey();
1091
1092            if (key.equalsIgnoreCase(_componentAttributeName))
1093                continue;
1094
1095            String JavaDoc value = (String JavaDoc) entry.getValue();
1096
1097            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    private void addAttributeToToken(OpenToken token, String JavaDoc name, String JavaDoc attributeValue)
1108    {
1109        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    private void closeTag() throws TemplateParseException
1127    {
1128        int cursorStart = _cursor;
1129        int length = _templateData.length;
1130        int startLine = _line;
1131
1132        Location startLocation = getCurrentLocation();
1133
1134        _cursor += CLOSE_TAG.length;
1135
1136        int tagStart = _cursor;
1137
1138        while (true)
1139        {
1140            if (_cursor >= length)
1141                templateParseProblem(
1142                        ParseMessages.incompleteCloseTag(startLine),
1143                        startLocation,
1144                        startLine,
1145                        cursorStart);
1146
1147            char ch = _templateData[_cursor];
1148
1149            if (ch == '>')
1150                break;
1151
1152            advance();
1153        }
1154
1155        String JavaDoc tagName = new String JavaDoc(_templateData, tagStart, _cursor - tagStart);
1156
1157        int stackPos = _stack.size() - 1;
1158        Tag tag = null;
1159
1160        while (stackPos >= 0)
1161        {
1162            tag = (Tag) _stack.get(stackPos);
1163
1164            if (tag.match(tagName))
1165                break;
1166
1167            if (tag._mustBalance)
1168                templateParseProblem(ParseMessages.improperlyNestedCloseTag(
1169                        tagName,
1170                        startLine,
1171                        tag._tagName,
1172                        tag._line), startLocation, startLine, cursorStart);
1173
1174            stackPos--;
1175        }
1176
1177        if (stackPos < 0)
1178            templateParseProblem(
1179                    ParseMessages.unmatchedCloseTag(tagName, startLine),
1180                    startLocation,
1181                    startLine,
1182                    cursorStart);
1183
1184        // Special case for the content tag
1185

1186        if (tag._content)
1187        {
1188            addTextToken(cursorStart - 1);
1189
1190            // Advance the cursor right to the end.
1191

1192            _cursor = length;
1193            _stack.clear();
1194            return;
1195        }
1196
1197        // When a component closes, add a CLOSE tag.
1198
if (tag._component)
1199        {
1200            addTextToken(cursorStart - 1);
1201
1202            _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            if (_blockStart < 0 && !tag._removeTag && !_ignoring)
1210                _blockStart = cursorStart;
1211        }
1212
1213        // Remove all elements at stackPos or above.
1214

1215        for (int i = _stack.size() - 1; i >= stackPos; i--)
1216            _stack.remove(i);
1217
1218        // Advance cursor past '>'
1219

1220        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        if (tag._removeTag)
1227            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        if (tag._ignoringBody)
1233            _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    private void advance()
1242    {
1243        int length = _templateData.length;
1244
1245        if (_cursor >= length)
1246            return;
1247
1248        char ch = _templateData[_cursor];
1249
1250        _cursor++;
1251
1252        if (ch == '\n')
1253        {
1254            _line++;
1255            _currentLocation = null;
1256            return;
1257        }
1258
1259        // A \r, or a \r\n also counts as a new line.
1260

1261        if (ch == '\r')
1262        {
1263            _line++;
1264            _currentLocation = null;
1265
1266            if (_cursor < length && _templateData[_cursor] == '\n')
1267                _cursor++;
1268
1269            return;
1270        }
1271
1272        // Not an end-of-line character.
1273

1274    }
1275
1276    private void advanceOverWhitespace()
1277    {
1278        int length = _templateData.length;
1279
1280        while (_cursor < length)
1281        {
1282            char ch = _templateData[_cursor];
1283            if (!Character.isWhitespace(ch))
1284                return;
1285
1286            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    private Map JavaDoc filter(Map JavaDoc input, String JavaDoc[] removeKeys)
1297    {
1298        if (input == null || input.isEmpty())
1299            return null;
1300
1301        Map JavaDoc result = null;
1302
1303        Iterator JavaDoc i = input.entrySet().iterator();
1304
1305        nextkey: while (i.hasNext())
1306        {
1307            Map.Entry JavaDoc entry = (Map.Entry JavaDoc) i.next();
1308
1309            String JavaDoc key = (String JavaDoc) entry.getKey();
1310
1311            for (int j = 0; j < removeKeys.length; j++)
1312            {
1313                if (key.equalsIgnoreCase(removeKeys[j]))
1314                    continue nextkey;
1315            }
1316
1317            if (result == null)
1318                result = new HashMap JavaDoc(input.size());
1319
1320            result.put(key, entry.getValue());
1321        }
1322
1323        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    protected String JavaDoc findValueCaselessly(String JavaDoc key, Map JavaDoc map)
1333    {
1334        String JavaDoc result = (String JavaDoc) map.get(key);
1335
1336        if (result != null)
1337            return result;
1338
1339        Iterator JavaDoc i = map.entrySet().iterator();
1340        while (i.hasNext())
1341        {
1342            Map.Entry JavaDoc entry = (Map.Entry JavaDoc) i.next();
1343
1344            String JavaDoc entryKey = (String JavaDoc) entry.getKey();
1345
1346            if (entryKey.equalsIgnoreCase(key))
1347                return (String JavaDoc) entry.getValue();
1348        }
1349
1350        return null;
1351    }
1352
1353    /**
1354     * Conversions needed by {@link #convertEntitiesToPlain(String)}
1355     */

1356
1357    private static final String JavaDoc[] 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    private String JavaDoc convertEntitiesToPlain(String JavaDoc input)
1368    {
1369        int inputLength = input.length();
1370
1371        StringBuffer JavaDoc buffer = new StringBuffer JavaDoc(inputLength);
1372
1373        int cursor = 0;
1374
1375        outer: while (cursor < inputLength)
1376        {
1377            for (int i = 0; i < CONVERSIONS.length; i += 2)
1378            {
1379                String JavaDoc entity = CONVERSIONS[i];
1380                int entityLength = entity.length();
1381                String JavaDoc value = CONVERSIONS[i + 1];
1382
1383                if (cursor + entityLength > inputLength)
1384                    continue;
1385
1386                if (input.substring(cursor, cursor + entityLength).equals(entity))
1387                {
1388                    buffer.append(value);
1389                    cursor += entityLength;
1390                    continue outer;
1391                }
1392            }
1393
1394            buffer.append(input.charAt(cursor));
1395            cursor++;
1396        }
1397
1398        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    private boolean checkBoolean(String JavaDoc key, Map JavaDoc map)
1407    {
1408        String JavaDoc value = findValueCaselessly(key, map);
1409
1410        if (value == null)
1411            return false;
1412
1413        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    protected Location getCurrentLocation()
1424    {
1425        if (_currentLocation == null)
1426            _currentLocation = new LocationImpl(_resourceLocation, _line);
1427
1428        return _currentLocation;
1429    }
1430
1431    public void setFactory(TemplateTokenFactory factory)
1432    {
1433        _factory = factory;
1434    }
1435
1436}
Popular Tags