// Copyright 2004, 2005 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.junit.parse;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
import org.apache.hivemind.Location;
import org.apache.hivemind.Resource;
import org.apache.hivemind.impl.DefaultClassResolver;
import org.apache.hivemind.util.ClasspathResource;
import org.apache.tapestry.parse.ITemplateParserDelegate;
import org.apache.tapestry.parse.LocalizationToken;
import org.apache.tapestry.parse.OpenToken;
import org.apache.tapestry.parse.TemplateParseException;
import org.apache.tapestry.parse.TemplateParser;
import org.apache.tapestry.parse.TemplateToken;
import org.apache.tapestry.parse.TemplateTokenFactory;
import org.apache.tapestry.parse.TextToken;
import org.apache.tapestry.parse.TokenType;
/**
* Tests for the Tapestry HTML template parser.
*
* @author Howard Lewis Ship
*/
public class TestTemplateParser extends TestCase
{
private static class ParserDelegate implements ITemplateParserDelegate
{
private final String _componentAttributeName;
public ParserDelegate()
{
this("jwcid");
}
public ParserDelegate(String componentAttributeName)
{
_componentAttributeName = componentAttributeName;
}
public boolean getKnownComponent(String componentId)
{
return true;
}
public boolean getAllowBody(String componentId, Location location)
{
return true;
}
public boolean getAllowBody(String libraryId, String type, Location location)
{
return true;
}
public String getComponentAttributeName()
{
return _componentAttributeName;
}
}
protected TemplateToken[] run(char[] templateData, ITemplateParserDelegate delegate,
Resource location) throws TemplateParseException
{
TemplateParser parser = new TemplateParser();
parser.setFactory(new TemplateTokenFactory());
return parser.parse(templateData, delegate, location);
}
protected TemplateToken[] run(InputStream stream, ITemplateParserDelegate delegate,
Resource location) throws TemplateParseException
{
StringBuffer buffer = new StringBuffer();
char[] block = new char[1000];
InputStreamReader reader = new InputStreamReader(stream);
try
{
while (true)
{
int count = reader.read(block, 0, block.length);
if (count < 0)
break;
buffer.append(block, 0, count);
}
reader.close();
}
catch (IOException ex)
{
fail("Unable to read from stream.");
}
return run(buffer.toString().toCharArray(), delegate, location);
}
protected TemplateToken[] run(String file) throws TemplateParseException
{
return run(file, new ParserDelegate());
}
protected TemplateToken[] run(String file, ITemplateParserDelegate delegate)
throws TemplateParseException
{
String thisClassName = getClass().getName();
String thisPath = "/" + thisClassName.replace('.', '/') + "/" + file;
Resource location = new ClasspathResource(new DefaultClassResolver(), thisPath);
InputStream stream = getClass().getResourceAsStream(file);
if (stream == null)
throw new TemplateParseException("File " + file + " not found.");
return run(stream, delegate, location);
}
private Map buildMap(String[] input)
{
Map result = new HashMap();
for (int i = 0; i < input.length; i += 2)
result.put(input[i], input[i + 1]);
return result;
}
// Note: the API of TextToken changed ... from startIndex/endIndex to offset/length.
// Rather than change *all* the tests, we'll just adjust here.
protected void assertTextToken(TemplateToken token, int startIndex, int endIndex)
{
TextToken t = (TextToken) token;
int expectedLength = endIndex - startIndex + 1;
assertEquals("Text token type.", TokenType.TEXT, t.getType());
assertEquals("Text token start index.", startIndex, t.getOffset());
assertEquals("Text token end index.", expectedLength, t.getLength());
}
/** @since 3.0 * */
protected void checkLine(TemplateToken token, int line)
{
assertEquals("Token line", line, token.getLocation().getLineNumber());
}
/** @since 2.0.4 * */
protected void assertLocalizationToken(TemplateToken token, String key, Map attributes, int line)
{
LocalizationToken t = (LocalizationToken) token;
assertEquals("Localization token type.", TokenType.LOCALIZATION, t.getType());
assertEquals("Localization key.", key, t.getKey());
assertEquals("Localization attributes.", attributes, t.getAttributes());
checkLine(token, line);
}
protected void assertOpenToken(TemplateToken token, String id, String tag, int line)
{
assertOpenToken(token, id, null, tag, line);
}
protected void assertOpenToken(TemplateToken token, String id, String componentType,
String tag, int line)
{
OpenToken t = (OpenToken) token;
assertEquals("Open token type", TokenType.OPEN, t.getType());
assertEquals("Open token id", id, t.getId());
assertEquals("Open token component type", componentType, t.getComponentType());
assertEquals("Open token tag", tag, t.getTag());
checkLine(token, line);
}
protected void assertTemplateAttributes(TemplateToken token, Map expected)
{
OpenToken t = (OpenToken) token;
assertEquals("Attributes", expected, t.getAttributesMap());
}
protected void assertCloseToken(TemplateToken token, int line)
{
assertEquals("Close token type.", TokenType.CLOSE, token.getType());
checkLine(token, line);
}
protected void assertTokenCount(TemplateToken[] tokens, int count)
{
assertNotNull("Parsed tokens.", tokens);
assertEquals("Parsed token count.", count, tokens.length);
}
private void runFailure(String file, String message)
{
runFailure(file, new ParserDelegate(), message);
}
private void runFailure(String file, ITemplateParserDelegate delegate, String message)
{
try
{
run(file, delegate);
fail("Invalid document " + file + " parsed without exception.");
}
catch (TemplateParseException ex)
{
assertEquals(message, ex.getMessage());
assertTrue(ex.getLocation().toString().indexOf(file) > 0);
}
}
public void testAllStatic() throws TemplateParseException
{
TemplateToken[] tokens = run("AllStatic.html");
assertTokenCount(tokens, 1);
assertTextToken(tokens[0], 0, 172);
}
public void testSingleEmptyTag() throws TemplateParseException
{
TemplateToken[] tokens = run("SingleEmptyTag.html");
assertTokenCount(tokens, 4);
assertTextToken(tokens[0], 0, 38);
assertOpenToken(tokens[1], "emptyTag", "span", 3);
assertCloseToken(tokens[2], 3);
assertTextToken(tokens[3], 63, 102);
}
public void testSimpleNested() throws TemplateParseException
{
TemplateToken[] tokens = run("SimpleNested.html");
assertTokenCount(tokens, 8);
assertOpenToken(tokens[1], "outer", "span", 3);
assertOpenToken(tokens[3], "inner", "span", 4);
assertCloseToken(tokens[4], 4);
assertCloseToken(tokens[6], 5);
}
public void testMixedNesting() throws TemplateParseException
{
TemplateToken[] tokens = run("MixedNesting.html");
assertTokenCount(tokens, 5);
assertOpenToken(tokens[1], "row", "span", 4);
assertCloseToken(tokens[3], 7);
}
public void testSingleQuotes() throws TemplateParseException
{
TemplateToken[] tokens = run("SingleQuotes.html");
assertTokenCount(tokens, 7);
assertOpenToken(tokens[1], "first", "span", 5);
assertOpenToken(tokens[4], "second", "span", 7);
}
public void testComplex() throws TemplateParseException
{
TemplateToken[] tokens = run("Complex.html");
assertTokenCount(tokens, 19);
// Just pick a few highlights out of it.
assertOpenToken(tokens[1], "ifData", "span", 3);
assertOpenToken(tokens[3], "e", "span", 10);
assertOpenToken(tokens[5], "row", "tr", 11);
}
public void testStartWithStaticTag() throws TemplateParseException
{
TemplateToken[] tokens = run("StartWithStaticTag.html");
assertTokenCount(tokens, 4);
assertTextToken(tokens[0], 0, 232);
assertOpenToken(tokens[1], "justBecause", "span", 9);
}
public void testUnterminatedCommentFailure()
{
runFailure("UnterminatedComment.html", "Comment on line 3 did not end.");
}
public void testUnclosedOpenTagFailure()
{
runFailure("UnclosedOpenTag.html", "Tag <body> on line 4 is never closed.");
}
public void testMissingAttributeValueFailure()
{
runFailure(
"MissingAttributeValue.html",
"Tag <img> on line 9 is missing a value for attribute src.");
}
public void testIncompleteCloseFailure()
{
runFailure("IncompleteClose.html", "Incomplete close tag on line 6.");
}
public void testMismatchedCloseTagsFailure()
{
runFailure(
"MismatchedCloseTags.html",
"Closing tag </th> on line 9 does not have a matching open tag.");
}
public void testInvalidDynamicNestingFailure()
{
runFailure(
"InvalidDynamicNesting.html",
"Closing tag </body> on line 12 is improperly nested with tag <span> on line 8.");
}
public void testUnknownComponentIdFailure()
{
ITemplateParserDelegate delegate = new ITemplateParserDelegate()
{
public boolean getKnownComponent(String componentId)
{
return !componentId.equals("row");
}
public boolean getAllowBody(String componentId, Location location)
{
return true;
}
public boolean getAllowBody(String libraryId, String type, Location location)
{
return true;
}
public String getComponentAttributeName()
{
return "jwcid";
}
};
runFailure(
"Complex.html",
delegate,
"Tag <tr> on line 11 references unknown component id 'row'.");
}
public void testBasicRemove() throws TemplateParseException
{
TemplateToken[] tokens = run("BasicRemove.html");
assertTokenCount(tokens, 10);
assertTextToken(tokens[0], 0, 119);
assertTextToken(tokens[1], 188, 268);
assertOpenToken(tokens[2], "e", "span", 23);
assertTextToken(tokens[3], 341, 342);
assertOpenToken(tokens[4], "row", "tr", 24);
assertTextToken(tokens[5], 359, 377);
assertCloseToken(tokens[6], 26);
assertTextToken(tokens[7], 383, 383);
assertCloseToken(tokens[8], 27);
assertTextToken(tokens[9], 391, 401);
}
public void testBodyRemove() throws TemplateParseException
{
ITemplateParserDelegate delegate = new ITemplateParserDelegate()
{
public boolean getKnownComponent(String id)
{
return true;
}
public boolean getAllowBody(String id, Location location)
{
return id.equals("form");
}
public boolean getAllowBody(String libraryId, String type, Location location)
{
return true;
}
public String getComponentAttributeName()
{
return "jwcid";
}
};
TemplateToken[] tokens = run("BodyRemove.html", delegate);
assertTokenCount(tokens, 8);
assertOpenToken(tokens[1], "form", "form", 9);
assertOpenToken(tokens[3], "inputType", "select", 11);
assertCloseToken(tokens[4], 15);
assertCloseToken(tokens[6], 16);
}
public void testRemovedComponentFailure()
{
runFailure(
"RemovedComponent.html",
"Tag <span> on line 5 is a dynamic component, and may not appear inside an ignored block.");
}
public void testNestedRemoveFailure()
{
runFailure(
"NestedRemove.html",
"Tag <span> on line 4 should be ignored, but is already inside "
+ "an ignored block (ignored blocks may not be nested).");
}
public void testBasicContent() throws TemplateParseException
{
TemplateToken[] tokens = run("BasicContent.html");
assertTokenCount(tokens, 4);
assertTextToken(tokens[0], 108, 165);
assertOpenToken(tokens[1], "nested", "span", 9);
assertCloseToken(tokens[2], 9);
assertTextToken(tokens[3], 188, 192);
}
public void testIgnoredContentFailure()
{
runFailure(
"IgnoredContent.html",
"Tag <td> on line 7 is the template content, and may not be in an ignored block.");
}
public void testTagAttributes() throws TemplateParseException
{
TemplateToken[] tokens = run("TagAttributes.html");
assertTokenCount(tokens, 5);
assertOpenToken(tokens[1], "tag", null, "span", 3);
assertTemplateAttributes(tokens[1], buildMap(new String[]
{ "class", "zip", "align", "right", "color", "#ff00ff" }));
}
/**
* @since 2.0.4
*/
public void testBasicLocalization() throws TemplateParseException
{
TemplateToken[] tokens = run("BasicLocalization.html");
assertTokenCount(tokens, 3);
assertTextToken(tokens[0], 0, 35);
assertLocalizationToken(tokens[1], "the.localization.key", null, 3);
assertTextToken(tokens[2], 89, 117);
}
/**
* Test that the parser fails if a localization block contains a component.
*
* @since 2.0.4
*/
public void testComponentInsideLocalization()
{
runFailure(
"ComponentInsideLocalization.html",
"Tag <span> on line 9 is a dynamic component, and may not appear inside an ignored block.");
}
/**
* Test that the parser fails if an invisible localization is nested within another invisible
* localization.
*
* @since 2.0.4
*/
public void testNestedLocalizations()
{
runFailure(
"NestedLocalizations.html",
"Tag <span> on line 4 is a dynamic component, and may not appear inside an ignored block.");
}
/**
* Test that the abbreviated form (a tag with no body) works.
*
* @since 2.0.4
*/
public void testEmptyLocalization() throws TemplateParseException
{
TemplateToken[] tokens = run("EmptyLocalization.html");
assertTokenCount(tokens, 3);
assertTextToken(tokens[0], 0, 62);
assertLocalizationToken(tokens[1], "empty.localization", null, 3);
assertTextToken(tokens[2], 97, 122);
}
/**
* Test attributes in the span. Also, checks that the parser caselessly identifies the "key"
* attribute and the tag name ("span").
*
* @since 2.0.4
*/
public void testLocalizationAttributes() throws TemplateParseException
{
TemplateToken[] tokens = run("LocalizationAttributes.html");
Map attributes = buildMap(new String[]
{ "alpha", "beta", "Fred", "Wilma" });
assertLocalizationToken(tokens[1], "localization.with.attributes", attributes, 3);
}
/**
* Tests for implicit components (both named and anonymous).
*
* @since 3.0
*/
public void testImplicitComponents() throws TemplateParseException
{
TemplateToken[] tokens = run("ImplicitComponents.html");
assertTokenCount(tokens, 18);
assertOpenToken(tokens[1], "$Body", "Body", "body", 4);
assertOpenToken(tokens[3], "loop", "Foreach", "tr", 7);
assertTemplateAttributes(tokens[3], buildMap(new String[]
{ "element", "tr", "source", "ognl:items" }));
assertOpenToken(tokens[5], "$Insert", "Insert", "span", 10);
assertTemplateAttributes(tokens[5], buildMap(new String[]
{ "value", "ognl:components.loop.value.name" }));
assertOpenToken(tokens[8], "$Insert_0", "Insert", "span", 11);
assertTemplateAttributes(tokens[8], buildMap(new String[]
{ "value", "ognl:components.loop.value.price" }));
assertOpenToken(tokens[13], "$InspectorButton", "contrib:InspectorButton", "span", 15);
}
/**
* Test for encoded characters in an expression.
*
* @since 3.0
*/
public void testEncodedExpressionCharacters() throws TemplateParseException
{
TemplateToken[] tokens = run("EncodedExpressionCharacters.html");
assertTokenCount(tokens, 3);
assertOpenToken(tokens[0], "$Insert", "Insert", "span", 1);
String expression = "ognl: { \"<&>\", \"Fun!\" }";
assertTemplateAttributes(tokens[0], buildMap(new String[]
{ "value", expression }));
}
/**
* Test ability to read string attributes.
*/
public void testStringAttributes() throws TemplateParseException
{
TemplateToken[] tokens = run("StringAttributes.html");
assertTokenCount(tokens, 4);
assertOpenToken(tokens[1], "$Image", "Image", "img", 2);
assertTemplateAttributes(tokens[1], buildMap(new String[]
{ "image", "ognl:assets.logo", "alt", "message:logo-title" }));
}
/**
* Test ability to use a different attribute name than the default ("jwcid").
*
* @since 4.0
*/
public void testOverrideDefaultAttributeName() throws Exception
{
TemplateToken[] tokens = run("OverrideDefaultAttributeName.html", new ParserDelegate("id"));
assertTokenCount(tokens, 8);
assertOpenToken(tokens[1], "outer", "span", 3);
assertOpenToken(tokens[3], "inner", "span", 4);
assertCloseToken(tokens[4], 4);
assertCloseToken(tokens[6], 5);
}
/**
* Like {@link #testOverrideDefaultAttributeName()}, but uses a more complicated attribute name
* (with a XML-style namespace prefix).
*/
public void testNamespaceAttributeName() throws Exception
{
TemplateToken[] tokens = run("NamespaceAttributeName.html", new ParserDelegate("t:id"));
assertTokenCount(tokens, 8);
assertOpenToken(tokens[1], "outer", "span", 3);
assertOpenToken(tokens[3], "inner", "span", 4);
assertCloseToken(tokens[4], 4);
assertCloseToken(tokens[6], 5);
}
/** @since 4.0 */
public void testDuplicateTagAttributeFailure()
{
runFailure(
"DuplicateTagAttribute.html",
"Tag <input> on line 3 contains more than one 'value' attribute.");
}
/** @since 4.0 */
public void testDuplicateTagAttributeFailureSingleQuotes()
{
runFailure(
"DuplicateTagAttributeSingleQuotes.html",
"Tag <input> on line 3 contains more than one 'value' attribute.");
}
/** @since 4.0 */
public void testSlashInComponentType() throws Exception
{
TemplateToken[] tokens = run("SlashInComponentType.html", new ParserDelegate());
assertEquals(6, tokens.length);
OpenToken token1 = (OpenToken) tokens[1];
assertEquals("$foo$Bar", token1.getId());
assertEquals("foo/Bar", token1.getComponentType());
OpenToken token2 = (OpenToken) tokens[4];
assertEquals("baz", token2.getId());
assertEquals("biff/bop/Boop", token2.getComponentType());
}
}