| /* |
| * Copyright (C) 2018 Google, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.google.escapevelocity; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.fail; |
| |
| 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; |
| import org.junit.rules.ExpectedException; |
| import org.junit.rules.TestName; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * @author emcmanus@google.com (Éamonn McManus) |
| */ |
| @RunWith(JUnit4.class) |
| public class TemplateTest { |
| @Rule public TestName testName = new TestName(); |
| @Rule public Expect expect = Expect.create(); |
| @Rule public ExpectedException thrown = ExpectedException.none(); |
| |
| private RuntimeInstance velocityRuntimeInstance; |
| |
| @Before |
| 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. |
| runtimeInstance.setProperty(RuntimeConstants.RUNTIME_REFERENCES_STRICT, "true"); |
| |
| // Disable any logging that Velocity might otherwise see fit to do. |
| runtimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, new NullLogChute()); |
| runtimeInstance.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new NullLogChute()); |
| return runtimeInstance; |
| } |
| |
| private void compare(String template) { |
| compare(template, ImmutableMap.<String, Object>of()); |
| } |
| |
| private void compare(String template, Map<String, ?> vars) { |
| compare(template, () -> vars); |
| } |
| |
| /** |
| * Checks that the given template and the given variables produce identical results with |
| * Velocity and EscapeVelocity. This uses a {@code Supplier} to define the variables to cover |
| * test cases that involve modifying the values of the variables. Otherwise the run using |
| * Velocity would change those values so that the run using EscapeVelocity would not be starting |
| * from the same point. |
| */ |
| private void compare(String template, Supplier<? extends Map<String, ?>> varsSupplier) { |
| Map<String, ?> velocityVars = varsSupplier.get(); |
| String velocityRendered = velocityRender(template, velocityVars); |
| Map<String, ?> escapeVelocityVars = varsSupplier.get(); |
| String escapeVelocityRendered; |
| try { |
| escapeVelocityRendered = |
| Template.parseFrom(new StringReader(template)).evaluate(escapeVelocityVars); |
| } catch (Exception e) { |
| throw new AssertionError( |
| "EscapeVelocity failed, but Velocity succeeded and returned: <" + velocityRendered + ">", |
| e); |
| } |
| String failure = "from Velocity: <" + velocityRendered + ">\n" |
| + "from EscapeVelocity: <" + escapeVelocityRendered + ">\n"; |
| expect.withMessage(failure).that(escapeVelocityRendered).isEqualTo(velocityRendered); |
| } |
| |
| private String velocityRender(String template, Map<String, ?> vars) { |
| VelocityContext velocityContext = new VelocityContext(new TreeMap<>(vars)); |
| StringWriter writer = new StringWriter(); |
| SimpleNode parsedTemplate; |
| try { |
| parsedTemplate = velocityRuntimeInstance.parse( |
| new StringReader(template), testName.getMethodName()); |
| } catch (org.apache.velocity.runtime.parser.ParseException e) { |
| throw new AssertionError(e); |
| } |
| boolean rendered = velocityRuntimeInstance.render( |
| velocityContext, writer, parsedTemplate.getTemplateName(), parsedTemplate); |
| assertThat(rendered).isTrue(); |
| return writer.toString(); |
| } |
| |
| @Test |
| public void empty() { |
| compare(""); |
| } |
| |
| @Test |
| public void literalOnly() { |
| compare("In the reign of James the Second \n It was generally reckoned\n"); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| public void substitutePropertyNoBraces() { |
| compare("=$t.name=", ImmutableMap.of("t", Thread.currentThread())); |
| } |
| |
| @Test |
| public void substitutePropertyWithBraces() { |
| compare("=${t.name}=", ImmutableMap.of("t", Thread.currentThread())); |
| } |
| |
| @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())); |
| } |
| |
| @Test |
| public void substituteMethodNoArgs() { |
| compare("<$c.size()>", ImmutableMap.of("c", ImmutableMap.of())); |
| } |
| |
| @Test |
| public void substituteMethodOneArg() { |
| compare("<$list.get(0)>", ImmutableMap.of("list", ImmutableList.of("foo"))); |
| } |
| |
| @Test |
| public void substituteMethodTwoArgs() { |
| compare("\n$s.indexOf(\"bar\", 2)\n", ImmutableMap.of("s", "barbarbar")); |
| } |
| |
| @Test |
| public void substituteMethodNoSynthetic() { |
| // If we aren't careful, we'll see both the inherited `Set<K> keySet()` from Map |
| // and the overridden `ImmutableSet<K> keySet()` in ImmutableMap. |
| compare("$map.keySet()", ImmutableMap.of("map", ImmutableMap.of("foo", "bar"))); |
| } |
| |
| @Test |
| public void substituteIndexNoBraces() { |
| compare("<$map[\"x\"]>", ImmutableMap.of("map", ImmutableMap.of("x", "y"))); |
| } |
| |
| @Test |
| public void substituteIndexWithBraces() { |
| compare("<${map[\"x\"]}>", ImmutableMap.of("map", ImmutableMap.of("x", "y"))); |
| } |
| |
| @Test |
| public void substituteIndexThenProperty() { |
| compare("<$map[2].name>", ImmutableMap.of("map", ImmutableMap.of(2, getClass()))); |
| } |
| |
| @Test |
| public void variableNameCantStartWithNonAscii() { |
| compare("<$Éamonn>", ImmutableMap.<String, Object>of()); |
| } |
| |
| @Test |
| public void variableNamesAreAscii() { |
| compare("<$Pádraig>", ImmutableMap.of("P", "(P)")); |
| } |
| |
| @Test |
| public void variableNameCharacters() { |
| 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 + "]"; |
| } |
| } |
| |
| @Test |
| public void substituteExoticIndex() { |
| // Any class with a get(X) method can be used with $x[i] |
| compare("<$x[\"foo\"]>", ImmutableMap.of("x", new Indexable())); |
| } |
| |
| @Test |
| public void simpleSet() { |
| compare("$x#set ($x = 17)#set ($y = 23) ($x, $y)", ImmutableMap.of("x", 1)); |
| } |
| |
| @Test |
| public void newlineAfterSet() { |
| compare("foo #set ($x = 17)\nbar", ImmutableMap.<String, Object>of()); |
| } |
| |
| @Test |
| public void newlineInSet() { |
| compare("foo #set ($x\n = 17)\nbar $x", ImmutableMap.<String, Object>of()); |
| } |
| |
| @Test |
| public void expressions() { |
| compare("#set ($x = 1 + 1) $x"); |
| compare("#set ($x = 1 + 2 * 3) $x"); |
| compare("#set ($x = (1 + 1 == 2)) $x"); |
| compare("#set ($x = (1 + 1 != 2)) $x"); |
| compare("#set ($x = 22 - 7) $x"); |
| compare("#set ($x = 22 / 7) $x"); |
| compare("#set ($x = 22 % 7) $x"); |
| } |
| |
| @Test |
| public void associativity() { |
| compare("#set ($x = 3 - 2 - 1) $x"); |
| compare("#set ($x = 16 / 4 / 4) $x"); |
| } |
| |
| @Test |
| public void precedence() { |
| compare("#set ($x = 1 + 2 + 3 * 4 * 5 + 6) $x"); |
| compare("#set($x=1+2+3*4*5+6)$x"); |
| compare("#set ($x = 1 + 2 * 3 == 3 * 2 + 1) $x"); |
| } |
| |
| @Test |
| public void and() { |
| compare("#set ($x = false && false) $x"); |
| compare("#set ($x = false && true) $x"); |
| compare("#set ($x = true && false) $x"); |
| compare("#set ($x = true && true) $x"); |
| } |
| |
| @Test |
| public void or() { |
| compare("#set ($x = false || false) $x"); |
| compare("#set ($x = false || true) $x"); |
| compare("#set ($x = true || false) $x"); |
| compare("#set ($x = true || true) $x"); |
| } |
| |
| @Test |
| public void not() { |
| compare("#set ($x = !true) $x"); |
| compare("#set ($x = !false) $x"); |
| } |
| |
| @Test |
| public void truthValues() { |
| compare("#set ($x = $true && true) $x", ImmutableMap.of("true", true)); |
| compare("#set ($x = $false && true) $x", ImmutableMap.of("false", false)); |
| compare("#set ($x = $emptyCollection && true) $x", |
| ImmutableMap.of("emptyCollection", ImmutableList.of())); |
| compare("#set ($x = $emptyString && true) $x", ImmutableMap.of("emptyString", "")); |
| } |
| |
| @Test |
| public void numbers() { |
| compare("#set ($x = 0) $x"); |
| compare("#set ($x = -1) $x"); |
| compare("#set ($x = " + Integer.MAX_VALUE + ") $x"); |
| compare("#set ($x = " + Integer.MIN_VALUE + ") $x"); |
| } |
| |
| private static final String[] RELATIONS = {"==", "!=", "<", ">", "<=", ">="}; |
| |
| @Test |
| public void intRelations() { |
| int[] numbers = {-1, 0, 1, 17}; |
| for (String relation : RELATIONS) { |
| for (int a : numbers) { |
| for (int b : numbers) { |
| compare("#set ($x = $a " + relation + " $b) $x", |
| ImmutableMap.<String, Object>of("a", a, "b", b)); |
| } |
| } |
| } |
| } |
| |
| @Test |
| public void relationPrecedence() { |
| compare("#set ($x = 1 < 2 == 2 < 1) $x"); |
| compare("#set ($x = 2 < 1 == 2 < 1) $x"); |
| } |
| |
| /** |
| * Tests the surprising definition of equality mentioned in |
| * {@link ExpressionNode.BinaryExpressionNode}. |
| */ |
| @Test |
| public void funkyEquals() { |
| compare("#set ($t = (123 == \"123\")) $t"); |
| compare("#set ($f = (123 == \"1234\")) $f"); |
| compare("#set ($x = ($sb1 == $sb2)) $x", ImmutableMap.of( |
| "sb1", (Object) new StringBuilder("123"), |
| "sb2", (Object) new StringBuilder("123"))); |
| } |
| |
| @Test |
| public void ifTrueNoElse() { |
| compare("x#if (true)y #end z"); |
| compare("x#if (true)y #end z"); |
| compare("x#if (true)y #end\nz"); |
| compare("x#if (true)y #end\n z"); |
| compare("x#if (true) y #end\nz"); |
| compare("x#if (true)\ny #end\nz"); |
| compare("x#if (true) y #end\nz"); |
| compare("$x #if (true) y #end $x ", ImmutableMap.of("x", "!")); |
| } |
| |
| @Test |
| public void ifFalseNoElse() { |
| compare("x#if (false)y #end z"); |
| compare("x#if (false)y #end\nz"); |
| compare("x#if (false)y #end\n z"); |
| compare("x#if (false) y #end\nz"); |
| compare("x#if (false)\ny #end\nz"); |
| compare("x#if (false) y #end\nz"); |
| } |
| |
| @Test |
| public void ifTrueWithElse() { |
| compare("x#if (true) a #else b #end z"); |
| } |
| |
| @Test |
| public void ifFalseWithElse() { |
| compare("x#if (false) a #else b #end z"); |
| } |
| |
| @Test |
| public void ifTrueWithElseIf() { |
| compare("x#if (true) a #elseif (true) b #else c #end z"); |
| } |
| |
| @Test |
| public void ifFalseWithElseIfTrue() { |
| compare("x#if (false) a #elseif (true) b #else c #end z"); |
| } |
| |
| @Test |
| public void ifFalseWithElseIfFalse() { |
| compare("x#if (false) a #elseif (false) b #else c #end z"); |
| } |
| |
| @Test |
| public void ifBraces() { |
| compare("x#{if}(false)a#{elseif}(false)b #{else}c#{end}z"); |
| } |
| @Test |
| public void ifUndefined() { |
| compare("#if ($undefined) really? #else indeed #end"); |
| } |
| |
| @Test |
| public void forEach() { |
| compare("x#foreach ($x in $c) <$x> #end y", |
| ImmutableMap.of("c", ImmutableList.of())); |
| compare("x#foreach ($x in $c) <$x> #end y", |
| ImmutableMap.of("c", ImmutableList.of("foo", "bar", "baz"))); |
| compare("x#foreach ($x in $c) <$x> #end y", |
| ImmutableMap.of("c", new String[] {"foo", "bar", "baz"})); |
| compare("x#foreach ($x in $c) <$x> #end y", |
| ImmutableMap.of("c", ImmutableMap.of("foo", "bar", "baz", "buh"))); |
| } |
| |
| @Test |
| public void forEachHasNext() { |
| compare("x#foreach ($x in $c) <$x#if ($foreach.hasNext), #end> #end y", |
| ImmutableMap.of("c", ImmutableList.of())); |
| compare("x#foreach ($x in $c) <$x#if ($foreach.hasNext), #end> #end y", |
| ImmutableMap.of("c", ImmutableList.of("foo", "bar", "baz"))); |
| } |
| |
| @Test |
| public void nestedForEach() { |
| String template = |
| "$x #foreach ($x in $listOfLists)\n" |
| + " #foreach ($y in $x)\n" |
| + " ($y)#if ($foreach.hasNext), #end\n" |
| + " #end#if ($foreach.hasNext); #end\n" |
| + "#end\n" |
| + "$x\n"; |
| Object listOfLists = ImmutableList.of( |
| ImmutableList.of("foo", "bar", "baz"), ImmutableList.of("fred", "jim", "sheila")); |
| compare(template, ImmutableMap.of("x", 23, "listOfLists", listOfLists)); |
| } |
| |
| @Test |
| public void forEachScope() { |
| String template = |
| "$x #foreach ($x in $list)\n" |
| + "[$x]\n" |
| + "#set ($x = \"bar\")\n" |
| + "#set ($othervar = \"baz\")\n" |
| + "#end\n" |
| + "$x $othervar"; |
| compare( |
| template, ImmutableMap.of("x", "foo", "list", ImmutableList.of("blim", "blam", "blum"))); |
| } |
| |
| @Test |
| public void setSpacing() { |
| // The spacing in the output from #set is eccentric. |
| compare("x#set ($x = 0)x"); |
| compare("x #set ($x = 0)x"); |
| compare("x #set ($x = 0) x"); |
| compare("$x#set ($x = 0)x", ImmutableMap.of("x", "!")); |
| |
| // Velocity WTF: the #set eats the space after $x and other references, so the output is <!x>. |
| compare("$x #set ($x = 0)x", ImmutableMap.of("x", "!")); |
| compare("$x.length() #set ($x = 0)x", ImmutableMap.of("x", "!")); |
| compare("$x.empty #set ($x = 0)x", ImmutableMap.of("x", "!")); |
| compare("$x[0] #set ($x = 0)x", ImmutableMap.of("x", ImmutableList.of("!"))); |
| |
| compare("x#set ($x = 0)\n $x!"); |
| |
| 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 |
| public void simpleMacro() { |
| String template = |
| "xyz\n" |
| + "#macro (m)\n" |
| + "hello world\n" |
| + "#end\n" |
| + "#m() abc #m()\n"; |
| compare(template); |
| } |
| |
| @Test |
| public void macroWithArgs() { |
| String template = |
| "$x\n" |
| + "#macro (m $x $y)\n" |
| + " #if ($x < $y) less #else greater #end\n" |
| + "#end\n" |
| + "#m(17 23) #m(23 17) #m(17 17)\n" |
| + "$x"; |
| compare(template, ImmutableMap.of("x", "tiddly")); |
| } |
| |
| /** |
| * Tests defining a macro inside a conditional. This proves that macros are not evaluated in the |
| * main control flow, but rather are extracted at parse time. It also tests what happens if there |
| * is more than one definition of the same macro. (It is not apparent from the test, but it is the |
| * first definition that is retained.) |
| */ |
| @Test |
| public void conditionalMacroDefinition() { |
| String templateFalse = |
| "#if (false)\n" |
| + " #macro (m) foo #end\n" |
| + "#else\n" |
| + " #macro (m) bar #end\n" |
| + "#end\n" |
| + "#m()\n"; |
| compare(templateFalse); |
| |
| String templateTrue = |
| "#if (true)\n" |
| + " #macro (m) foo #end\n" |
| + "#else\n" |
| + " #macro (m) bar #end\n" |
| + "#end\n" |
| + "#m()\n"; |
| compare(templateTrue); |
| } |
| |
| /** |
| * Tests referencing a macro before it is defined. Since macros are extracted at parse time but |
| * references are only used at evaluation time, this works. |
| */ |
| @Test |
| public void forwardMacroReference() { |
| String template = |
| "#m(17)\n" |
| + "#macro (m $x)\n" |
| + " !$x!\n" |
| + "#end"; |
| compare(template); |
| } |
| |
| @Test |
| public void macroArgsSeparatedBySpaces() { |
| String template = |
| "#macro (sum $x $y $z)\n" |
| + " #set ($sum = $x + $y + $z)\n" |
| + " $sum\n" |
| + "#end\n" |
| + "#sum ($list[0] $list.get(1) 5)\n"; |
| compare(template, ImmutableMap.of("list", ImmutableList.of(3, 4))); |
| } |
| |
| @Test |
| public void macroArgsSeparatedByCommas() { |
| String template = |
| "#macro (sum $x $y $z)\n" |
| + " #set ($sum = $x + $y + $z)\n" |
| + " $sum\n" |
| + "#end\n" |
| + "#sum ($list[0],$list.get(1),5)\n"; |
| compare(template, ImmutableMap.of("list", ImmutableList.of(3, 4))); |
| } |
| |
| // The following tests are based on http://wiki.apache.org/velocity/MacroEvaluationStrategy. |
| // They verify some of the trickier details of Velocity's call-by-name semantics. |
| |
| @Test |
| public void callBySharing() { |
| // The example on the web page is wrong because $map.put('x', 'a') evaluates to null, which |
| // Velocity rejects as a render error. We fix this by ensuring that the returned previous value |
| // is not null. |
| // Here, the value of $y should not be affected by #set($x = "a"), even though the name passed |
| // to $x is $y. |
| String template = |
| "#macro(callBySharing $x $map)\n" |
| + " #set($x = \"a\")\n" |
| + " $map.put(\"x\", \"a\")\n" |
| + "#end\n" |
| + "#callBySharing($y $map)\n" |
| + "y is $y\n" |
| + "map[x] is $map[\"x\"]\n"; |
| Supplier<Map<String, Object>> makeMap = new Supplier<Map<String, Object>>() { |
| @Override public Map<String, Object> get() { |
| return ImmutableMap.<String, Object>of( |
| "y", "y", |
| "map", new HashMap<String, Object>(ImmutableMap.of("x", (Object) "foo"))); |
| } |
| }; |
| compare(template, makeMap); |
| } |
| |
| @Test |
| public void callByMacro() { |
| // Since #callByMacro1 never references its argument, $x.add("t") is never evaluated during it. |
| // Since #callByMacro2 references its argument twice, $x.add("t") is evaluated twice during it. |
| String template = |
| "#macro(callByMacro1 $p)\n" |
| + " not using\n" |
| + "#end\n" |
| + "#macro(callByMacro2 $p)\n" |
| + " using: $p\n" |
| + " using again: $p\n" |
| + " using again: $p\n" |
| + "#end\n" |
| + "#callByMacro1($x.add(\"t\"))\n" |
| + "x = $x\n" |
| + "#callByMacro2($x.add(\"t\"))\n" |
| + "x = $x\n"; |
| Supplier<Map<String, Object>> makeMap = new Supplier<Map<String, Object>>() { |
| @Override public Map<String, Object> get() { |
| return ImmutableMap.<String, Object>of("x", new ArrayList<Object>()); |
| } |
| }; |
| compare(template, makeMap); |
| } |
| |
| @Test |
| public void callByValue() { |
| // The assignments to the macro parameters $a and $b cause those parameters to be shadowed, |
| // so the output is: a b becomes b a. |
| String template = |
| "#macro(callByValueSwap $a $b)\n" |
| + " $a $b becomes\n" |
| + " #set($tmp = $a)\n" |
| + " #set($a = $b)\n" |
| + " #set($b = $tmp)\n" |
| + " $a $b\n" |
| + "#end" |
| + "#callByValueSwap(\"a\", \"b\")"; |
| compare(template); |
| } |
| |
| // First "Call by macro expansion example" doesn't apply as long as we don't have map literals. |
| |
| @Test |
| public void nameCaptureSwap() { |
| // Here, the arguments $a and $b are variables rather than literals, which means that their |
| // values change when we set those variables. #set($tmp = $a) changes the meaning of $b since |
| // $b is the name $tmp. So #set($a = $b) shadows parameter $a with the value of $tmp, which we |
| // have just set to "a". Then #set($b = $tmp) shadows parameter $b also with the value of $tmp. |
| // The end result is: a b becomes a a. |
| String template = |
| "#macro(nameCaptureSwap $a $b)\n" |
| + " $a $b becomes\n" |
| + " #set($tmp = $a)\n" |
| + " #set($a = $b)\n" |
| + " #set($b = $tmp)\n" |
| + " $a $b\n" |
| + "#end\n" |
| + "#set($x = \"a\")\n" |
| + "#set($tmp = \"b\")\n" |
| + "#nameCaptureSwap($x $tmp)"; |
| compare(template); |
| } |
| |
| @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); |
| thrown.expectMessage("#oops is neither a standard directive nor a macro that has been defined"); |
| Template.parseFrom(new StringReader(template)); |
| } |
| |
| @Test |
| public void macroArgumentMismatch() throws IOException { |
| String template = |
| "#macro (twoArgs $a $b) $a $b #end\n" |
| + "#twoArgs(23)\n"; |
| thrown.expect(ParseException.class); |
| thrown.expectMessage("Wrong number of arguments to #twoArgs: expected 2, got 1"); |
| 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"); |
| } |
| } |
| } |