diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ConstantExpressionNode.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ConstantExpressionNode.java index f557eec73a..e8df21bb69 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ConstantExpressionNode.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ConstantExpressionNode.java @@ -48,8 +48,8 @@ class ConstantExpressionNode extends ExpressionNode { private final Object value; - ConstantExpressionNode(int lineNumber, Object value) { - super(lineNumber); + ConstantExpressionNode(String resourceName, int lineNumber, Object value) { + super(resourceName, lineNumber); this.value = value; } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/DirectiveNode.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/DirectiveNode.java index 4c17edcb6c..e390588064 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/DirectiveNode.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/DirectiveNode.java @@ -45,8 +45,8 @@ * @author emcmanus@google.com (Éamonn McManus) */ abstract class DirectiveNode extends Node { - DirectiveNode(int lineNumber) { - super(lineNumber); + DirectiveNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } /** @@ -62,7 +62,7 @@ static class SetNode extends DirectiveNode { private final Node expression; SetNode(String var, Node expression) { - super(expression.lineNumber); + super(expression.resourceName, expression.lineNumber); this.var = var; this.expression = expression; } @@ -86,8 +86,13 @@ static class IfNode extends DirectiveNode { private final Node truePart; private final Node falsePart; - IfNode(int lineNumber, ExpressionNode condition, Node trueNode, Node falseNode) { - super(lineNumber); + IfNode( + String resourceName, + int lineNumber, + ExpressionNode condition, + Node trueNode, + Node falseNode) { + super(resourceName, lineNumber); this.condition = condition; this.truePart = trueNode; this.falsePart = falseNode; @@ -112,8 +117,8 @@ static class ForEachNode extends DirectiveNode { private final ExpressionNode collection; private final Node body; - ForEachNode(int lineNumber, String var, ExpressionNode in, Node body) { - super(lineNumber); + ForEachNode(String resourceName, int lineNumber, String var, ExpressionNode in, Node body) { + super(resourceName, lineNumber); this.var = var; this.collection = in; this.body = body; @@ -130,7 +135,7 @@ Object evaluate(EvaluationContext context) { } else if (collectionValue instanceof Map) { iterable = ((Map) collectionValue).values(); } else { - throw new EvaluationException("Not iterable: " + collectionValue); + throw evaluationException("Not iterable: " + collectionValue); } Runnable undo = context.setVar(var, null); StringBuilder sb = new StringBuilder(); @@ -178,8 +183,12 @@ static class MacroCallNode extends DirectiveNode { private final ImmutableList thunks; private Macro macro; - MacroCallNode(int lineNumber, String name, ImmutableList argumentNodes) { - super(lineNumber); + MacroCallNode( + String resourceName, + int lineNumber, + String name, + ImmutableList argumentNodes) { + super(resourceName, lineNumber); this.name = name; this.thunks = argumentNodes; } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ExpressionNode.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ExpressionNode.java index 7978e730c4..c1112cd5b1 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ExpressionNode.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ExpressionNode.java @@ -42,8 +42,8 @@ * @author emcmanus@google.com (Éamonn McManus) */ abstract class ExpressionNode extends Node { - ExpressionNode(int lineNumber) { - super(lineNumber); + ExpressionNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } /** @@ -113,7 +113,7 @@ static class BinaryExpressionNode extends ExpressionNode { final ExpressionNode rhs; BinaryExpressionNode(ExpressionNode lhs, Operator op, ExpressionNode rhs) { - super(lhs.lineNumber); + super(lhs.resourceName, lhs.lineNumber); this.lhs = lhs; this.op = op; this.rhs = rhs; @@ -194,7 +194,7 @@ static class NotExpressionNode extends ExpressionNode { private final ExpressionNode expr; NotExpressionNode(ExpressionNode expr) { - super(expr.lineNumber); + super(expr.resourceName, expr.lineNumber); this.expr = expr; } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Node.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Node.java index 4f54341e6a..5136fbf6de 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Node.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Node.java @@ -40,9 +40,11 @@ * @author emcmanus@google.com (Éamonn McManus) */ abstract class Node { + final String resourceName; final int lineNumber; - Node(int lineNumber) { + Node(String resourceName, int lineNumber) { + this.resourceName = resourceName; this.lineNumber = lineNumber; } @@ -55,20 +57,28 @@ abstract class Node { */ abstract Object evaluate(EvaluationContext context); + private String where() { + String where = "In expression on line " + lineNumber; + if (resourceName != null) { + where += " of " + resourceName; + } + return where; + } + EvaluationException evaluationException(String message) { - return new EvaluationException("In expression on line " + lineNumber + ": " + message); + return new EvaluationException(where() + ": " + message); } EvaluationException evaluationException(Throwable cause) { - return new EvaluationException("In expression on line " + lineNumber + ": " + cause, cause); + return new EvaluationException(where() + ": " + cause, cause); } /** * Returns an empty node in the parse tree. This is used for example to represent the trivial * "else" part of an {@code #if} that does not have an explicit {@code #else}. */ - static Node emptyNode(int lineNumber) { - return new Cons(lineNumber, ImmutableList.of()); + static Node emptyNode(String resourceName, int lineNumber) { + return new Cons(resourceName, lineNumber, ImmutableList.of()); } /** @@ -76,15 +86,15 @@ static Node emptyNode(int lineNumber) { * new node produces the same string as evaluating each of the given nodes and concatenating the * result. */ - static Node cons(int lineNumber, ImmutableList nodes) { - return new Cons(lineNumber, nodes); + static Node cons(String resourceName, int lineNumber, ImmutableList nodes) { + return new Cons(resourceName, lineNumber, nodes); } private static final class Cons extends Node { private final ImmutableList nodes; - Cons(int lineNumber, ImmutableList nodes) { - super(lineNumber); + Cons(String resourceName, int lineNumber, ImmutableList nodes) { + super(resourceName, lineNumber); this.nodes = nodes; } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ParseException.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ParseException.java index 72961a1b7c..45507f05bf 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ParseException.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ParseException.java @@ -40,11 +40,19 @@ public class ParseException extends RuntimeException { private static final long serialVersionUID = 1; - ParseException(String message, int lineNumber) { - super(message + ", on line " + lineNumber); + ParseException(String message, String resourceName, int lineNumber) { + super(message + ", " + where(resourceName, lineNumber)); } - ParseException(String message, int lineNumber, String context) { - super(message + ", on line " + lineNumber + ", at text starting: " + context); + ParseException(String message, String resourceName, int lineNumber, String context) { + super(message + ", " + where(resourceName, lineNumber) + ", at text starting: " + context); + } + + private static String where(String resourceName, int lineNumber) { + if (resourceName == null) { + return "on line " + lineNumber; + } else { + return "on line " + lineNumber + " of " + resourceName; + } } } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java index 5bac24d2fa..4caa892cde 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Parser.java @@ -47,6 +47,7 @@ import com.google.auto.value.processor.escapevelocity.TokenNode.ForEachTokenNode; import com.google.auto.value.processor.escapevelocity.TokenNode.IfTokenNode; import com.google.auto.value.processor.escapevelocity.TokenNode.MacroDefinitionTokenNode; +import com.google.auto.value.processor.escapevelocity.TokenNode.NestedTokenNode; import com.google.common.base.CharMatcher; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; @@ -68,6 +69,8 @@ class Parser { private static final int EOF = -1; private final LineNumberReader reader; + private final String resourceName; + private final Template.ResourceOpener resourceOpener; /** * The invariant of this parser is that {@code c} is always the next character of interest. @@ -77,10 +80,13 @@ class Parser { */ private int c; - Parser(Reader reader) throws IOException { + Parser(Reader reader, String resourceName, Template.ResourceOpener resourceOpener) + throws IOException { this.reader = new LineNumberReader(reader); this.reader.setLineNumber(1); next(); + this.resourceName = resourceName; + this.resourceOpener = resourceOpener; } /** @@ -118,13 +124,18 @@ class Parser { * define a simple parser over the resultant tokens that is the second phase. */ Template parse() throws IOException { + ImmutableList tokens = parseTokens(); + return new Reparser(tokens).reparse(); + } + + private ImmutableList parseTokens() throws IOException { ImmutableList.Builder tokens = ImmutableList.builder(); Node token; do { token = parseNode(); tokens.add(token); } while (!(token instanceof EofNode)); - return new Reparser(tokens.build()).reparse(); + return tokens.build(); } private int lineNumber() { @@ -186,16 +197,53 @@ private Node parseNode() throws IOException { next(); if (c == '#') { return parseComment(); - } else { + } else if (isAsciiLetter(c) || c == '{') { return parseDirective(); + } else if (c == '[') { + return parseHashSquare(); + } else { + // For consistency with Velocity, we treat # not followed by # or a letter as a plain + // character, and we treat #$foo as a literal # followed by the reference $foo. + // But the # is its own ConstantExpressionNode; we don't try to merge it with adjacent text. + return new ConstantExpressionNode(resourceName, lineNumber(), "#"); } } if (c == EOF) { - return new EofNode(lineNumber()); + return new EofNode(resourceName, lineNumber()); } return parseNonDirective(); } + private Node parseHashSquare() throws IOException { + // We've just seen #[ which might be the start of a #[[quoted block]]#. If the next character + // is not another [ then it's not a quoted block, but it *is* a literal #[ followed by whatever + // that next character is. + assert c == '['; + next(); + if (c != '[') { + return new ConstantExpressionNode(resourceName, lineNumber(), "#["); + } + next(); + StringBuilder sb = new StringBuilder(); + while (true) { + if (c == EOF) { + throw parseException("Unterminated #[[ - did not see matching ]]#"); + } + if (c == '#') { + // This might be the last character of ]]# or it might just be a random #. + int len = sb.length(); + if (len > 1 && sb.charAt(len - 1) == ']' && sb.charAt(len - 2) == ']') { + next(); + break; + } + } + sb.append((char) c); + next(); + } + String quoted = sb.substring(0, sb.length() - 2); + return new ConstantExpressionNode(resourceName, lineNumber(), quoted); + } + /** * Parses a single non-directive node from the reader. *
{@code
@@ -228,6 +276,7 @@ private Node parseNonDirective() throws IOException {
    *                 |
    *                 |
    *                 |
+   *                 |
    *                 |
    *                 |
    *                
@@ -243,20 +292,31 @@ private Node parseDirective() throws IOException {
       directive = parseId("Directive");
     }
     Node node;
-    if (directive.equals("end")) {
-      node = new EndTokenNode(lineNumber());
-    } else if (directive.equals("if") || directive.equals("elseif")) {
-      node = parseIfOrElseIf(directive);
-    } else if (directive.equals("else")) {
-      node = new ElseTokenNode(lineNumber());
-    } else if (directive.equals("foreach")) {
-      node = parseForEach();
-    } else if (directive.equals("set")) {
-      node = parseSet();
-    } else if (directive.equals("macro")) {
-      node = parseMacroDefinition();
-    } else {
-      node = parsePossibleMacroCall(directive);
+    switch (directive) {
+      case "end":
+        node = new EndTokenNode(resourceName, lineNumber());
+        break;
+      case "if":
+      case "elseif":
+        node = parseIfOrElseIf(directive);
+        break;
+      case "else":
+        node = new ElseTokenNode(resourceName, lineNumber());
+        break;
+      case "foreach":
+        node = parseForEach();
+        break;
+      case "set":
+        node = parseSet();
+        break;
+      case "parse":
+        node = parseParse();
+        break;
+      case "macro":
+        node = parseMacroDefinition();
+        break;
+      default:
+        node = parsePossibleMacroCall(directive);
     }
     // Velocity skips a newline after any directive.
     // TODO(emcmanus): in fact it also skips space before the newline, which should be implemented.
@@ -325,6 +385,34 @@ private Node parseSet() throws IOException {
     return new SetNode(var, expression);
   }
 
+  /**
+   * Parses a {@code #parse} token from the reader. 
{@code
+   *  -> #parse (  )
+   * }
+ * + *

The way this works is inconsistent with Velocity. In Velocity, the {@code #parse} directive + * is evaluated when it is encountered during template evaluation. That means that the argument + * can be a variable, and it also means that you can use {@code #if} to choose whether or not + * to do the {@code #parse}. Neither of those is true in EscapeVelocity. The contents of the + * {@code #parse} are integrated into the containing template pretty much as if they had been + * written inline. That also means that EscapeVelocity allows forward references to macros + * inside {@code #parse} directives, which Velocity does not. + */ + private Node parseParse() throws IOException { + expect('('); + skipSpace(); + if (c != '"') { + throw parseException("#parse only supported with string literal argument"); + } + String nestedResourceName = readStringLiteral(); + expect(')'); + try (Reader nestedReader = resourceOpener.openResource(nestedResourceName)) { + Parser nestedParser = new Parser(nestedReader, nestedResourceName, resourceOpener); + ImmutableList nestedTokens = nestedParser.parseTokens(); + return new NestedTokenNode(nestedResourceName, nestedTokens); + } + } + /** * Parses a {@code #macro} token from the reader.

{@code
    *  -> #macro (   )
@@ -351,7 +439,7 @@ private Node parseMacroDefinition() throws IOException {
       next();
       parameterNames.add(parseId("Macro parameter name"));
     }
-    return new MacroDefinitionTokenNode(lineNumber(), name, parameterNames.build());
+    return new MacroDefinitionTokenNode(resourceName, lineNumber(), name, parameterNames.build());
   }
 
   /**
@@ -386,7 +474,8 @@ private Node parsePossibleMacroCall(String directive) throws IOException {
         next();
       }
     }
-    return new DirectiveNode.MacroCallNode(lineNumber(), directive, parameterNodes.build());
+    return new DirectiveNode.MacroCallNode(
+        resourceName, lineNumber(), directive, parameterNodes.build());
   }
 
   /**
@@ -399,7 +488,7 @@ private Node parseComment() throws IOException {
       next();
     }
     next();
-    return new CommentTokenNode(lineNumber);
+    return new CommentTokenNode(resourceName, lineNumber);
   }
 
   /**
@@ -418,11 +507,13 @@ private Node parsePlainText(int firstChar) throws IOException {
         case '$':
         case '#':
           break literal;
+        default:
+          // Just some random character.
       }
       sb.appendCodePoint(c);
       next();
     }
-    return new ConstantExpressionNode(lineNumber(), sb.toString());
+    return new ConstantExpressionNode(resourceName, lineNumber(), sb.toString());
   }
 
   /**
@@ -457,7 +548,7 @@ private ReferenceNode parseReference() throws IOException {
    */
   private ReferenceNode parseReferenceNoBrace() throws IOException {
     String id = parseId("Reference");
-    ReferenceNode lhs = new PlainReferenceNode(lineNumber(), id);
+    ReferenceNode lhs = new PlainReferenceNode(resourceName, lineNumber(), id);
     return parseReferenceSuffix(lhs);
   }
 
@@ -487,13 +578,13 @@ private ReferenceNode parseReferenceSuffix(ReferenceNode lhs) throws IOException
    * Parses a reference member, which is either a property reference like {@code $x.y} or a method
    * call like {@code $x.y($z)}.
    * 
{@code
-   *  -> .
-   *  ->  |
+   *  -> .
+   *  ->  |
    *                                    (  )
    * }
* * @param lhs the reference node representing what appears to the left of the dot, like the - * {@code $x} in {@code $x.foo} or {@code $x.foo()}. + * {@code $x} in {@code $x.foo} or {@code $x.foo()}. */ private ReferenceNode parseReferenceMember(ReferenceNode lhs) throws IOException { assert c == '.'; @@ -518,7 +609,7 @@ private ReferenceNode parseReferenceMember(ReferenceNode lhs) throws IOException * }
* * @param lhs the reference node representing what appears to the left of the dot, like the - * {@code $x} in {@code $x.foo()}. + * {@code $x} in {@code $x.foo()}. */ private ReferenceNode parseReferenceMethodParams(ReferenceNode lhs, String id) throws IOException { @@ -547,7 +638,7 @@ private ReferenceNode parseReferenceMethodParams(ReferenceNode lhs, String id) * }
* * @param lhs the reference node representing what appears to the left of the dot, like the - * {@code $x} in {@code $x[$i]}. + * {@code $x} in {@code $x[$i]}. */ private ReferenceNode parseReferenceIndex(ReferenceNode lhs) throws IOException { assert c == '['; @@ -764,6 +855,10 @@ private ExpressionNode parsePrimary() throws IOException { } private ExpressionNode parseStringLiteral() throws IOException { + return new ConstantExpressionNode(resourceName, lineNumber(), readStringLiteral()); + } + + private String readStringLiteral() throws IOException { assert c == '"'; StringBuilder sb = new StringBuilder(); next(); @@ -782,7 +877,7 @@ private ExpressionNode parseStringLiteral() throws IOException { next(); } next(); - return new ConstantExpressionNode(lineNumber(), sb.toString()); + return sb.toString(); } private ExpressionNode parseIntLiteral(String prefix) throws IOException { @@ -795,7 +890,7 @@ private ExpressionNode parseIntLiteral(String prefix) throws IOException { if (value == null) { throw parseException("Invalid integer: " + sb); } - return new ConstantExpressionNode(lineNumber(), value); + return new ConstantExpressionNode(resourceName, lineNumber(), value); } /** @@ -813,7 +908,7 @@ private ExpressionNode parseBooleanLiteral() throws IOException { } else { throw parseException("Identifier in expression must be preceded by $ or be true or false"); } - return new ConstantExpressionNode(lineNumber(), value); + return new ConstantExpressionNode(resourceName, lineNumber(), value); } private static final CharMatcher ASCII_LETTER = @@ -880,6 +975,6 @@ private ParseException parseException(String message) throws IOException { context.append("..."); } } - return new ParseException(message, lineNumber(), context.toString()); + return new ParseException(message, resourceName, lineNumber(), context.toString()); } } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/README.md b/value/src/main/java/com/google/auto/value/processor/escapevelocity/README.md index 4edee7b6da..9882aea879 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/README.md +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/README.md @@ -306,6 +306,44 @@ Macros can make templates hard to understand. You may prefer to put the logic in rather than a macro, and call the method from the template using `$methods.doSomething("foo")` or whatever. +## Block quoting + +If you have text that should be treated verbatim, you can enclose it in `#[[...]]#`. The text +represented by `...` will be copied into the output. `#` and `$` characters will have no +effect in that text. + +``` +#[[ This is not a #directive, and this is not a $variable. ]]# +``` + +## Including other templates + +If you want to include a template from another file, you can use the `#parse` directive. +This can be useful if you have macros that are shared between templates, for example. + +``` +#set ($foo = "bar") +#parse("macros.vm") +#mymacro($foo) ## #mymacro defined in macros.vm +``` + +For this to work, you will need to tell EscapeVelocity how to find "resources" such as +`macro.vm` in the example. You might use something like this: + +``` +ResourceOpener resourceOpener = resourceName -> { + InputStream inputStream = getClass().getResource(resourceName); + if (inputStream == null) { + throw new IOException("Unknown resource: " + resourceName); + } + return new BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)); +}; +Template template = Template.parseFrom("foo.vm", resourceOpener); +``` + +In this case, the `resourceOpener` is used to find the main template `foo.vm`, as well as any +templates it may reference in `#parse` directives. + ## Spaces For the most part, spaces and newlines in the template are preserved exactly in the output. diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ReferenceNode.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ReferenceNode.java index e32234a672..7d6873f31c 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/ReferenceNode.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/ReferenceNode.java @@ -51,8 +51,8 @@ * @author emcmanus@google.com (Éamonn McManus) */ abstract class ReferenceNode extends ExpressionNode { - ReferenceNode(int lineNumber) { - super(lineNumber); + ReferenceNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } /** @@ -62,8 +62,8 @@ abstract class ReferenceNode extends ExpressionNode { static class PlainReferenceNode extends ReferenceNode { final String id; - PlainReferenceNode(int lineNumber, String id) { - super(lineNumber); + PlainReferenceNode(String resourceName, int lineNumber, String id) { + super(resourceName, lineNumber); this.id = id; } @@ -71,7 +71,7 @@ static class PlainReferenceNode extends ReferenceNode { if (context.varIsDefined(id)) { return context.getVar(id); } else { - throw new EvaluationException("Undefined reference $" + id); + throw evaluationException("Undefined reference $" + id); } } @@ -94,7 +94,7 @@ static class MemberReferenceNode extends ReferenceNode { final String id; MemberReferenceNode(ReferenceNode lhs, String id) { - super(lhs.lineNumber); + super(lhs.resourceName, lhs.lineNumber); this.lhs = lhs; this.id = id; } @@ -105,7 +105,7 @@ static class MemberReferenceNode extends ReferenceNode { @Override Object evaluate(EvaluationContext context) { Object lhsValue = lhs.evaluate(context); if (lhsValue == null) { - throw new EvaluationException("Cannot get member " + id + " of null value"); + throw evaluationException("Cannot get member " + id + " of null value"); } // Velocity specifies that, given a reference .foo, it will first look for getfoo() and then // for getFoo(), and likewise given .Foo it will look for getFoo() and then getfoo(). @@ -125,7 +125,7 @@ static class MemberReferenceNode extends ReferenceNode { } } } - throw new EvaluationException( + throw evaluationException( "Member " + id + " does not correspond to a public getter of " + lhsValue + ", a " + lhsValue.getClass().getName()); } @@ -152,7 +152,7 @@ static class IndexReferenceNode extends ReferenceNode { final ExpressionNode index; IndexReferenceNode(ReferenceNode lhs, ExpressionNode index) { - super(lhs.lineNumber); + super(lhs.resourceName, lhs.lineNumber); this.lhs = lhs; this.index = index; } @@ -160,17 +160,17 @@ static class IndexReferenceNode extends ReferenceNode { @Override Object evaluate(EvaluationContext context) { Object lhsValue = lhs.evaluate(context); if (lhsValue == null) { - throw new EvaluationException("Cannot index null value"); + throw evaluationException("Cannot index null value"); } if (lhsValue instanceof List) { Object indexValue = index.evaluate(context); if (!(indexValue instanceof Integer)) { - throw new EvaluationException("List index is not an integer: " + indexValue); + throw evaluationException("List index is not an integer: " + indexValue); } List lhsList = (List) lhsValue; int i = (Integer) indexValue; if (i < 0 || i >= lhsList.size()) { - throw new EvaluationException( + throw evaluationException( "List index " + i + " is not valid for list of size " + lhsList.size()); } return lhsList.get(i); @@ -196,7 +196,7 @@ static class MethodReferenceNode extends ReferenceNode { final List args; MethodReferenceNode(ReferenceNode lhs, String id, List args) { - super(lhs.lineNumber); + super(lhs.resourceName, lhs.lineNumber); this.lhs = lhs; this.id = id; this.args = args; @@ -224,11 +224,11 @@ static class MethodReferenceNode extends ReferenceNode { if (lhsValue == null) { throw evaluationException("Cannot invoke method " + id + " on null value"); } - List argValues = new ArrayList(); + List argValues = new ArrayList<>(); for (ExpressionNode arg : args) { argValues.add(arg.evaluate(context)); } - List methodsWithName = Lists.newArrayList(); + List methodsWithName = new ArrayList<>(); for (Method method : lhsValue.getClass().getMethods()) { if (method.getName().equals(id) && !method.isSynthetic()) { methodsWithName.add(method); @@ -252,8 +252,8 @@ static class MethodReferenceNode extends ReferenceNode { return invokeMethod(Iterables.getOnlyElement(compatibleMethods), lhsValue, argValues); default: throw evaluationException( - "Ambiguous method invocation, could be one of:" - + Joiner.on('\n').join(compatibleMethods)); + "Ambiguous method invocation, could be one of:\n " + + Joiner.on("\n ").join(compatibleMethods)); } } diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Reparser.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Reparser.java index a6fb2c7cab..e748905fa8 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Reparser.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Reparser.java @@ -47,6 +47,7 @@ import com.google.auto.value.processor.escapevelocity.TokenNode.IfOrElseIfTokenNode; import com.google.auto.value.processor.escapevelocity.TokenNode.IfTokenNode; import com.google.auto.value.processor.escapevelocity.TokenNode.MacroDefinitionTokenNode; +import com.google.auto.value.processor.escapevelocity.TokenNode.NestedTokenNode; import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -89,17 +90,25 @@ class Reparser { private final Map macros; Reparser(ImmutableList nodes) { + this(nodes, Maps.newTreeMap()); + } + + private Reparser(ImmutableList nodes, Map macros) { this.nodes = removeSpaceBeforeSet(nodes); this.nodeIndex = 0; - this.macros = Maps.newTreeMap(); + this.macros = macros; } Template reparse() { - Node root = parseTo(EOF_SET, new EofNode(1)); + Node root = reparseNodes(); linkMacroCalls(); return new Template(root); } + private Node reparseNodes() { + return parseTo(EOF_SET, new EofNode((String) null, 1)); + } + /** * Returns a copy of the given list where spaces have been moved where appropriate after {@code * #set}. This hack is needed to match what appears to be special treatment in Apache Velocity of @@ -162,7 +171,9 @@ private Node parseTo(Set> stopSet, TokenNode forWhat) } if (currentNode instanceof EofNode) { throw new ParseException( - "Reached end of file while parsing " + forWhat.name(), forWhat.lineNumber); + "Reached end of file while parsing " + forWhat.name(), + forWhat.resourceName, + forWhat.lineNumber); } Node parsed; if (currentNode instanceof TokenNode) { @@ -173,7 +184,7 @@ private Node parseTo(Set> stopSet, TokenNode forWhat) } nodeList.add(parsed); } - return Node.cons(forWhat.lineNumber, nodeList.build()); + return Node.cons(forWhat.resourceName, forWhat.lineNumber, nodeList.build()); } private Node currentNode() { @@ -194,11 +205,13 @@ private Node parseTokenNode() { TokenNode tokenNode = (TokenNode) currentNode(); nextNode(); if (tokenNode instanceof CommentTokenNode) { - return emptyNode(tokenNode.lineNumber); + return emptyNode(tokenNode.resourceName, tokenNode.lineNumber); } else if (tokenNode instanceof IfTokenNode) { return parseIfOrElseIf((IfTokenNode) tokenNode); } else if (tokenNode instanceof ForEachTokenNode) { return parseForEach((ForEachTokenNode) tokenNode); + } else if (tokenNode instanceof NestedTokenNode) { + return parseNested((NestedTokenNode) tokenNode); } else if (tokenNode instanceof MacroDefinitionTokenNode) { return parseMacroDefinition((MacroDefinitionTokenNode) tokenNode); } else { @@ -210,7 +223,8 @@ private Node parseTokenNode() { private Node parseForEach(ForEachTokenNode forEach) { Node body = parseTo(END_SET, forEach); nextNode(); // Skip #end - return new ForEachNode(forEach.lineNumber, forEach.var, forEach.collection, body); + return new ForEachNode( + forEach.resourceName, forEach.lineNumber, forEach.var, forEach.collection, body); } private Node parseIfOrElseIf(IfOrElseIfTokenNode ifOrElseIf) { @@ -219,7 +233,7 @@ private Node parseIfOrElseIf(IfOrElseIfTokenNode ifOrElseIf) { Node token = currentNode(); nextNode(); // Skip #else or #elseif (cond) or #end. if (token instanceof EndTokenNode) { - falsePart = emptyNode(token.lineNumber); + falsePart = emptyNode(token.resourceName, token.lineNumber); } else if (token instanceof ElseTokenNode) { falsePart = parseTo(END_SET, ifOrElseIf); nextNode(); // Skip #end @@ -232,7 +246,17 @@ private Node parseIfOrElseIf(IfOrElseIfTokenNode ifOrElseIf) { } else { throw new AssertionError(currentNode()); } - return new IfNode(ifOrElseIf.lineNumber, ifOrElseIf.condition, truePart, falsePart); + return new IfNode( + ifOrElseIf.resourceName, ifOrElseIf.lineNumber, ifOrElseIf.condition, truePart, falsePart); + } + + // This is a #parse("foo.vm") directive. We've already done the first phase of parsing on the + // contents of foo.vm. Now we need to do the second phase, and insert the result into the + // reparsed nodes. We can call Reparser recursively, but we must ensure that any macros found + // are added to the containing Reparser's macro definitions. + private Node parseNested(NestedTokenNode nested) { + Reparser reparser = new Reparser(nested.nodes, this.macros); + return reparser.reparseNodes(); } private Node parseMacroDefinition(MacroDefinitionTokenNode macroDefinition) { @@ -243,7 +267,7 @@ private Node parseMacroDefinition(MacroDefinitionTokenNode macroDefinition) { macroDefinition.lineNumber, macroDefinition.name, macroDefinition.parameterNames, body); macros.put(macroDefinition.name, macro); } - return emptyNode(macroDefinition.lineNumber); + return emptyNode(macroDefinition.resourceName, macroDefinition.lineNumber); } private void linkMacroCalls() { @@ -260,6 +284,7 @@ private void linkMacroCall(MacroCallNode macroCall) { throw new ParseException( "#" + macroCall.name() + " is neither a standard directive nor a macro that has been defined", + macroCall.resourceName, macroCall.lineNumber); } if (macro.parameterCount() != macroCall.argumentCount()) { @@ -267,6 +292,7 @@ private void linkMacroCall(MacroCallNode macroCall) { "Wrong number of arguments to #" + macroCall.name() + ": expected " + macro.parameterCount() + ", got " + macroCall.argumentCount(), + macroCall.resourceName, macroCall.lineNumber); } macroCall.setMacro(macro); diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Template.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Template.java index 8b48b77ec4..8d7efb9d60 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/Template.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/Template.java @@ -50,10 +50,62 @@ public class Template { private final Node root; /** - * Parse a VTL template from the given {@code Reader}. + * Used to resolve references to resources in the template, through {@code #parse} directives. + * + *

Here is an example that opens nested templates as resources relative to the calling class: + * + *

+   *   ResourceOpener resourceOpener = resourceName -> {
+   *     InputStream inputStream = getClass().getResource(resourceName);
+   *     if (inputStream == null) {
+   *       throw new IOException("Unknown resource: " + resourceName);
+   *     }
+   *     return new BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8));
+   *   };
+   * 
+ */ + @FunctionalInterface + public interface ResourceOpener { + + /** + * Returns a Reader that will be used to read the given resource, then closed. + * + * @param resourceName the name of the resource to be read. This will never be null. + */ + Reader openResource(String resourceName) throws IOException; + } + + /** + * Parses a VTL template from the given {@code Reader}. The given Reader will be closed on + * return from this method. */ public static Template parseFrom(Reader reader) throws IOException { - return new Parser(reader).parse(); + ResourceOpener resourceOpener = resourceName -> { + if (resourceName == null) { + return reader; + } else { + throw new IOException("No ResourceOpener has been configured to read " + resourceName); + } + }; + try { + return parseFrom((String) null, resourceOpener); + } finally { + reader.close(); + } + } + + /** + * Parse a VTL template of the given name using the given {@code ResourceOpener}. + * + * @param resourceName name of the resource. May be null. + * @param resourceOpener used to open included files for {@code #parse} directives in the + * template. + */ + public static Template parseFrom( + String resourceName, ResourceOpener resourceOpener) throws IOException { + try (Reader reader = resourceOpener.openResource(resourceName)) { + return new Parser(reader, resourceName, resourceOpener).parse(); + } } Template(Node root) { diff --git a/value/src/main/java/com/google/auto/value/processor/escapevelocity/TokenNode.java b/value/src/main/java/com/google/auto/value/processor/escapevelocity/TokenNode.java index 236160923c..16734ec0c0 100644 --- a/value/src/main/java/com/google/auto/value/processor/escapevelocity/TokenNode.java +++ b/value/src/main/java/com/google/auto/value/processor/escapevelocity/TokenNode.java @@ -43,8 +43,8 @@ * @author emcmanus@google.com (Éamonn McManus) */ abstract class TokenNode extends Node { - TokenNode(int lineNumber) { - super(lineNumber); + TokenNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } /** @@ -52,7 +52,7 @@ abstract class TokenNode extends Node { * final parse tree. */ @Override Object evaluate(EvaluationContext vars) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(getClass().getName()); } /** @@ -65,8 +65,8 @@ abstract class TokenNode extends Node { * initial token string and also the last one in the parse tree. */ static final class EofNode extends TokenNode { - EofNode(int lineNumber) { - super(lineNumber); + EofNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } @Override @@ -76,8 +76,8 @@ String name() { } static final class EndTokenNode extends TokenNode { - EndTokenNode(int lineNumber) { - super(lineNumber); + EndTokenNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } @Override String name() { @@ -92,8 +92,8 @@ static final class EndTokenNode extends TokenNode { * behaviour. */ static class CommentTokenNode extends TokenNode { - CommentTokenNode(int lineNumber) { - super(lineNumber); + CommentTokenNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } @Override String name() { @@ -105,7 +105,7 @@ abstract static class IfOrElseIfTokenNode extends TokenNode { final ExpressionNode condition; IfOrElseIfTokenNode(ExpressionNode condition) { - super(condition.lineNumber); + super(condition.resourceName, condition.lineNumber); this.condition = condition; } } @@ -131,8 +131,8 @@ static final class ElseIfTokenNode extends IfOrElseIfTokenNode { } static final class ElseTokenNode extends TokenNode { - ElseTokenNode(int lineNumber) { - super(lineNumber); + ElseTokenNode(String resourceName, int lineNumber) { + super(resourceName, lineNumber); } @Override String name() { @@ -145,7 +145,7 @@ static final class ForEachTokenNode extends TokenNode { final ExpressionNode collection; ForEachTokenNode(String var, ExpressionNode collection) { - super(collection.lineNumber); + super(collection.resourceName, collection.lineNumber); this.var = var; this.collection = collection; } @@ -155,12 +155,26 @@ static final class ForEachTokenNode extends TokenNode { } } + static final class NestedTokenNode extends TokenNode { + final ImmutableList nodes; + + NestedTokenNode(String resourceName, ImmutableList nodes) { + super(resourceName, 1); + this.nodes = nodes; + } + + @Override String name() { + return "#parse(\"" + resourceName + "\")"; + } + } + static final class MacroDefinitionTokenNode extends TokenNode { final String name; final ImmutableList parameterNames; - MacroDefinitionTokenNode(int lineNumber, String name, List parameterNames) { - super(lineNumber); + MacroDefinitionTokenNode( + String resourceName, int lineNumber, String name, List parameterNames) { + super(resourceName, lineNumber); this.name = name; this.parameterNames = ImmutableList.copyOf(parameterNames); } diff --git a/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java b/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java index ee9c8eda08..0b13f9a439 100644 --- a/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java +++ b/value/src/test/java/com/google/auto/value/processor/escapevelocity/TemplateTest.java @@ -14,24 +14,32 @@ package com.google.auto.value.processor.escapevelocity; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.truth.Expect; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; +import java.util.function.Supplier; +import org.apache.commons.collections.ExtendedProperties; import org.apache.velocity.VelocityContext; +import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeInstance; import org.apache.velocity.runtime.log.NullLogChute; import org.apache.velocity.runtime.parser.node.SimpleNode; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -52,18 +60,21 @@ public class TemplateTest { private RuntimeInstance velocityRuntimeInstance; @Before - public void setUp() { - velocityRuntimeInstance = new RuntimeInstance(); + public void initVelocityRuntimeInstance() { + velocityRuntimeInstance = newVelocityRuntimeInstance(); + velocityRuntimeInstance.init(); + } + + private RuntimeInstance newVelocityRuntimeInstance() { + RuntimeInstance runtimeInstance = new RuntimeInstance(); // Ensure that $undefinedvar will produce an exception rather than outputting $undefinedvar. - velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); - velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, - new NullLogChute()); + runtimeInstance.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); // Disable any logging that Velocity might otherwise see fit to do. - velocityRuntimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute()); - - velocityRuntimeInstance.init(); + runtimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, new NullLogChute()); + runtimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute()); + return runtimeInstance; } private void compare(String template) { @@ -71,7 +82,7 @@ private void compare(String template) { } private void compare(String template, Map vars) { - compare(template, Suppliers.ofInstance(vars)); + compare(template, () -> vars); } /** @@ -89,8 +100,9 @@ private void compare(String template, Supplier> varsSup try { escapeVelocityRendered = Template.parseFrom(new StringReader(template)).evaluate(escapeVelocityVars); - } catch (IOException e) { - throw new AssertionError(e); + } catch (Exception e) { + throw new AssertionError( + "EscapeVelocity failed, but Velocity succeeded and returned: " + velocityRendered, e); } String failure = "from velocity: <" + velocityRendered + ">\n" + "from escape velocity: <" + escapeVelocityRendered + ">\n"; @@ -98,7 +110,7 @@ private void compare(String template, Supplier> varsSup } private String velocityRender(String template, Map vars) { - VelocityContext velocityContext = new VelocityContext(new TreeMap(vars)); + VelocityContext velocityContext = new VelocityContext(new TreeMap<>(vars)); StringWriter writer = new StringWriter(); SimpleNode parsedTemplate; try { @@ -128,6 +140,24 @@ public void comment() { compare("line 1 ##\n line 2"); } + @Test + public void ignoreHashIfNotDirectiveOrComment() { + compare("# if is not a directive because of the space"); + compare("#"); + compare("# "); + compare("${foo}#${bar}", ImmutableMap.of("foo", "xxx", "bar", "yyy")); + } + + @Test + public void blockQuote() { + compare("#[[]]#"); + compare("x#[[]]#y"); + compare("#[[$notAReference #notADirective]]#"); + compare("#[[ [[ ]] ]# ]]#"); + compare("#[ foo"); + compare("x\n #[[foo\nbar\nbaz]]#y"); + } + @Test public void substituteNoBraces() { compare(" $x ", ImmutableMap.of("x", 1729)); @@ -319,7 +349,7 @@ public void relationPrecedence() { /** * Tests the surprising definition of equality mentioned in - * {@link ExpressionNode.EqualsExpressionNode}. + * {@link ExpressionNode.BinaryExpressionNode}. */ @Test public void funkyEquals() { @@ -650,4 +680,101 @@ public void macroArgumentMismatch() throws IOException { Template.parseFrom(new StringReader(template)); } + /** + * A Velocity ResourceLoader that looks resources up in a map. This allows us to test directives + * that read "resources", for example {@code #parse}, without needing to make separate files to + * put them in. + */ + private static final class MapResourceLoader extends ResourceLoader { + private final ImmutableMap resourceMap; + + MapResourceLoader(ImmutableMap resourceMap) { + this.resourceMap = resourceMap; + } + + @Override + public void init(ExtendedProperties configuration) { + } + + @Override + public InputStream getResourceStream(String source) { + String resource = resourceMap.get(source); + if (resource == null) { + throw new ResourceNotFoundException(source); + } + return new ByteArrayInputStream(resource.getBytes(StandardCharsets.ISO_8859_1)); + } + + @Override + public boolean isSourceModified(Resource resource) { + return false; + } + + @Override + public long getLastModified(Resource resource) { + return 0; + } + }; + + private String renderWithResources( + String templateResourceName, + ImmutableMap resourceMap, + ImmutableMap vars) { + MapResourceLoader mapResourceLoader = new MapResourceLoader(resourceMap); + RuntimeInstance runtimeInstance = newVelocityRuntimeInstance(); + runtimeInstance.setProperty("resource.loader", "map"); + runtimeInstance.setProperty("map.resource.loader.instance", mapResourceLoader); + runtimeInstance.init(); + org.apache.velocity.Template velocityTemplate = + runtimeInstance.getTemplate(templateResourceName); + StringWriter velocityWriter = new StringWriter(); + VelocityContext velocityContext = new VelocityContext(new TreeMap<>(vars)); + velocityTemplate.merge(velocityContext, velocityWriter); + return velocityWriter.toString(); + } + + @Test + public void parseDirective() throws IOException { + // If outer.vm does #parse("nested.vm"), then we should be able to #set a variable in + // nested.vm and use it in outer.vm, and we should be able to define a #macro in nested.vm + // and call it in outer.vm. + ImmutableMap resources = ImmutableMap.of( + "outer.vm", + "first line\n" + + "#parse (\"nested.vm\")\n" + + "<#decorate (\"left\" \"right\")>\n" + + "$baz skidoo\n" + + "last line\n", + "nested.vm", + "nested template first line\n" + + "[#if ($foo == $bar) equal #else not equal #end]\n" + + "#macro (decorate $a $b) < $a | $b > #end\n" + + "#set ($baz = 23)\n" + + "nested template last line\n"); + + ImmutableMap vars = ImmutableMap.of("foo", "foovalue", "bar", "barvalue"); + + String velocityResult = renderWithResources("outer.vm", resources, vars); + + Template.ResourceOpener resourceOpener = resourceName -> { + String resource = resources.get(resourceName); + if (resource == null) { + throw new FileNotFoundException(resourceName); + } + return new StringReader(resource); + }; + Template template = Template.parseFrom("outer.vm", resourceOpener); + + String result = template.evaluate(vars); + assertThat(result).isEqualTo(velocityResult); + + ImmutableMap badVars = ImmutableMap.of("foo", "foovalue"); + try { + template.evaluate(badVars); + fail(); + } catch (EvaluationException e) { + assertThat(e).hasMessageThat().isEqualTo( + "In expression on line 2 of nested.vm: Undefined reference $bar"); + } + } }