aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉamonn McManus <eamonn@mcmanus.net>2018-04-20 17:14:33 -0700
committerÉamonn McManus <eamonn@mcmanus.net>2018-04-20 17:14:33 -0700
commit89ea4ddf7f3d0005543fb740481eeb0dc5cddac9 (patch)
tree9961d4cc4e5047bc1275746c952b9247e542384d
parentd68715b3e62b4f1b8ebd7fa051004934a2ba913d (diff)
downloadescapevelocity-89ea4ddf7f3d0005543fb740481eeb0dc5cddac9.tar.gz
Update to latest Google version of EscapeVelocity.
-rw-r--r--pom.xml13
-rw-r--r--src/main/java/com/google/escapevelocity/ConstantExpressionNode.java19
-rw-r--r--src/main/java/com/google/escapevelocity/DirectiveNode.java23
-rw-r--r--src/main/java/com/google/escapevelocity/EvaluationContext.java31
-rw-r--r--src/main/java/com/google/escapevelocity/EvaluationException.java19
-rw-r--r--src/main/java/com/google/escapevelocity/ExpressionNode.java19
-rw-r--r--src/main/java/com/google/escapevelocity/Macro.java32
-rw-r--r--src/main/java/com/google/escapevelocity/Node.java21
-rw-r--r--src/main/java/com/google/escapevelocity/ParseException.java19
-rw-r--r--src/main/java/com/google/escapevelocity/Parser.java214
-rw-r--r--src/main/java/com/google/escapevelocity/ReferenceNode.java57
-rw-r--r--src/main/java/com/google/escapevelocity/Reparser.java40
-rw-r--r--src/main/java/com/google/escapevelocity/Template.java34
-rw-r--r--src/main/java/com/google/escapevelocity/TokenNode.java21
-rw-r--r--src/test/java/com/google/escapevelocity/ReferenceNodeTest.java2
-rw-r--r--src/test/java/com/google/escapevelocity/TemplateTest.java226
16 files changed, 638 insertions, 152 deletions
diff --git a/pom.xml b/pom.xml
index a58896d..b87abb1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,21 +26,18 @@
A reimplementation of a subset of the Apache Velocity templating system.
</description>
- <!-- TODO(emcmanus)
<scm>
- <url>http://github.com/google/auto</url>
- <connection>scm:git:git://github.com/google/auto.git</connection>
- <developerConnection>scm:git:ssh://git@github.com/google/auto.git</developerConnection>
+ <url>http://github.com/google/escapevelocity</url>
+ <connection>scm:git:git://github.com/google/escapevelocity.git</connection>
+ <developerConnection>scm:git:ssh://git@github.com/google/escapevelocity.git</developerConnection>
<tag>HEAD</tag>
</scm>
- -->
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.5-jre</version>
- <scope>test</scope>
</dependency>
<!-- test dependencies -->
<dependency>
@@ -76,8 +73,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
- <source>1.7</source>
- <target>1.7</target>
+ <source>1.8</source>
+ <target>1.8</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
diff --git a/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java
index a4dfe17..982a4a9 100644
--- a/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java
+++ b/src/main/java/com/google/escapevelocity/ConstantExpressionNode.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
/**
diff --git a/src/main/java/com/google/escapevelocity/DirectiveNode.java b/src/main/java/com/google/escapevelocity/DirectiveNode.java
index cf33f55..db6a3a3 100644
--- a/src/main/java/com/google/escapevelocity/DirectiveNode.java
+++ b/src/main/java/com/google/escapevelocity/DirectiveNode.java
@@ -11,8 +11,29 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
@@ -186,7 +207,7 @@ abstract class DirectiveNode extends Node {
@Override
Object evaluate(EvaluationContext context) {
- assert macro != null : "Macro should have been linked: #" + name;
+ Verify.verifyNotNull(macro, "Macro #%s should have been linked", name);
return macro.evaluate(context, thunks);
}
}
diff --git a/src/main/java/com/google/escapevelocity/EvaluationContext.java b/src/main/java/com/google/escapevelocity/EvaluationContext.java
index 43b7868..4c4b27d 100644
--- a/src/main/java/com/google/escapevelocity/EvaluationContext.java
+++ b/src/main/java/com/google/escapevelocity/EvaluationContext.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
import java.util.Map;
@@ -60,17 +79,9 @@ interface EvaluationContext {
Runnable undo;
if (vars.containsKey(var)) {
final Object oldValue = vars.get(var);
- undo = new Runnable() {
- @Override public void run() {
- vars.put(var, oldValue);
- }
- };
+ undo = () -> vars.put(var, oldValue);
} else {
- undo = new Runnable() {
- @Override public void run() {
- vars.remove(var);
- }
- };
+ undo = () -> vars.remove(var);
}
vars.put(var, value);
return undo;
diff --git a/src/main/java/com/google/escapevelocity/EvaluationException.java b/src/main/java/com/google/escapevelocity/EvaluationException.java
index 67aa15c..c64318c 100644
--- a/src/main/java/com/google/escapevelocity/EvaluationException.java
+++ b/src/main/java/com/google/escapevelocity/EvaluationException.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
/**
diff --git a/src/main/java/com/google/escapevelocity/ExpressionNode.java b/src/main/java/com/google/escapevelocity/ExpressionNode.java
index 4ee29c5..e666ed1 100644
--- a/src/main/java/com/google/escapevelocity/ExpressionNode.java
+++ b/src/main/java/com/google/escapevelocity/ExpressionNode.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
import com.google.escapevelocity.Parser.Operator;
diff --git a/src/main/java/com/google/escapevelocity/Macro.java b/src/main/java/com/google/escapevelocity/Macro.java
index 151ded2..fbb1764 100644
--- a/src/main/java/com/google/escapevelocity/Macro.java
+++ b/src/main/java/com/google/escapevelocity/Macro.java
@@ -11,8 +11,29 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -50,7 +71,7 @@ class Macro {
Object evaluate(EvaluationContext context, List<Node> thunks) {
try {
- assert thunks.size() == parameterNames.size() : "Argument mistmatch for " + name;
+ Verify.verify(thunks.size() == parameterNames.size(), "Argument mistmatch for %s", name);
Map<String, Node> parameterThunks = new LinkedHashMap<>();
for (int i = 0; i < parameterNames.size(); i++) {
parameterThunks.put(parameterNames.get(i), thunks.get(i));
@@ -121,12 +142,9 @@ class Macro {
} else {
parameterThunks.remove(var);
final Runnable originalUndo = originalEvaluationContext.setVar(var, value);
- return new Runnable() {
- @Override
- public void run() {
- originalUndo.run();
- parameterThunks.put(var, thunk);
- }
+ return () -> {
+ originalUndo.run();
+ parameterThunks.put(var, thunk);
};
}
}
diff --git a/src/main/java/com/google/escapevelocity/Node.java b/src/main/java/com/google/escapevelocity/Node.java
index eca745f..d11c95d 100644
--- a/src/main/java/com/google/escapevelocity/Node.java
+++ b/src/main/java/com/google/escapevelocity/Node.java
@@ -11,8 +11,29 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
+import com.google.common.collect.ImmutableList;
+
/**
* A node in the parse tree.
*
diff --git a/src/main/java/com/google/escapevelocity/ParseException.java b/src/main/java/com/google/escapevelocity/ParseException.java
index 241a192..9d4a39c 100644
--- a/src/main/java/com/google/escapevelocity/ParseException.java
+++ b/src/main/java/com/google/escapevelocity/ParseException.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
/**
diff --git a/src/main/java/com/google/escapevelocity/Parser.java b/src/main/java/com/google/escapevelocity/Parser.java
index 9982be3..0beaf18 100644
--- a/src/main/java/com/google/escapevelocity/Parser.java
+++ b/src/main/java/com/google/escapevelocity/Parser.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
import com.google.escapevelocity.DirectiveNode.SetNode;
@@ -29,14 +48,16 @@ import com.google.escapevelocity.TokenNode.ForEachTokenNode;
import com.google.escapevelocity.TokenNode.IfTokenNode;
import com.google.escapevelocity.TokenNode.MacroDefinitionTokenNode;
import com.google.escapevelocity.TokenNode.NestedTokenNode;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Verify;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Chars;
+import com.google.common.primitives.Ints;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
/**
* A parser that reads input from the given {@link Reader} and parses it to produce a
@@ -53,12 +74,20 @@ class Parser {
/**
* The invariant of this parser is that {@code c} is always the next character of interest.
- * This means that we never have to "unget" a character by reading too far. For example, after
- * we parse an integer, {@code c} will be the first character after the integer, which is exactly
- * the state we will be in when there are no more digits.
+ * This means that we almost never have to "unget" a character by reading too far. For example,
+ * after we parse an integer, {@code c} will be the first character after the integer, which is
+ * exactly the state we will be in when there are no more digits.
+ *
+ * <p>Sometimes we need to read two characters ahead, and in that case we use {@link #pushback}.
*/
private int c;
+ /**
+ * A single character of pushback. If this is not negative, the {@link #next()} method will
+ * return it instead of reading a character.
+ */
+ private int pushback = -1;
+
Parser(Reader reader, String resourceName, Template.ResourceOpener resourceOpener)
throws IOException {
this.reader = new LineNumberReader(reader);
@@ -127,11 +156,29 @@ class Parser {
*/
private void next() throws IOException {
if (c != EOF) {
- c = reader.read();
+ if (pushback < 0) {
+ c = reader.read();
+ } else {
+ c = pushback;
+ pushback = -1;
+ }
}
}
/**
+ * Saves the current character {@code c} to be read again, and sets {@code c} to the given
+ * {@code c1}. Suppose the text contains {@code xy} and we have just read {@code y}.
+ * So {@code c == 'y'}. Now if we execute {@code pushback('x')}, we will have
+ * {@code c == 'x'} and the next call to {@link #next()} will set {@code c == 'y'}. Subsequent
+ * calls to {@code next()} will continue reading from {@link #reader}. So the pushback
+ * essentially puts us back in the state we were in before we read {@code y}.
+ */
+ private void pushback(int c1) {
+ pushback = c;
+ c = c1;
+ }
+
+ /**
* If {@code c} is a space character, keeps reading until {@code c} is a non-space character or
* there are no more characters.
*/
@@ -174,17 +221,24 @@ class Parser {
private Node parseNode() throws IOException {
if (c == '#') {
next();
- if (c == '#') {
- return parseComment();
- } 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(), "#");
+ switch (c) {
+ case '#':
+ return parseLineComment();
+ case '*':
+ return parseBlockComment();
+ case '[':
+ return parseHashSquare();
+ case '{':
+ return parseDirective();
+ default:
+ if (isAsciiLetter(c)) {
+ return parseDirective();
+ } else {
+ // For consistency with Velocity, we treat # not followed by a letter or one of the
+ // characters above as a plain character, and we treat #$foo as a literal # followed by
+ // the reference $foo.
+ return parsePlainText('#');
+ }
}
}
if (c == EOF) {
@@ -200,13 +254,15 @@ class Parser {
assert c == '[';
next();
if (c != '[') {
- return new ConstantExpressionNode(resourceName, lineNumber(), "#[");
+ return parsePlainText(new StringBuilder("#["));
}
+ int startLine = lineNumber();
next();
StringBuilder sb = new StringBuilder();
while (true) {
if (c == EOF) {
- throw parseException("Unterminated #[[ - did not see matching ]]#");
+ throw new ParseException(
+ "Unterminated #[[ - did not see matching ]]#", resourceName, startLine);
}
if (c == '#') {
// This might be the last character of ]]# or it might just be a random #.
@@ -458,10 +514,10 @@ class Parser {
}
/**
- * Parses and discards a comment, which is {@code ##} followed by any number of characters up to
- * and including the next newline.
+ * Parses and discards a line comment, which is {@code ##} followed by any number of characters
+ * up to and including the next newline.
*/
- private Node parseComment() throws IOException {
+ private Node parseLineComment() throws IOException {
int lineNumber = lineNumber();
while (c != '\n' && c != EOF) {
next();
@@ -471,6 +527,27 @@ class Parser {
}
/**
+ * Parses and discards a block comment, which is {@code #*} followed by everything up to and
+ * including the next {@code *#}.
+ */
+ private Node parseBlockComment() throws IOException {
+ assert c == '*';
+ int startLine = lineNumber();
+ int lastC = '\0';
+ next();
+ while (!(lastC == '*' && c == '#')) {
+ if (c == EOF) {
+ throw new ParseException(
+ "Unterminated #* - did not see matching *#", resourceName, startLine);
+ }
+ lastC = c;
+ next();
+ }
+ next();
+ return new CommentTokenNode(resourceName, startLine);
+ }
+
+ /**
* Parses plain text, which is text that contains neither {@code $} nor {@code #}. The given
* {@code firstChar} is the first character of the plain text, and {@link #c} is the second
* (if the plain text is more than one character).
@@ -478,7 +555,10 @@ class Parser {
private Node parsePlainText(int firstChar) throws IOException {
StringBuilder sb = new StringBuilder();
sb.appendCodePoint(firstChar);
+ return parsePlainText(sb);
+ }
+ private Node parsePlainText(StringBuilder sb) throws IOException {
literal:
while (true) {
switch (c) {
@@ -508,7 +588,27 @@ class Parser {
*
* <p>On entry to this method, {@link #c} is the character immediately after the {@code $}.
*/
- private ReferenceNode parseReference() throws IOException {
+ private Node parseReference() throws IOException {
+ if (c == '{') {
+ next();
+ if (!isAsciiLetter(c)) {
+ return parsePlainText(new StringBuilder("${"));
+ }
+ ReferenceNode node = parseReferenceNoBrace();
+ expect('}');
+ return node;
+ } else {
+ return parseReferenceNoBrace();
+ }
+ }
+
+ /**
+ * Same as {@link #parseReference()}, except it really must be a reference. A {@code $} in
+ * normal text doesn't start a reference if it is not followed by an identifier. But in an
+ * expression, for example in {@code #if ($x == 23)}, {@code $} must be followed by an
+ * identifier.
+ */
+ private ReferenceNode parseRequiredReference() throws IOException {
if (c == '{') {
next();
ReferenceNode node = parseReferenceNoBrace();
@@ -568,6 +668,11 @@ class Parser {
private ReferenceNode parseReferenceMember(ReferenceNode lhs) throws IOException {
assert c == '.';
next();
+ if (!isAsciiLetter(c)) {
+ // We've seen something like `$foo.!`, so it turns out it's not a member after all.
+ pushback('.');
+ return lhs;
+ }
String id = parseId("Member");
ReferenceNode reference;
if (c == '(') {
@@ -670,19 +775,15 @@ class Parser {
* Maps a code point to the operators that begin with that code point. For example, maps
* {@code <} to {@code LESS} and {@code LESS_OR_EQUAL}.
*/
- private static final Map<Integer, List<Operator>> CODE_POINT_TO_OPERATORS;
+ private static final ImmutableListMultimap<Integer, Operator> CODE_POINT_TO_OPERATORS;
static {
- Map<Integer, List<Operator>> map = new HashMap<>();
+ ImmutableListMultimap.Builder<Integer, Operator> builder = ImmutableListMultimap.builder();
for (Operator operator : Operator.values()) {
if (operator != Operator.STOP) {
- Integer key = operator.symbol.codePointAt(0);
- if (!map.containsKey(key)) {
- map.put(key, new ArrayList<Operator>());
- }
- map.get(key).add(operator);
+ builder.put((int) operator.symbol.charAt(0), operator);
}
}
- CODE_POINT_TO_OPERATORS = Collections.unmodifiableMap(map);
+ CODE_POINT_TO_OPERATORS = builder.build();
}
/**
@@ -753,17 +854,17 @@ class Parser {
*/
private void nextOperator() throws IOException {
skipSpace();
- List<Operator> possibleOperators = CODE_POINT_TO_OPERATORS.get(c);
- if (possibleOperators == null) {
+ ImmutableList<Operator> possibleOperators = CODE_POINT_TO_OPERATORS.get(c);
+ if (possibleOperators.isEmpty()) {
currentOperator = Operator.STOP;
return;
}
- int firstChar = c;
+ char firstChar = Chars.checkedCast(c);
next();
Operator operator = null;
for (Operator possibleOperator : possibleOperators) {
if (possibleOperator.symbol.length() == 1) {
- assert operator == null;
+ Verify.verify(operator == null);
operator = possibleOperator;
} else if (possibleOperator.symbol.charAt(1) == c) {
next();
@@ -771,7 +872,8 @@ class Parser {
}
}
if (operator == null) {
- throw parseException("Expected " + possibleOperators.get(0) + ", not just " + firstChar);
+ throw parseException(
+ "Expected " + Iterables.getOnlyElement(possibleOperators) + ", not just " + firstChar);
}
currentOperator = operator;
}
@@ -818,7 +920,7 @@ class Parser {
ExpressionNode node;
if (c == '$') {
next();
- node = parseReference();
+ node = parseRequiredReference();
} else if (c == '"') {
node = parseStringLiteral();
} else if (c == '-') {
@@ -869,10 +971,8 @@ class Parser {
sb.appendCodePoint(c);
next();
}
- int value;
- try {
- value = Integer.parseInt(sb.toString());
- } catch (NumberFormatException e) {
+ Integer value = Ints.tryParse(sb.toString());
+ if (value == null) {
throw parseException("Invalid integer: " + sb);
}
return new ConstantExpressionNode(resourceName, lineNumber(), value);
@@ -896,29 +996,31 @@ class Parser {
return new ConstantExpressionNode(resourceName, lineNumber(), value);
}
- private static final ImmutableAsciiSet ASCII_LETTER =
- ImmutableAsciiSet.ofRange('A', 'Z')
- .union(ImmutableAsciiSet.ofRange('a', 'z'));
+ private static final CharMatcher ASCII_LETTER =
+ CharMatcher.inRange('A', 'Z')
+ .or(CharMatcher.inRange('a', 'z'))
+ .precomputed();
- private static final ImmutableAsciiSet ASCII_DIGIT =
- ImmutableAsciiSet.ofRange('0', '9');
+ private static final CharMatcher ASCII_DIGIT =
+ CharMatcher.inRange('0', '9')
+ .precomputed();
- private static final ImmutableAsciiSet ID_CHAR =
+ private static final CharMatcher ID_CHAR =
ASCII_LETTER
- .union(ASCII_DIGIT)
- .union(ImmutableAsciiSet.of('-'))
- .union(ImmutableAsciiSet.of('_'));
+ .or(ASCII_DIGIT)
+ .or(CharMatcher.anyOf("-_"))
+ .precomputed();
private static boolean isAsciiLetter(int c) {
- return ASCII_LETTER.contains(c);
+ return (char) c == c && ASCII_LETTER.matches((char) c);
}
private static boolean isAsciiDigit(int c) {
- return ASCII_DIGIT.contains(c);
+ return (char) c == c && ASCII_DIGIT.matches((char) c);
}
private static boolean isIdChar(int c) {
- return ID_CHAR.contains(c);
+ return (char) c == c && ID_CHAR.matches((char) c);
}
/**
diff --git a/src/main/java/com/google/escapevelocity/ReferenceNode.java b/src/main/java/com/google/escapevelocity/ReferenceNode.java
index 865d02a..6302561 100644
--- a/src/main/java/com/google/escapevelocity/ReferenceNode.java
+++ b/src/main/java/com/google/escapevelocity/ReferenceNode.java
@@ -11,14 +11,35 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Primitives;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -227,13 +248,11 @@ abstract class ReferenceNode extends ExpressionNode {
throw evaluationException(
"Parameters for method " + id + " have wrong types: " + argValues);
case 1:
- return invokeMethod(compatibleMethods.get(0), lhsValue, argValues);
+ return invokeMethod(Iterables.getOnlyElement(compatibleMethods), lhsValue, argValues);
default:
- StringBuilder error = new StringBuilder("Ambiguous method invocation, could be one of:");
- for (Method method : compatibleMethods) {
- error.append("\n ").append(method);
- }
- throw evaluationException(error.toString());
+ throw evaluationException(
+ "Ambiguous method invocation, could be one of:\n "
+ + Joiner.on("\n ").join(compatibleMethods));
}
}
@@ -258,29 +277,11 @@ abstract class ReferenceNode extends ExpressionNode {
return true;
}
- private static final Map<Class<?>, Class<?>> BOXED_TO_UNBOXED;
- static {
- Map<Class<?>, Class<?>> map = new HashMap<>();
- map.put(Byte.class, byte.class);
- map.put(Short.class, short.class);
- map.put(Integer.class, int.class);
- map.put(Long.class, long.class);
- map.put(Float.class, float.class);
- map.put(Double.class, double.class);
- map.put(Character.class, char.class);
- map.put(Boolean.class, boolean.class);
- BOXED_TO_UNBOXED = Collections.unmodifiableMap(map);
- }
-
private static boolean primitiveIsCompatible(Class<?> primitive, Object value) {
- if (value == null) {
- return false;
- }
- Class<?> unboxed = BOXED_TO_UNBOXED.get(value.getClass());
- if (unboxed == null) {
+ if (value == null || !Primitives.isWrapperType(value.getClass())) {
return false;
}
- return primitiveTypeIsAssignmentCompatible(primitive, unboxed);
+ return primitiveTypeIsAssignmentCompatible(primitive, Primitives.unwrap(value.getClass()));
}
private static final ImmutableList<Class<?>> NUMERICAL_PRIMITIVES = ImmutableList.<Class<?>>of(
diff --git a/src/main/java/com/google/escapevelocity/Reparser.java b/src/main/java/com/google/escapevelocity/Reparser.java
index 6235bc4..8e86180 100644
--- a/src/main/java/com/google/escapevelocity/Reparser.java
+++ b/src/main/java/com/google/escapevelocity/Reparser.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
import static com.google.escapevelocity.Node.emptyNode;
@@ -29,6 +48,10 @@ import com.google.escapevelocity.TokenNode.IfOrElseIfTokenNode;
import com.google.escapevelocity.TokenNode.IfTokenNode;
import com.google.escapevelocity.TokenNode.MacroDefinitionTokenNode;
import com.google.escapevelocity.TokenNode.NestedTokenNode;
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
@@ -67,7 +90,7 @@ class Reparser {
private final Map<String, Macro> macros;
Reparser(ImmutableList<Node> nodes) {
- this(nodes, new TreeMap<String, Macro>());
+ this(nodes, new TreeMap<>());
}
private Reparser(ImmutableList<Node> nodes, Map<String, Macro> macros) {
@@ -94,7 +117,7 @@ class Reparser {
* ({@code $x} or {@code $x.foo} etc); a macro definition; or another {@code #set}.
*/
private static ImmutableList<Node> removeSpaceBeforeSet(ImmutableList<Node> nodes) {
- assert nodes.get(nodes.size() - 1) instanceof EofNode : nodes.get(nodes.size() - 1);
+ assert Iterables.getLast(nodes) instanceof EofNode;
// Since the last node is EofNode, the i + 1 and i + 2 accesses below are safe.
ImmutableList.Builder<Node> newNodes = ImmutableList.builder();
for (int i = 0; i < nodes.size(); i++) {
@@ -120,18 +143,7 @@ class Reparser {
private static boolean isWhitespaceLiteral(Node node) {
if (node instanceof ConstantExpressionNode) {
Object constant = node.evaluate(null);
- if (constant instanceof String) {
- String s = (String) constant;
- int i = 0;
- while (i < s.length()) {
- int c = s.codePointAt(i);
- if (!Character.isWhitespace(c)) {
- return false;
- }
- i += Character.charCount(c);
- }
- return true;
- }
+ return constant instanceof String && CharMatcher.whitespace().matchesAllOf((String) constant);
}
return false;
}
diff --git a/src/main/java/com/google/escapevelocity/Template.java b/src/main/java/com/google/escapevelocity/Template.java
index 646c42b..3613dbe 100644
--- a/src/main/java/com/google/escapevelocity/Template.java
+++ b/src/main/java/com/google/escapevelocity/Template.java
@@ -11,6 +11,25 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
import com.google.escapevelocity.EvaluationContext.PlainEvaluationContext;
@@ -60,15 +79,12 @@ public class Template {
* Parses a VTL template from the given {@code Reader}. The given Reader will be closed on
* return from this method.
*/
- public static Template parseFrom(final Reader reader) throws IOException {
- ResourceOpener resourceOpener = new ResourceOpener() {
- @Override
- public Reader openResource(String resourceName) throws IOException {
- if (resourceName == null) {
- return reader;
- } else {
- throw new IOException("No ResourceOpener has been configured to read " + resourceName);
- }
+ public static Template parseFrom(Reader reader) throws IOException {
+ ResourceOpener resourceOpener = resourceName -> {
+ if (resourceName == null) {
+ return reader;
+ } else {
+ throw new IOException("No ResourceOpener has been configured to read " + resourceName);
}
};
try {
diff --git a/src/main/java/com/google/escapevelocity/TokenNode.java b/src/main/java/com/google/escapevelocity/TokenNode.java
index 1e92109..a7226fc 100644
--- a/src/main/java/com/google/escapevelocity/TokenNode.java
+++ b/src/main/java/com/google/escapevelocity/TokenNode.java
@@ -11,8 +11,28 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 com.google.escapevelocity;
+import com.google.common.collect.ImmutableList;
import java.util.List;
/**
@@ -164,4 +184,3 @@ abstract class TokenNode extends Node {
}
}
}
-
diff --git a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java
index 660c237..c71eb1a 100644
--- a/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java
+++ b/src/test/java/com/google/escapevelocity/ReferenceNodeTest.java
@@ -15,11 +15,11 @@ package com.google.escapevelocity;
import static com.google.common.truth.Truth.assertThat;
+import com.google.escapevelocity.ReferenceNode.MethodReferenceNode;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Primitives;
import com.google.common.truth.Expect;
-import com.google.escapevelocity.ReferenceNode.MethodReferenceNode;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
diff --git a/src/test/java/com/google/escapevelocity/TemplateTest.java b/src/test/java/com/google/escapevelocity/TemplateTest.java
index bd769d6..0437734 100644
--- a/src/test/java/com/google/escapevelocity/TemplateTest.java
+++ b/src/test/java/com/google/escapevelocity/TemplateTest.java
@@ -14,24 +14,32 @@
package com.google.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 @@ public class TemplateTest {
}
private void compare(String template, Map<String, ?> vars) {
- compare(template, Suppliers.ofInstance(vars));
+ compare(template, () -> vars);
}
/**
@@ -89,11 +100,13 @@ public class TemplateTest {
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";
+ String failure = "from Velocity: <" + velocityRendered + ">\n"
+ + "from EscapeVelocity: <" + escapeVelocityRendered + ">\n";
expect.withMessage(failure).that(escapeVelocityRendered).isEqualTo(velocityRendered);
}
@@ -124,17 +137,58 @@ public class TemplateTest {
}
@Test
- public void comment() {
+ public void lineComment() {
compare("line 1 ##\n line 2");
}
@Test
+ public void blockComment() {
+ compare("line 1 #* blah\n line 2 * #\n line 3 *# \n line 4");
+ compare("foo #*# bar *# baz");
+ compare("foo #* one *# #* two *# #* three *#");
+ compare("foo #** bar *# #* baz **#");
+ }
+
+ @Test
+ public void ignoreHashIfNotDirectiveOrComment() {
+ compare("# if is not a directive because of the space");
+ compare("#<foo>");
+ compare("# <foo>");
+ 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));
compare(" ! $x ! ", ImmutableMap.of("x", 1729));
}
@Test
+ public void dollarWithoutId() {
+ compare(" $? ");
+ compare(" $$ ");
+ compare(" $. ");
+ compare(" $[ ");
+ }
+
+ @Test
+ public void doubleDollar() {
+ // The first $ is plain text and the second one starts a reference.
+ compare(" $$foo ", ImmutableMap.of("foo", true));
+ compare(" $${foo} ", ImmutableMap.of("foo", true));
+ }
+
+ @Test
public void substituteWithBraces() {
compare("a${x}\nb", ImmutableMap.of("x", "1729"));
}
@@ -150,6 +204,18 @@ public class TemplateTest {
}
@Test
+ public void substituteNotPropertyId() {
+ compare("$foo.!", ImmutableMap.of("foo", false));
+ }
+
+ /* TODO(emcmanus): make this work.
+ @Test
+ public void substituteNotPropertyId() {
+ compare("$foo.!", ImmutableMap.of("foo", false));
+ }
+ */
+
+ @Test
public void substituteNestedProperty() {
compare("\n$t.name.empty\n", ImmutableMap.of("t", Thread.currentThread()));
}
@@ -206,6 +272,10 @@ public class TemplateTest {
compare("<AZaz-foo_bar23>", ImmutableMap.of("AZaz-foo_bar23", "(P)"));
}
+ /**
+ * A public class with a public {@code get} method that has one argument. That means instances can
+ * be used like {@code $indexable["foo"]}.
+ */
public static class Indexable {
public String get(String y) {
return "[" + y + "]";
@@ -319,7 +389,7 @@ public class TemplateTest {
/**
* Tests the surprising definition of equality mentioned in
- * {@link ExpressionNode.EqualsExpressionNode}.
+ * {@link ExpressionNode.BinaryExpressionNode}.
*/
@Test
public void funkyEquals() {
@@ -452,6 +522,7 @@ public class TemplateTest {
compare("x #set($x = 0) #set($x = 0) #set($x = 0) y");
compare("x ## comment\n #set($x = 0) y");
+ compare("x #* comment *# #set($x = 0) y");
}
@Test
@@ -633,6 +704,14 @@ public class TemplateTest {
}
@Test
+ public void badBraceReference() throws IOException {
+ String template = "line 1\nline 2\nbar${foo.!}baz";
+ thrown.expect(ParseException.class);
+ thrown.expectMessage("Expected }, on line 3, at text starting: .!}baz");
+ Template.parseFrom(new StringReader(template));
+ }
+
+ @Test
public void undefinedMacro() throws IOException {
String template = "#oops()";
thrown.expect(ParseException.class);
@@ -650,4 +729,117 @@ public class TemplateTest {
Template.parseFrom(new StringReader(template));
}
+ @Test
+ public void unclosedBlockQuote() throws IOException {
+ String template = "foo\nbar #[[\nblah\nblah";
+ thrown.expect(ParseException.class);
+ thrown.expectMessage("Unterminated #[[ - did not see matching ]]#, on line 2");
+ Template.parseFrom(new StringReader(template));
+ }
+
+ @Test
+ public void unclosedBlockComment() throws IOException {
+ String template = "foo\nbar #*\nblah\nblah";
+ thrown.expect(ParseException.class);
+ thrown.expectMessage("Unterminated #* - did not see matching *#, on line 2");
+ 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<String, String> resourceMap;
+
+ MapResourceLoader(ImmutableMap<String, String> 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<String, String> resourceMap,
+ ImmutableMap<String, String> 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<String, String> 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<String, String> 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<String, String> 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");
+ }
+ }
}