001: // Copyright 2006, 2007 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.tapestry.internal.services;
016:
017: import static java.lang.String.format;
018: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newSet;
019:
020: import java.util.Arrays;
021: import java.util.List;
022: import java.util.Set;
023:
024: import org.apache.tapestry.internal.parser.AttributeToken;
025: import org.apache.tapestry.internal.parser.BlockToken;
026: import org.apache.tapestry.internal.parser.BodyToken;
027: import org.apache.tapestry.internal.parser.CDATAToken;
028: import org.apache.tapestry.internal.parser.CommentToken;
029: import org.apache.tapestry.internal.parser.ComponentTemplate;
030: import org.apache.tapestry.internal.parser.DTDToken;
031: import org.apache.tapestry.internal.parser.EndElementToken;
032: import org.apache.tapestry.internal.parser.ExpansionToken;
033: import org.apache.tapestry.internal.parser.ParameterToken;
034: import org.apache.tapestry.internal.parser.StartComponentToken;
035: import org.apache.tapestry.internal.parser.StartElementToken;
036: import org.apache.tapestry.internal.parser.TemplateToken;
037: import org.apache.tapestry.internal.parser.TextToken;
038: import org.apache.tapestry.internal.parser.TokenType;
039: import org.apache.tapestry.internal.test.InternalBaseTestCase;
040: import org.apache.tapestry.ioc.Locatable;
041: import org.apache.tapestry.ioc.Location;
042: import org.apache.tapestry.ioc.Resource;
043: import org.apache.tapestry.ioc.internal.util.ClasspathResource;
044: import org.apache.tapestry.ioc.internal.util.TapestryException;
045: import org.testng.annotations.DataProvider;
046: import org.testng.annotations.Test;
047:
048: /**
049: * This is used to test the template parser ... and in some cases, the underlying behavior of the
050: * SAX APIs.
051: */
052: public class TemplateParserImplTest extends InternalBaseTestCase {
053:
054: private TemplateParser getParser() {
055: return this .getService(TemplateParser.class);
056: }
057:
058: private synchronized ComponentTemplate parse(String file) {
059: Resource resource = getResource(file);
060:
061: return getParser().parseTemplate(resource);
062: }
063:
064: private synchronized List<TemplateToken> tokens(String file) {
065: Resource resource = getResource(file);
066:
067: return getParser().parseTemplate(resource).getTokens();
068: }
069:
070: private Resource getResource(String file) {
071: String packageName = getClass().getPackage().getName();
072:
073: String path = packageName.replace('.', '/') + "/" + file;
074:
075: ClassLoader loader = getClass().getClassLoader();
076:
077: return new ClasspathResource(loader, path);
078: }
079:
080: @SuppressWarnings("unchecked")
081: private <T extends TemplateToken> T get(List l, int index) {
082: Object raw = l.get(index);
083:
084: return (T) raw;
085: }
086:
087: private void checkLine(Locatable l, int expectedLineNumber) {
088: assertEquals(l.getLocation().getLine(), expectedLineNumber);
089: }
090:
091: @Test
092: synchronized void just_HTML() {
093: Resource resource = getResource("justHTML.html");
094:
095: ComponentTemplate template = getParser()
096: .parseTemplate(resource);
097:
098: assertSame(template.getResource(), resource);
099:
100: List<TemplateToken> tokens = template.getTokens();
101:
102: // They add up quick ...
103:
104: assertEquals(tokens.size(), 20);
105:
106: StartElementToken t0 = get(tokens, 0);
107:
108: // Spot check a few things ...
109:
110: assertEquals(t0.getName(), "html");
111: checkLine(t0, 1);
112:
113: TextToken t1 = get(tokens, 1);
114: // Concerned this may not work cross platform.
115: assertEquals(t1.getText(), "\n ");
116:
117: StartElementToken t2 = get(tokens, 2);
118: assertEquals(t2.getName(), "head");
119: checkLine(t2, 2);
120:
121: TextToken t5 = get(tokens, 5);
122: assertEquals(t5.getText(), "title");
123: checkLine(t5, 3);
124:
125: get(tokens, 6);
126:
127: StartElementToken t12 = get(tokens, 12);
128: assertEquals(t12.getName(), "p");
129:
130: AttributeToken t13 = get(tokens, 13);
131: assertEquals(t13.getName(), "class");
132: assertEquals(t13.getValue(), "important");
133:
134: TextToken t14 = get(tokens, 14);
135: // Simplify the text, converting consecutive whitespace to just a single space.
136: assertEquals(t14.getText().replaceAll("\\s+", " ").trim(),
137: "Tapestry rocks! Line 2");
138:
139: // Line number is the *start* line of the whole text block.
140: checkLine(t14, 6);
141: }
142:
143: @Test
144: void xml_entity() {
145: List<TemplateToken> tokens = tokens("xmlEntity.html");
146:
147: assertEquals(tokens.size(), 3);
148:
149: TextToken t = get(tokens, 1);
150:
151: // This is OK because the org.apache.tapestry.dom.Text will convert the characters back into
152: // XML entities.
153:
154: assertEquals(t.getText().trim(), "lt:< gt:> amp:&");
155: }
156:
157: /** Test disabled when not online. */
158: @Test(enabled=false)
159: void html_entity() {
160: List<TemplateToken> tokens = tokens("html_entity.html");
161:
162: assertEquals(tokens.size(), 3);
163:
164: TextToken t = get(tokens, 1);
165:
166: // HTML entities are parsed into values that will ultimately
167: // be output as numeric entities. This is less than ideal; would like
168: // to find a way to keep the entities in their original form (possibly
169: // involving a new type of token), but SAX seems to be fighting me on this.
170: // You have to have a DOCTYPE just to parse a template that uses
171: // an HTML entity.
172:
173: assertEquals(t.getText().trim(), "nbsp:[\u00a0]");
174: }
175:
176: @Test
177: void cdata() {
178: List<TemplateToken> tokens = tokens("cdata.html");
179:
180: // Whitespace text tokens around the CDATA
181:
182: assertEquals(tokens.size(), 5);
183:
184: CDATAToken t = get(tokens, 2);
185:
186: assertEquals(t.getText(),
187: "CDATA: <foo> & <bar> and <baz>");
188: checkLine(t, 2);
189: }
190:
191: @Test
192: void comment() {
193: List<TemplateToken> tokens = tokens("comment.html");
194:
195: // Again, whitespace before and after the comment adds some tokens
196:
197: assertEquals(tokens.size(), 5);
198:
199: CommentToken t = get(tokens, 2);
200:
201: // Comments are now trimmed of leading and trailing whitespace. This may mean
202: // that the output isn't precisely what's in the template, but a) its a comment
203: // and b) that's pretty much true of everything in the templates.
204:
205: assertEquals(t.getComment(), "Single line comment");
206: }
207:
208: @Test
209: void multiline_comment() {
210: List<TemplateToken> tokens = tokens("multilineComment.html");
211:
212: // Again, whitespace before and after the comment adds some tokens
213:
214: assertEquals(tokens.size(), 5);
215:
216: CommentToken t = get(tokens, 2);
217:
218: String comment = t.getComment().trim().replaceAll("\\s+", " ");
219:
220: assertEquals(comment, "Line one Line two Line three");
221: }
222:
223: @Test
224: void component() {
225: List<TemplateToken> tokens = tokens("component.html");
226:
227: assertEquals(tokens.size(), 6);
228:
229: StartComponentToken t = get(tokens, 2);
230: assertEquals(t.getId(), "fred");
231: assertEquals(t.getComponentType(), "somecomponent");
232: assertNull(t.getMixins());
233: checkLine(t, 2);
234:
235: get(tokens, 3);
236: }
237:
238: @Test
239: void component_with_body() {
240: List<TemplateToken> tokens = tokens("componentWithBody.html");
241:
242: assertEquals(tokens.size(), 7);
243:
244: get(tokens, 2);
245:
246: TextToken t = get(tokens, 3);
247:
248: assertEquals(t.getText().trim(), "fred's body");
249:
250: get(tokens, 4);
251: }
252:
253: @Test
254: public void root_element_is_component() {
255: List<TemplateToken> tokens = tokens("root_element_is_component.html");
256:
257: assertEquals(tokens.size(), 3);
258:
259: StartComponentToken start = get(tokens, 0);
260:
261: assertEquals(start.getId(), "fred");
262: assertEquals(start.getComponentType(), "Fred");
263: assertNull(start.getElementName());
264:
265: AttributeToken attr = get(tokens, 1);
266:
267: assertEquals(attr.getName(), "param");
268: assertEquals(attr.getValue(), "value");
269:
270: assertTrue(EndElementToken.class.isInstance(tokens.get(2)));
271: }
272:
273: @Test
274: public void instrumented_element() {
275: ComponentTemplate template = parse("instrumented_element.html");
276: List<TemplateToken> tokens = template.getTokens();
277:
278: assertEquals(tokens.size(), 3);
279:
280: StartComponentToken start = get(tokens, 0);
281:
282: assertEquals(start.getId(), "fred");
283: assertEquals(start.getComponentType(), "Fred");
284: assertEquals(start.getElementName(), "html");
285:
286: AttributeToken attr = get(tokens, 1);
287:
288: assertEquals(attr.getName(), "param");
289: assertEquals(attr.getValue(), "value");
290:
291: assertTrue(EndElementToken.class.isInstance(tokens.get(2)));
292:
293: assertEquals(template.getComponentIds(), Arrays.asList("fred"));
294: }
295:
296: @Test
297: void body_element() {
298: List<TemplateToken> tokens = tokens("body_element.html");
299:
300: // start(html), text, body, text, end(html)
301: assertEquals(tokens.size(), 5);
302:
303: // javac bug requires use of isInstance() instead of instanceof
304: // https://bugs.eclipse.org/bugs/show_bug.cgi?id=113218
305: assertTrue(BodyToken.class.isInstance(get(tokens, 2)));
306: }
307:
308: @Test
309: void content_within_body_element() {
310: List<TemplateToken> tokens = parse(
311: "content_within_body_element.html").getTokens();
312:
313: assertEquals(tokens.size(), 5);
314:
315: // javac bug is requires use of isInstance() instead of instanceof
316: // https://bugs.eclipse.org/bugs/show_bug.cgi?id=113218
317:
318: assertTrue(BodyToken.class.isInstance(get(tokens, 2)));
319: assertTrue(TextToken.class.isInstance(get(tokens, 3)));
320: assertTrue(EndElementToken.class.isInstance(get(tokens, 4)));
321: }
322:
323: @Test
324: void component_with_parameters() {
325: List<TemplateToken> tokens = tokens("componentWithParameters.html");
326:
327: assertEquals(tokens.size(), 9);
328:
329: TemplateToken templateToken = get(tokens, 2);
330: Location l = templateToken.getLocation();
331:
332: AttributeToken t1 = get(tokens, 3);
333:
334: // TODO: Not sure what order the attributes appear in. Order in the XML? Sorted
335: // alphabetically? Random 'cause they're hashed?
336:
337: assertEquals(t1.getName(), "cherry");
338: assertEquals(t1.getValue(), "bomb");
339: assertSame(t1.getLocation(), l);
340:
341: AttributeToken t2 = get(tokens, 4);
342: assertEquals(t2.getName(), "align");
343: assertEquals(t2.getValue(), "right");
344: assertSame(t2.getLocation(), l);
345:
346: TextToken t3 = get(tokens, 5);
347:
348: assertEquals(t3.getText().trim(), "fred's body");
349:
350: get(tokens, 6);
351: }
352:
353: @Test
354: public void component_with_mixins() {
355: List<TemplateToken> tokens = tokens("component_with_mixins.html");
356:
357: assertEquals(tokens.size(), 6);
358:
359: StartComponentToken t = get(tokens, 2);
360:
361: assertEquals(t.getId(), "fred");
362: assertEquals(t.getComponentType(), "comp");
363: assertEquals(t.getMixins(), "Barney");
364: }
365:
366: @Test
367: public void empty_string_mixins_is_null() {
368: List<TemplateToken> tokens = tokens("empty_string_mixins_is_null.html");
369:
370: assertEquals(tokens.size(), 6);
371:
372: StartComponentToken t = get(tokens, 2);
373:
374: assertEquals(t.getId(), "fred");
375: // We also check that empty string type is null ..
376: assertNull(t.getComponentType());
377: assertNull(t.getMixins());
378: }
379:
380: @Test
381: public void component_ids() {
382: ComponentTemplate template = parse("component_ids.html");
383:
384: Set<String> ids = template.getComponentIds();
385:
386: assertEquals(ids, newSet(Arrays.asList("bomb", "border",
387: "zebra")));
388: }
389:
390: @Test
391: public void expansions_in_normal_text() {
392: List<TemplateToken> tokens = tokens("expansions_in_normal_text.html");
393:
394: assertEquals(tokens.size(), 7);
395:
396: TextToken t1 = get(tokens, 1);
397:
398: assertEquals(t1.getText().trim(), "Expansion #1[");
399:
400: ExpansionToken t2 = get(tokens, 2);
401: assertEquals(t2.getExpression(), "expansion1");
402:
403: TextToken t3 = get(tokens, 3);
404: assertEquals(t3.getText().replaceAll("\\s+", " "),
405: "] Expansion #2[");
406:
407: ExpansionToken t4 = get(tokens, 4);
408: assertEquals(t4.getExpression(), "expansion2");
409:
410: TextToken t5 = get(tokens, 5);
411: assertEquals(t5.getText().trim(), "]");
412: }
413:
414: @Test
415: public void expansions_must_be_on_one_line() {
416: List<TemplateToken> tokens = tokens("expansions_must_be_on_one_line.html");
417:
418: assertEquals(tokens.size(), 3);
419:
420: TextToken t1 = get(tokens, 1);
421:
422: assertEquals(t1.getText().replaceAll("\\s+", " "),
423: " ${expansions must be on a single line} ");
424:
425: }
426:
427: @Test
428: public void multiple_expansions_on_one_line() {
429: List<TemplateToken> tokens = tokens("multiple_expansions_on_one_line.html");
430:
431: assertEquals(tokens.size(), 10);
432:
433: ExpansionToken token3 = get(tokens, 3);
434:
435: assertEquals(token3.getExpression(), "classLoader");
436:
437: TextToken token4 = get(tokens, 4);
438:
439: assertEquals(token4.getText(), " [");
440:
441: ExpansionToken token5 = get(tokens, 5);
442:
443: assertEquals(token5.getExpression(), "classLoader.class.name");
444:
445: TextToken token6 = get(tokens, 6);
446:
447: assertEquals(token6.getText(), "]");
448: }
449:
450: @Test
451: public void expansions_not_allowed_in_cdata() {
452: List<TemplateToken> tokens = tokens("expansions_not_allowed_in_cdata.html");
453:
454: assertEquals(tokens.size(), 5);
455:
456: CDATAToken t2 = get(tokens, 2);
457:
458: assertEquals(t2.getText(), "${not-an-expansion}");
459: }
460:
461: @Test
462: public void expansions_not_allowed_in_attributes() {
463: List<TemplateToken> tokens = tokens("expansions_not_allowed_in_attributes.html");
464:
465: assertEquals(tokens.size(), 4);
466:
467: AttributeToken t1 = get(tokens, 1);
468:
469: assertEquals(t1.getName(), "exp");
470: assertEquals(t1.getValue(), "${not-an-expansion}");
471: }
472:
473: @Test
474: public void parameter_element() {
475: List<TemplateToken> tokens = tokens("parameter_element.html");
476:
477: ParameterToken token4 = get(tokens, 4);
478: assertEquals(token4.getName(), "fred");
479:
480: CommentToken token6 = get(tokens, 6);
481: assertEquals(token6.getComment(), "fred content");
482:
483: TemplateToken token8 = get(tokens, 8);
484:
485: assertEquals(token8.getTokenType(), TokenType.END_ELEMENT);
486: }
487:
488: @Test
489: public void complex_component_type() {
490: List<TemplateToken> tokens = tokens("complex_component_type.html");
491:
492: assertEquals(tokens.size(), 6);
493:
494: StartComponentToken token2 = get(tokens, 2);
495:
496: assertEquals(token2.getComponentType(), "subfolder/nifty");
497: }
498:
499: @Test
500: public void block_element() {
501: List<TemplateToken> tokens = tokens("block_element.html");
502:
503: BlockToken token2 = get(tokens, 2);
504: assertEquals(token2.getId(), "block0");
505:
506: CommentToken token4 = get(tokens, 4);
507: assertEquals(token4.getComment(), "block0 content");
508:
509: BlockToken token8 = get(tokens, 8);
510: assertNull(token8.getId());
511:
512: CommentToken token10 = get(tokens, 10);
513: assertEquals(token10.getComment(), "anon block content");
514: }
515:
516: @DataProvider(name="parse_failure_data")
517: public Object[][] parse_failure_data() {
518: return new Object[][] {
519: {
520: "mixin_requires_id_or_type.html",
521: "You may not specify mixins for element <span> because it does not represent a component (which requires either an id attribute or a type attribute).",
522: 2 },
523: {
524: "illegal_nesting_within_body_element.html",
525: "Element 'xyz' is nested within a Tapestry body element",
526: 2 },
527: {
528: "unexpected_attribute_in_parameter_element.html",
529: "Element <parameter> does not support an attribute named 'grok'. The only allowed attribute name is 'name'.",
530: 4 },
531: {
532: "name_attribute_of_parameter_element_omitted.html",
533: "The name attribute of the <parameter> element must be specified.",
534: 4 },
535: {
536: "name_attribute_of_parameter_element_blank.html",
537: "The name attribute of the <parameter> element must be specified.",
538: 4 },
539: {
540: "unexpected_attribute_in_block_element.html",
541: "Element <block> does not support an attribute named 'name'. The only allowed attribute name is 'id'.",
542: 3 },
543:
544: };
545: }
546:
547: @Test(dataProvider="parse_failure_data")
548: public void parse_failure(String fileName,
549: String errorMessageSubstring, int expectedLine) {
550: try {
551: parse(fileName);
552: unreachable();
553: } catch (TapestryException ex) {
554: if (!ex.getMessage().contains(errorMessageSubstring)) {
555: throw new AssertionError(
556: format(
557: "Message [%s] does not contain substring [%s].",
558: ex.getMessage(), errorMessageSubstring));
559: }
560:
561: assertEquals(ex.getLocation().getLine(), expectedLine);
562: }
563: }
564:
565: @DataProvider(name="doctype_parsed_correctly_data")
566: public Object[][] doctype_parsed_correctly_data() {
567: return new Object[][] { { "xhtml1_strict_doctype.html" },
568: { "xhtml1_transitional_doctype.html" },
569: { "xhtml1_frameset_doctype.html" } };
570: }
571:
572: @Test(dataProvider="doctype_parsed_correctly_data")
573: public void doctype_parsed_correctly(String fileName)
574: throws Exception {
575: List<TemplateToken> tokens = tokens(fileName);
576: assertEquals(tokens.size(), 11);
577: TextToken t = get(tokens, 8);
578: assertEquals(t.getText().trim(), "<Test>");
579: }
580:
581: @DataProvider(name="doctype_added_correctly_data")
582: public Object[][] doctype_token_added_correctly_data() {
583: return new Object[][] {
584: { "xhtml1_strict_doctype.html", "html",
585: "-//W3C//DTD XHTML 1.0 Strict//EN",
586: "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" },
587: { "xhtml1_transitional_doctype.html", "html",
588: "-//W3C//DTD XHTML 1.0 Transitional//EN",
589: "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" },
590: { "xhtml1_frameset_doctype.html", "html",
591: "-//W3C//DTD XHTML 1.0 Frameset//EN",
592: "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd" },
593: { "system_doctype.xml", "foo", null,
594: "src/test/resources/org/apache/tapestry/internal/services/simple.dtd" }, };
595: }
596:
597: @Test(dataProvider="doctype_added_correctly_data")
598: public void doctype_added_correctly(String fileName, String name,
599: String publicId, String systemId) throws Exception {
600: List<TemplateToken> tokens = tokens(fileName);
601: DTDToken t2 = get(tokens, 0);
602: assertEquals(t2.getName(), name);
603: assertEquals(t2.getPublicId(), publicId);
604: assertEquals(t2.getSystemId(), systemId);
605: }
606:
607: }
|