001 // Copyright 2004, 2005 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.junit.parse; 016 017 import java.io.IOException; 018 import java.io.InputStream; 019 import java.util.HashMap; 020 import java.util.Map; 021 022 import org.apache.commons.io.IOUtils; 023 import org.apache.hivemind.Location; 024 import org.apache.hivemind.Resource; 025 import org.apache.hivemind.impl.DefaultClassResolver; 026 import org.apache.hivemind.util.ClasspathResource; 027 import org.apache.tapestry.BaseComponentTestCase; 028 import org.apache.tapestry.parse.ITemplateParserDelegate; 029 import org.apache.tapestry.parse.LocalizationToken; 030 import org.apache.tapestry.parse.OpenToken; 031 import org.apache.tapestry.parse.TemplateParseException; 032 import org.apache.tapestry.parse.TemplateParser; 033 import org.apache.tapestry.parse.TemplateToken; 034 import org.apache.tapestry.parse.TemplateTokenFactory; 035 import org.apache.tapestry.parse.TextToken; 036 import org.apache.tapestry.parse.TokenType; 037 import org.testng.annotations.Test; 038 039 /** 040 * Tests for the Tapestry HTML template parser. 041 * 042 * @author Howard Lewis Ship 043 */ 044 @Test 045 public class TestTemplateParser extends BaseComponentTestCase 046 { 047 private static class ParserDelegate implements ITemplateParserDelegate 048 { 049 private final String _componentAttributeName; 050 051 public ParserDelegate() 052 { 053 this("jwcid"); 054 } 055 056 public ParserDelegate(String componentAttributeName) 057 { 058 _componentAttributeName = componentAttributeName; 059 } 060 061 public boolean getKnownComponent(String componentId) 062 { 063 return true; 064 } 065 066 public boolean getAllowBody(String componentId, Location location) 067 { 068 return true; 069 } 070 071 public boolean getAllowBody(String libraryId, String type, Location location) 072 { 073 return true; 074 } 075 076 public String getComponentAttributeName() 077 { 078 return _componentAttributeName; 079 } 080 } 081 082 protected TemplateToken[] run(char[] templateData, ITemplateParserDelegate delegate, 083 Resource location) throws TemplateParseException 084 { 085 TemplateParser parser = new TemplateParser(); 086 087 parser.setFactory(new TemplateTokenFactory()); 088 089 return parser.parse(templateData, delegate, location); 090 } 091 092 protected TemplateToken[] run(InputStream stream, ITemplateParserDelegate delegate, 093 Resource location) throws TemplateParseException 094 { 095 096 char[] data = null; 097 098 try 099 { 100 data = IOUtils.toCharArray(stream, "US-ASCII"); 101 102 stream.close(); 103 } 104 catch (IOException ex) 105 { 106 fail("Unable to read from stream."); 107 } 108 109 return run(data, delegate, location); 110 } 111 112 protected TemplateToken[] run(String file) throws TemplateParseException 113 { 114 return run(file, new ParserDelegate()); 115 } 116 117 protected TemplateToken[] run(String file, ITemplateParserDelegate delegate) 118 throws TemplateParseException 119 { 120 String thisClassName = getClass().getName(); 121 String thisPath = "/" + thisClassName.replace('.', '/') + "/" + file; 122 123 Resource location = new ClasspathResource(new DefaultClassResolver(), thisPath); 124 125 InputStream stream = getClass().getResourceAsStream(file); 126 127 if (stream == null) 128 throw new TemplateParseException("File " + file + " not found."); 129 130 return run(stream, delegate, location); 131 } 132 133 private Map buildMap(String[] input) 134 { 135 Map result = new HashMap(); 136 137 for (int i = 0; i < input.length; i += 2) 138 result.put(input[i], input[i + 1]); 139 140 return result; 141 } 142 143 // Note: the API of TextToken changed ... from startIndex/endIndex to offset/length. 144 // Rather than change *all* the tests, we'll just adjust here. 145 146 protected void assertTextToken(TemplateToken token, int startIndex, int endIndex) 147 { 148 TextToken t = (TextToken) token; 149 150 int expectedLength = endIndex - startIndex + 1; 151 152 assertEquals(TokenType.TEXT, t.getType()); 153 assertEquals(startIndex, t.getOffset()); 154 assertEquals(expectedLength, t.getLength()); 155 } 156 157 protected void assertText(TextToken token, int offset, int length) 158 { 159 assertEquals(token.getOffset(), offset); 160 assertEquals(token.getLength(), length); 161 } 162 163 /** @since 3.0 * */ 164 165 protected void checkLine(TemplateToken token, int line) 166 { 167 assertEquals(line, token.getLocation().getLineNumber()); 168 } 169 170 /** @since 2.0.4 * */ 171 172 protected void assertLocalizationToken(TemplateToken token, String key, Map attributes, int line) 173 { 174 LocalizationToken t = (LocalizationToken) token; 175 176 assertEquals(TokenType.LOCALIZATION, t.getType()); 177 assertEquals( key, t.getKey()); 178 179 assertEquals(attributes, t.getAttributes()); 180 181 checkLine(token, line); 182 } 183 184 protected void assertOpenToken(TemplateToken token, String id, String tag, int line) 185 { 186 assertOpenToken(token, id, null, tag, line); 187 } 188 189 protected void assertOpenToken(TemplateToken token, String id, String componentType, 190 String tag, int line) 191 { 192 OpenToken t = (OpenToken) token; 193 194 assertEquals(t.getType(), TokenType.OPEN); 195 assertEquals(t.getId(), id); 196 assertEquals(t.getComponentType(), componentType); 197 assertEquals(t.getTag(), tag); 198 199 checkLine(token, line); 200 } 201 202 protected void assertTemplateAttributes(TemplateToken token, Map expected) 203 { 204 OpenToken t = (OpenToken) token; 205 206 assertEquals(t.getAttributesMap(), expected); 207 } 208 209 protected void assertCloseToken(TemplateToken token, int line) 210 { 211 assertEquals(token.getType(), TokenType.CLOSE); 212 213 checkLine(token, line); 214 } 215 216 protected void assertTokenCount(TemplateToken[] tokens, int count) 217 { 218 assertNotNull(tokens); 219 assertEquals(tokens.length, count); 220 } 221 222 private void runFailure(String file, String message) 223 { 224 runFailure(file, new ParserDelegate(), message); 225 } 226 227 private void runFailure(String file, ITemplateParserDelegate delegate, String message) 228 { 229 try 230 { 231 run(file, delegate); 232 233 fail("Invalid document " + file + " parsed without exception."); 234 } 235 catch (TemplateParseException ex) 236 { 237 assertEquals(message, ex.getMessage()); 238 assertTrue(ex.getLocation().toString().indexOf(file) > 0); 239 } 240 } 241 242 public void testAllStatic() throws TemplateParseException 243 { 244 TemplateToken[] tokens = run("AllStatic.html"); 245 246 assertTokenCount(tokens, 1); 247 assertTextToken(tokens[0], 0, 172); 248 } 249 250 public void testSingleEmptyTag() throws TemplateParseException 251 { 252 TemplateToken[] tokens = run("SingleEmptyTag.html"); 253 254 assertTokenCount(tokens, 4); 255 256 assertTextToken(tokens[0], 0, 38); 257 assertOpenToken(tokens[1], "emptyTag", "span", 3); 258 assertCloseToken(tokens[2], 3); 259 assertTextToken(tokens[3], 63, 102); 260 } 261 262 public void testSimpleNested() throws TemplateParseException 263 { 264 TemplateToken[] tokens = run("SimpleNested.html"); 265 266 assertTokenCount(tokens, 8); 267 assertOpenToken(tokens[1], "outer", "span", 3); 268 assertOpenToken(tokens[3], "inner", "span", 4); 269 assertCloseToken(tokens[4], 4); 270 assertCloseToken(tokens[6], 5); 271 } 272 273 public void testMixedNesting() throws TemplateParseException 274 { 275 TemplateToken[] tokens = run("MixedNesting.html"); 276 277 assertTokenCount(tokens, 5); 278 assertOpenToken(tokens[1], "row", "span", 4); 279 assertCloseToken(tokens[3], 7); 280 } 281 282 public void testSingleQuotes() throws TemplateParseException 283 { 284 TemplateToken[] tokens = run("SingleQuotes.html"); 285 286 assertTokenCount(tokens, 7); 287 assertOpenToken(tokens[1], "first", "span", 5); 288 assertOpenToken(tokens[4], "second", "span", 7); 289 } 290 291 public void testComplex() throws TemplateParseException 292 { 293 TemplateToken[] tokens = run("Complex.html"); 294 295 assertTokenCount(tokens, 19); 296 297 // Just pick a few highlights out of it. 298 299 assertOpenToken(tokens[1], "ifData", "span", 3); 300 assertOpenToken(tokens[3], "e", "span", 10); 301 assertOpenToken(tokens[5], "row", "tr", 11); 302 } 303 304 public void testStartWithStaticTag() throws TemplateParseException 305 { 306 TemplateToken[] tokens = run("StartWithStaticTag.html"); 307 308 assertTokenCount(tokens, 4); 309 assertTextToken(tokens[0], 0, 232); 310 assertOpenToken(tokens[1], "justBecause", "span", 9); 311 } 312 313 public void testUnterminatedCommentFailure() 314 { 315 runFailure("UnterminatedComment.html", "Comment on line 3 did not end."); 316 } 317 318 public void testUnclosedOpenTagFailure() 319 { 320 runFailure("UnclosedOpenTag.html", "Tag <body> on line 4 is never closed."); 321 } 322 323 public void testMissingAttributeValueFailure() 324 { 325 runFailure( 326 "MissingAttributeValue.html", 327 "Tag <img> on line 9 is missing a value for attribute src."); 328 } 329 330 public void testIncompleteCloseFailure() 331 { 332 runFailure("IncompleteClose.html", "Incomplete close tag on line 6."); 333 } 334 335 public void testMismatchedCloseTagsFailure() 336 { 337 runFailure( 338 "MismatchedCloseTags.html", 339 "Closing tag </th> on line 9 does not have a matching open tag."); 340 } 341 342 public void testInvalidDynamicNestingFailure() 343 { 344 runFailure( 345 "InvalidDynamicNesting.html", 346 "Closing tag </body> on line 12 is improperly nested with tag <span> on line 8."); 347 } 348 349 public void testUnknownComponentIdFailure() 350 { 351 ITemplateParserDelegate delegate = new ITemplateParserDelegate() 352 { 353 public boolean getKnownComponent(String componentId) 354 { 355 return !componentId.equals("row"); 356 } 357 358 public boolean getAllowBody(String componentId, Location location) 359 { 360 return true; 361 } 362 363 public boolean getAllowBody(String libraryId, String type, Location location) 364 { 365 return true; 366 } 367 368 public String getComponentAttributeName() 369 { 370 return "jwcid"; 371 } 372 }; 373 374 runFailure( 375 "Complex.html", 376 delegate, 377 "Tag <tr> on line 11 references unknown component id 'row'."); 378 } 379 380 public void testBasicRemove() throws TemplateParseException 381 { 382 TemplateToken[] tokens = run("BasicRemove.html"); 383 384 assertTokenCount(tokens, 10); 385 assertTextToken(tokens[0], 0, 119); 386 assertTextToken(tokens[1], 188, 268); 387 assertOpenToken(tokens[2], "e", "span", 23); 388 assertTextToken(tokens[3], 341, 342); 389 assertOpenToken(tokens[4], "row", "tr", 24); 390 assertTextToken(tokens[5], 359, 377); 391 assertCloseToken(tokens[6], 26); 392 assertTextToken(tokens[7], 383, 383); 393 assertCloseToken(tokens[8], 27); 394 assertTextToken(tokens[9], 391, 401); 395 } 396 397 public void testBodyRemove() throws TemplateParseException 398 { 399 ITemplateParserDelegate delegate = new ITemplateParserDelegate() 400 { 401 public boolean getKnownComponent(String id) 402 { 403 return true; 404 } 405 406 public boolean getAllowBody(String id, Location location) 407 { 408 return id.equals("form"); 409 } 410 411 public boolean getAllowBody(String libraryId, String type, Location location) 412 { 413 return true; 414 } 415 416 public String getComponentAttributeName() 417 { 418 return "jwcid"; 419 } 420 }; 421 422 TemplateToken[] tokens = run("BodyRemove.html", delegate); 423 424 assertTokenCount(tokens, 8); 425 assertOpenToken(tokens[1], "form", "form", 9); 426 assertOpenToken(tokens[3], "inputType", "select", 11); 427 assertCloseToken(tokens[4], 15); 428 assertCloseToken(tokens[6], 16); 429 } 430 431 public void testRemovedComponentFailure() 432 { 433 runFailure( 434 "RemovedComponent.html", 435 "Tag <span> on line 5 is a dynamic component, and may not appear inside an ignored block."); 436 } 437 438 public void testNestedRemoveFailure() 439 { 440 runFailure( 441 "NestedRemove.html", 442 "Tag <span> on line 4 should be ignored, but is already inside " 443 + "an ignored block (ignored blocks may not be nested)."); 444 } 445 446 public void testBasicContent() throws TemplateParseException 447 { 448 TemplateToken[] tokens = run("BasicContent.html"); 449 450 assertTokenCount(tokens, 4); 451 assertTextToken(tokens[0], 108, 165); 452 assertOpenToken(tokens[1], "nested", "span", 9); 453 assertCloseToken(tokens[2], 9); 454 assertTextToken(tokens[3], 188, 192); 455 } 456 457 public void testIgnoredContentFailure() 458 { 459 runFailure( 460 "IgnoredContent.html", 461 "Tag <td> on line 7 is the template content, and may not be in an ignored block."); 462 } 463 464 public void testTagAttributes() throws TemplateParseException 465 { 466 TemplateToken[] tokens = run("TagAttributes.html"); 467 468 assertTokenCount(tokens, 5); 469 assertOpenToken(tokens[1], "tag", null, "span", 3); 470 471 assertTemplateAttributes(tokens[1], buildMap(new String[] 472 { "class", "zip", "align", "right", "color", "#ff00ff" })); 473 474 } 475 476 /** 477 * @since 2.0.4 478 */ 479 480 public void test_Basic_Localization() throws TemplateParseException 481 { 482 TemplateToken[] tokens = run("BasicLocalization.html"); 483 484 assertTokenCount(tokens, 7); 485 assertTextToken(tokens[0], 0, 35); 486 assertLocalizationToken(tokens[1], "the.localization.key", null, 3); 487 assertText((TextToken)tokens[2], 87, 44); 488 assertLocalizationToken(tokens[3], "hello-key", null, 7); 489 assertText((TextToken)tokens[4], 165, 1); 490 assertLocalizationToken(tokens[5], "world-key", null, 7); 491 assertText((TextToken)tokens[6], 200, 1); 492 } 493 494 /** 495 * Test that the parser fails if a localization block contains a component. 496 * 497 * @since 2.0.4 498 */ 499 500 public void testComponentInsideLocalization() 501 { 502 runFailure( 503 "ComponentInsideLocalization.html", 504 "Tag <span> on line 9 is a dynamic component, and may not appear inside an ignored block."); 505 } 506 507 /** 508 * Test that the parser fails if an invisible localization is nested within another invisible 509 * localization. 510 * 511 * @since 2.0.4 512 */ 513 514 public void testNestedLocalizations() 515 { 516 runFailure( 517 "NestedLocalizations.html", 518 "Tag <span> on line 4 is a dynamic component, and may not appear inside an ignored block."); 519 } 520 521 /** 522 * Test that the abbreviated form (a tag with no body) works. 523 * 524 * @since 2.0.4 525 */ 526 527 public void test_Empty_Localization() throws TemplateParseException 528 { 529 TemplateToken[] tokens = run("EmptyLocalization.html"); 530 531 assertTokenCount(tokens, 3); 532 assertTextToken(tokens[0], 0, 62); 533 assertLocalizationToken(tokens[1], "empty.localization", null, 3); 534 assertTextToken(tokens[2], 95, 122); 535 } 536 537 /** 538 * Test attributes in the span. Also, checks that the parser caselessly identifies the "key" 539 * attribute and the tag name ("span"). 540 * 541 * @since 2.0.4 542 */ 543 544 public void testLocalizationAttributes() throws TemplateParseException 545 { 546 TemplateToken[] tokens = run("LocalizationAttributes.html"); 547 548 Map attributes = buildMap(new String[] { "alpha", "beta", "Fred", "Wilma" }); 549 550 assertLocalizationToken(tokens[1], "localization.with.attributes", attributes, 3); 551 } 552 553 /** 554 * Tests for implicit components (both named and anonymous). 555 * 556 * @since 3.0 557 */ 558 559 public void testImplicitComponents() throws TemplateParseException 560 { 561 TemplateToken[] tokens = run("ImplicitComponents.html"); 562 563 assertTokenCount(tokens, 18); 564 565 assertOpenToken(tokens[1], "$Body", "Body", "body", 4); 566 assertOpenToken(tokens[3], "loop", "For", "tr", 7); 567 568 assertTemplateAttributes(tokens[3], buildMap(new String[] 569 { "element", "tr", "source", "ognl:items" })); 570 571 assertOpenToken(tokens[5], "$Insert", "Insert", "span", 10); 572 573 assertTemplateAttributes(tokens[5], buildMap(new String[] 574 { "value", "ognl:components.loop.value.name" })); 575 576 assertOpenToken(tokens[8], "$Insert_0", "Insert", "span", 11); 577 578 assertTemplateAttributes(tokens[8], buildMap(new String[] 579 { "value", "ognl:components.loop.value.price" })); 580 581 assertOpenToken(tokens[13], "$InspectorButton", "contrib:InspectorButton", "span", 15); 582 } 583 584 /** 585 * Test for encoded characters in an expression. 586 * 587 * @since 3.0 588 */ 589 590 public void testEncodedExpressionCharacters() throws TemplateParseException 591 { 592 TemplateToken[] tokens = run("EncodedExpressionCharacters.html"); 593 594 assertTokenCount(tokens, 3); 595 596 assertOpenToken(tokens[0], "$Insert", "Insert", "span", 1); 597 598 String expression = "ognl: { \"<&>\", \"Fun!\" }"; 599 600 assertTemplateAttributes(tokens[0], buildMap(new String[] 601 { "value", expression })); 602 603 } 604 605 /** 606 * Test ability to read string attributes. 607 */ 608 609 public void testStringAttributes() throws TemplateParseException 610 { 611 TemplateToken[] tokens = run("StringAttributes.html"); 612 613 assertTokenCount(tokens, 4); 614 615 assertOpenToken(tokens[1], "$Image", "Image", "img", 2); 616 617 assertTemplateAttributes(tokens[1], buildMap(new String[] 618 { "image", "ognl:assets.logo", "alt", "message:logo-title" })); 619 620 } 621 622 /** 623 * Test ability to use a different attribute name than the default ("jwcid"). 624 * 625 * @since 4.0 626 */ 627 628 public void testOverrideDefaultAttributeName() throws Exception 629 { 630 TemplateToken[] tokens = run("OverrideDefaultAttributeName.html", new ParserDelegate("id")); 631 632 assertTokenCount(tokens, 8); 633 assertOpenToken(tokens[1], "outer", "span", 3); 634 assertOpenToken(tokens[3], "inner", "span", 4); 635 assertCloseToken(tokens[4], 4); 636 assertCloseToken(tokens[6], 5); 637 } 638 639 /** 640 * Like {@link #testOverrideDefaultAttributeName()}, but uses a more complicated attribute name 641 * (with a XML-style namespace prefix). 642 */ 643 644 public void testNamespaceAttributeName() throws Exception 645 { 646 TemplateToken[] tokens = run("NamespaceAttributeName.html", new ParserDelegate("t:id")); 647 648 assertTokenCount(tokens, 8); 649 assertOpenToken(tokens[1], "outer", "span", 3); 650 assertOpenToken(tokens[3], "inner", "span", 4); 651 assertCloseToken(tokens[4], 4); 652 assertCloseToken(tokens[6], 5); 653 } 654 655 /** @since 4.0 */ 656 public void testDuplicateTagAttributeFailure() 657 { 658 runFailure( 659 "DuplicateTagAttribute.html", 660 "Tag <input> on line 3 contains more than one 'value' attribute."); 661 } 662 663 /** @since 4.0 */ 664 public void testDuplicateTagAttributeFailureSingleQuotes() 665 { 666 runFailure( 667 "DuplicateTagAttributeSingleQuotes.html", 668 "Tag <input> on line 3 contains more than one 'value' attribute."); 669 } 670 671 /** @since 4.0 */ 672 public void testSlashInComponentType() throws Exception 673 { 674 TemplateToken[] tokens = run("SlashInComponentType.html", new ParserDelegate()); 675 676 assertEquals(6, tokens.length); 677 678 OpenToken token1 = (OpenToken) tokens[1]; 679 680 assertEquals("$foo$Bar", token1.getId()); 681 assertEquals("foo/Bar", token1.getComponentType()); 682 683 OpenToken token2 = (OpenToken) tokens[4]; 684 685 assertEquals("baz", token2.getId()); 686 assertEquals("biff/bop/Boop", token2.getComponentType()); 687 } 688 }