|
|||||||||||||||||||
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% |
|
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>/></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><span key="<i>value</i>"> ... </span></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 <span> 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., </foo>. 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 '<' 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 '>'.
|
|
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 |
{ "<", "<", ">", ">", """, "\"", "&", "&" }; |
|
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;;, &quot;; &lt;; and &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 |
} |
|