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    }