diff --git a/pom.xml b/pom.xml index 2e884ad0a..7fd7d699c 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 0.8.14 true - 0.96 + 0.95 0.89 0.89 0.80 diff --git a/src/main/java/org/apache/commons/jexl3/JxltEngine.java b/src/main/java/org/apache/commons/jexl3/JxltEngine.java index ce684dd75..c91153dcf 100644 --- a/src/main/java/org/apache/commons/jexl3/JxltEngine.java +++ b/src/main/java/org/apache/commons/jexl3/JxltEngine.java @@ -288,7 +288,7 @@ public interface Template { /** * Gets this script pragmas. * - * @return the (non null, may be empty) pragmas map + * @return the (non-null, possibly empty) pragmas map * @since 3.1 */ Map getPragmas(); @@ -310,6 +310,22 @@ public interface Template { * @return the prepared version of the template */ Template prepare(JexlContext context); + + /** + * Prepares this template by expanding any contained deferred TemplateExpression with optional arguments. + *

This binds arguments to template parameters for immediate expressions when the template also + * uses deferred/nested expressions.

+ * + * @param context the context to prepare against + * @param args the arguments to bind (optional) + * @return the prepared version of the template + * @since 3.6.2 + */ + default Template prepare(JexlContext context, Object... args) { + throw new UnsupportedOperationException( + "This template implementation does not support prepare with arguments. " + + "Override this method to provide support."); + } } /** diff --git a/src/main/java/org/apache/commons/jexl3/internal/Engine.java b/src/main/java/org/apache/commons/jexl3/internal/Engine.java index 661ad76b8..f5bb14779 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -1042,7 +1042,8 @@ protected JexlContext.ThreadLocal putThreadLocal(final JexlContext.ThreadLocal t } @Override - public void setClassLoader(final ClassLoader loader) { + public void setClassLoader(final ClassLoader classLoader) { + final ClassLoader loader = classLoader == null? JexlUberspect.class.getClassLoader() : classLoader; uberspect.setClassLoader(loader); if (functions != null) { final Iterable names = new ArrayList<>(functions.keySet()); diff --git a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java index 101020011..db0189f64 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1498,19 +1498,23 @@ protected Object visit(final ASTJexlScript script, final Object data) { } block = new LexicalFrame(frame, block).defineArgs(); try { - final int numChildren = script.jjtGetNumChildren(); - Object result = null; - for (int i = 0; i < numChildren; i++) { - final JexlNode child = script.jjtGetChild(i); - result = child.jjtAccept(this, data); - cancelCheck(child); - } - return result; + return runScript(script, data); } finally { block = block.pop(); } } + protected final Object runScript(final ASTJexlScript script, final Object data) { + final int numChildren = script.jjtGetNumChildren(); + Object result = null; + for (int i = 0; i < numChildren; i++) { + final JexlNode child = script.jjtGetChild(i); + result = child.jjtAccept(this, data); + cancelCheck(child); + } + return result; + } + @Override protected Object visit(final ASTLENode node, final Object data) { final Object left = node.jjtGetChild(0).jjtAccept(this, data); diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java index 75b66e11a..28b11a088 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java @@ -45,12 +45,10 @@ * @since 3.0 */ public final class TemplateEngine extends JxltEngine { - /** * Abstract the source fragments, verbatim or immediate typed text blocks. */ static final class Block { - /** The type of block: verbatim or directive. */ private final BlockType type; @@ -116,7 +114,6 @@ void toString(final StringBuilder strb, final String prefix) { * The enum capturing the difference between verbatim and code source fragments. */ enum BlockType { - /** Block is to be output "as is" but may be a unified expression. */ VERBATIM, @@ -126,7 +123,6 @@ enum BlockType { /** A composite unified expression: "... ${...} ... #{...} ...". */ final class CompositeExpression extends TemplateExpression { - /** Bit encoded (deferred count > 0) bit 1, (immediate count > 0) bit 0. */ private final int meta; @@ -227,16 +223,11 @@ protected TemplateExpression prepare(final Interpreter interpreter) { /** A constant unified expression. */ final class ConstantExpression extends TemplateExpression { - /** The constant held by this unified expression. */ private final Object value; /** * Creates a constant unified expression. - *

- * If the wrapped constant is a string, it is treated - * as a JEXL strings with respect to escaping. - *

* * @param val the constant value * @param source the source TemplateExpression if any @@ -270,16 +261,14 @@ ExpressionType getType() { /** A deferred unified expression: #{jexl}. */ final class DeferredExpression extends JexlBasedExpression { - /** * Creates a deferred unified expression. * * @param expr the unified expression as a string * @param node the unified expression as an AST - * @param source the source unified expression if any */ - DeferredExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) { - super(expr, node, source); + DeferredExpression(final CharSequence expr, final JexlNode node) { + super(expr, node, null); } @Override @@ -308,7 +297,6 @@ protected TemplateExpression prepare(final Interpreter interpreter) { * Keeps count of sub-expressions by type. */ static final class ExpressionBuilder { - /** Per TemplateExpression type counters. */ private final int[] counts; @@ -390,7 +378,6 @@ StringBuilder toString(final StringBuilder error) { * @see ExpressionBuilder */ enum ExpressionType { - /** Constant TemplateExpression, count index 0. */ CONSTANT(0), @@ -429,7 +416,6 @@ int getIndex() { /** An immediate unified expression: ${jexl}. */ final class ImmediateExpression extends JexlBasedExpression { - /** * Creates an immediate unified expression. * @@ -456,7 +442,6 @@ protected TemplateExpression prepare(final Interpreter interpreter) { /** The base for JEXL based unified expressions. */ abstract class JexlBasedExpression extends TemplateExpression { - /** The JEXL string for this unified expression. */ protected final CharSequence expr; @@ -519,17 +504,17 @@ protected JexlOptions options(final JexlContext context) { * Note that the deferred syntax is JEXL's. */ final class NestedExpression extends JexlBasedExpression { - + private final Scope scope; /** * Creates a nested unified expression. * * @param expr the unified expression as a string * @param node the unified expression as an AST - * @param source the source unified expression if any */ - NestedExpression(final CharSequence expr, final JexlNode node, final TemplateExpression source) { - super(expr, node, source); - if (this.source != this) { + NestedExpression(final CharSequence expr, final JexlNode node, final Scope sc) { + super(expr, node, null); + this.scope = sc; + if (source != this) { throw new IllegalArgumentException("Nested TemplateExpression cannot have a source"); } } @@ -558,7 +543,7 @@ public boolean isImmediate() { @Override protected TemplateExpression prepare(final Interpreter interpreter) { final String value = interpreter.interpret(node).toString(); - final JexlNode dnode = jexl.jxltParse(node.jexlInfo(), noscript, value, null); + final JexlNode dnode = jexl.jxltParse(node.jexlInfo(), noscript, value, scope); return new ImmediateExpression(value, dnode, this); } } @@ -589,7 +574,6 @@ private enum ParseState { * The abstract base class for all unified expressions, immediate '${...}' and deferred '#{...}'. */ abstract class TemplateExpression implements Expression { - /** The source of this template expression(see {@link TemplateEngine.TemplateExpression#prepare}). */ protected final TemplateExpression source; @@ -722,14 +706,15 @@ public final TemplateExpression prepare(final JexlContext context) { * * @param frame the frame storing parameters and local variables * @param context the context storing global variables - * @param opts flags and properties that can alter the evaluation behavior. + * @param options flags and properties that can alter the evaluation behavior. * @return the expression value * @throws JexlException if expression preparation fails */ - protected final TemplateExpression prepare(final JexlContext context, final Frame frame, final JexlOptions opts) { + protected final TemplateExpression prepare(final JexlContext context, final Frame frame, final JexlOptions options) { try { - final JexlOptions interOptions = opts != null ? opts : jexl.evalOptions(context); - final Interpreter interpreter = jexl.createInterpreter(context, frame, interOptions); + final TemplateInterpreter.Arguments args = new TemplateInterpreter.Arguments(jexl).context(context) + .options(options != null ? options : options(context)).frame(frame); + final Interpreter interpreter = jexl.createTemplateInterpreter(args); return prepare(interpreter); } catch (final JexlException xjexl) { final JexlException xuel = createException(xjexl.getInfo(), "prepare", this, xjexl); @@ -1068,12 +1053,13 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final // materialize the immediate expr final String src = escapeString(strb); final JexlInfo srcInfo = info.at(lineno, column); - final TemplateExpression iexpr = new ImmediateExpression(src, jexl.jxltParse(srcInfo, noscript, src, scope), null); + final TemplateExpression iexpr = new ImmediateExpression(src, + jexl.jxltParse(srcInfo, noscript, src, scope), null); builder.add(iexpr); strb.delete(0, Integer.MAX_VALUE); state = ParseState.CONST; } - } else { + } else if (!isIgnorable(c)) { if (c == '{') { immediate1 += 1; } @@ -1082,58 +1068,59 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final } break; case DEFERRED1: // #{... - // skip inner strings (for '}') + // skip inner strings - for '}' - // nested immediate in deferred; need to balance count of '{' & '}' // closing '}' switch (c) { - case '"': - case '\'': - strb.append(c); - column = StringParser.readString(strb, expr, column + 1, c); - continue; - case '{': - if (expr.charAt(column - 1) == immediateChar) { - inner1 += 1; - strb.deleteCharAt(strb.length() - 1); - nested = true; - } else { - deferred1 += 1; + case '"': + case '\'': strb.append(c); - } - continue; - case '}': - // balance nested immediate - if (deferred1 > 0) { - deferred1 -= 1; - strb.append(c); - } else if (inner1 > 0) { - inner1 -= 1; - } else { - // materialize the nested/deferred expr - final String src = escapeString(strb); - final JexlInfo srcInfo = info.at(lineno, column); - TemplateExpression dexpr; - if (nested) { - dexpr = new NestedExpression( + column = StringParser.readString(strb, expr, column + 1, c); + continue; + case '{': + if (expr.charAt(column - 1) == immediateChar) { + inner1 += 1; + strb.deleteCharAt(strb.length() - 1); + nested = true; + } else { + deferred1 += 1; + strb.append(c); + } + continue; + case '}': + // balance nested immediate + if (deferred1 > 0) { + deferred1 -= 1; + strb.append(c); + } else if (inner1 > 0) { + inner1 -= 1; + } else { + // materialize the nested/deferred expr + final String src = escapeString(strb); + final JexlInfo srcInfo = info.at(lineno, column); + TemplateExpression dexpr; + if (nested) { + dexpr = new NestedExpression( escapeString(expr.substring(inested, column + 1)), jexl.jxltParse(srcInfo, noscript, src, scope), - null); - } else { - dexpr = new DeferredExpression( - src, - jexl.jxltParse(srcInfo, noscript, src, scope), - null); + scope); + } else { + dexpr = new DeferredExpression( + src, + jexl.jxltParse(srcInfo, noscript, src, scope)); + } + builder.add(dexpr); + strb.delete(0, Integer.MAX_VALUE); + nested = false; + state = ParseState.CONST; } - builder.add(dexpr); - strb.delete(0, Integer.MAX_VALUE); - nested = false; - state = ParseState.CONST; - } - break; - default: - // do buildup expr - column = append(strb, expr, column, c); - break; + break; + default: + if (!isIgnorable(c)) { + // do buildup expr + column = append(strb, expr, column, c); + } + break; } break; case ESCAPE: @@ -1184,6 +1171,25 @@ private String escapeString(final CharSequence str) { return StringParser.escapeString(str, (char) 0); } + /** + * Determines whether the given character is considered ignorable whitespace in + * template expressions. + *

+ * The characters newline ({@code '\n'}), carriage return ({@code '\r'}), tab + * ({@code '\t'}) and form feed ({@code '\f'}) are treated as ignorable within + * template expressions and are skipped by the parser. These characters are + * not ignored between the expression prefix ({@code '$'} or + * {@code '#'}) and the opening brace {@code '{'}; in that position they + * influence parsing instead of being discarded. + *

+ * + * @param c the character to test + * @return {@code true} if the character is ignorable, {@code false} otherwise + */ + private static boolean isIgnorable(char c) { + return c == '\n' || c == '\r' || c == '\t' || c == '\f'; + } + /** * Reads lines of a template grouping them by typed blocks. * diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java index 953d44c24..074e90c88 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java @@ -39,13 +39,11 @@ *

public for introspection purpose.

*/ public class TemplateInterpreter extends Interpreter { - /** * Helper ctor. *

Stores the different properties required to create a Template interpreter. */ public static class Arguments { - /** The engine. */ Engine jexl; @@ -218,11 +216,11 @@ public void print(final int e) { * @param composite the composite expression */ private void printComposite(final TemplateEngine.CompositeExpression composite) { - final TemplateEngine.TemplateExpression[] cexprs = composite.exprs; + final TemplateEngine.TemplateExpression[] composites = composite.exprs; Object value; - for (final TemplateExpression cexpr : cexprs) { - value = cexpr.evaluate(this); - doPrint(cexpr.getInfo(), value); + for (final TemplateExpression expr : composites) { + value = expr.evaluate(this); + doPrint(expr.getInfo(), value); } } @@ -299,14 +297,22 @@ protected Interpreter createInterpreter(final JexlContext context, final Frame l }; } // otherwise... - final int numChildren = script.jjtGetNumChildren(); - Object result = null; - for (int i = 0; i < numChildren; i++) { - final JexlNode child = script.jjtGetChild(i); - result = child.jjtAccept(this, data); - cancelCheck(child); + final Object[] stack = saveStack(); + try { + return runScript(script, data); + } finally { + restoreStack(stack); + } + } + + private Object[] saveStack() { + return frame != null && frame.stack != null? frame.stack.clone() : null; + } + + private void restoreStack(Object[] stack) { + if (stack != null) { + System.arraycopy(stack, 0, frame.stack, 0, stack.length); } - return result; } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java index da1cb42be..c03952679 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java @@ -319,19 +319,24 @@ public Set> getVariables() { @Override public TemplateScript prepare(final JexlContext context) { + return prepare(context, (Object[]) null); + } + + @Override + public TemplateScript prepare(final JexlContext context, final Object... args) { final Engine jexl = jxlt.getEngine(); final JexlOptions options = jexl.evalOptions(script, context); - final Frame frame = script.createFrame((Object[]) null); + final Frame frame = script.createFrame(args); final TemplateInterpreter.Arguments targs = new TemplateInterpreter .Arguments(jxlt.getEngine()) .context(context) .options(options) .frame(frame); final Interpreter interpreter = jexl.createTemplateInterpreter(targs); - final TemplateExpression[] immediates = new TemplateExpression[exprs.length]; + final TemplateExpression[] prepared = new TemplateExpression[exprs.length]; for (int e = 0; e < exprs.length; ++e) { try { - immediates[e] = exprs[e].prepare(interpreter); + prepared[e] = exprs[e].prepare(interpreter); } catch (final JexlException xjexl) { final JexlException xuel = TemplateEngine.createException(xjexl.getInfo(), "prepare", exprs[e], xjexl); if (jexl.isSilent()) { @@ -343,7 +348,7 @@ public TemplateScript prepare(final JexlContext context) { throw xuel; } } - return new TemplateScript(jxlt, prefix, source, script, immediates); + return new TemplateScript(jxlt, prefix, source, script, prepared); } @Override diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java index 8e962b596..ec2c98b19 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Introspector.java @@ -24,10 +24,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.commons.jexl3.introspection.JexlPermissions; +import org.apache.commons.jexl3.introspection.JexlUberspect; import org.apache.commons.logging.Log; /** @@ -150,7 +152,8 @@ public Introspector(final Log log, final ClassLoader loader, final JexlPermissio */ public Class getClassByName(final String className) { try { - final Class clazz = Class.forName(className, false, loader); + final ClassLoader classLoader = Objects.requireNonNull(loader, "class loader should not be null"); + final Class clazz = Class.forName(className, false, classLoader); return permissions.allow(clazz)? clazz : null; } catch (final ClassNotFoundException xignore) { return null; @@ -383,7 +386,7 @@ public Method[] getMethods(final Class c, final String methodName) { * @param classLoader the class loader; if null, use this instance class loader */ public void setLoader(final ClassLoader classLoader) { - final ClassLoader current = classLoader == null ? getClass().getClassLoader() : classLoader; + final ClassLoader current = classLoader == null ? JexlUberspect.class.getClassLoader() : classLoader; lock.writeLock().lock(); try { final ClassLoader previous = loader; diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java index 64cb56b60..610120e43 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Uberspect.java @@ -460,16 +460,17 @@ public int getVersion() { } @Override - public void setClassLoader(final ClassLoader nloader) { + public void setClassLoader(final ClassLoader loader) { + final ClassLoader classLoader = loader == null ? JexlUberspect.class.getClassLoader() : loader; synchronized (this) { Introspector intro = ref.get(); if (intro != null) { - intro.setLoader(nloader); + intro.setLoader(classLoader); } else { - intro = new Introspector(logger, nloader, permissions); + intro = new Introspector(logger, classLoader, permissions); ref = new SoftReference<>(intro); } - loader = new SoftReference<>(intro.getLoader()); + this.loader = new SoftReference<>(intro.getLoader()); operatorMap.clear(); version.incrementAndGet(); } diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java index 76b666b74..384028743 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTJexlScript.java @@ -83,7 +83,7 @@ public int getArgCount() { * @return the captured variable names */ public String[] getCapturedVariables() { - return scope != null ? scope.getCapturedVariables() : null; + return scope != null ? scope.getCapturedVariables() : new String[0]; } /** @@ -99,7 +99,7 @@ public JexlFeatures getFeatures() { * @return the local variable names */ public String[] getLocalVariables() { - return scope != null ? scope.getLocalVariables() : null; + return scope != null ? scope.getLocalVariables() : new String[0]; } /** @@ -108,7 +108,7 @@ public String[] getLocalVariables() { * @return the parameter names */ public String[] getParameters() { - return scope != null ? scope.getParameters() : null; + return scope != null ? scope.getParameters() : new String[0]; } /** diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 2deb4ab91..4ebdce4d0 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -30,6 +30,7 @@ import java.io.Closeable; import java.io.File; +import java.io.StringWriter; import java.lang.reflect.Method; import java.math.BigDecimal; import java.util.Arrays; @@ -980,5 +981,91 @@ void test451() { assertThrows(JexlException.Property.class, () -> jexl451.createScript("o.class.classLoader", "o").execute(null, new Object())); } + + @Test + void testIssue455a() { + final JexlEngine jexl = new JexlBuilder().create(); + String code = "name -> `${name +\n\t\f\r name}`"; + JexlScript script = jexl.createScript(code); + Object o = script.execute(null, "Hello"); + String ctl = "HelloHello"; + Assertions.assertEquals(ctl, o); + } + + @Test + void testIssue455b() { + final JexlEngine jexl = new JexlBuilder().create(); + String code = "name -> `${name}\n${name}`;"; + JexlScript script = jexl.createScript(code); + Object o = script.execute(null, "Hello"); + String ctl = "Hello\nHello"; + Assertions.assertEquals(ctl, o); + } + + @Test + void testIssue455c() { + final JexlEngine jexl = new JexlBuilder().create(); + final JexlContext context = new MapContext(); + context.set("name", "Hello"); + final JxltEngine jxlt = jexl.createJxltEngine(); + final JxltEngine.Template template = jxlt.createTemplate("\n\t${name\n\t+\r\f name}\n"); + final StringWriter writer = new StringWriter(); + template.evaluate(context, writer); + assertEquals("\n\tHelloHello\n", writer.toString()); + } + + @Test + void testIssue455d() { + final JexlEngine jexl = new JexlBuilder().create(); + // 'ref' contains 'greeting' which is the name of the variable to expand + String code = "`#{${\nref\t}}\n#{${\rref\f}}`;"; + JexlScript script = jexl.createScript(code, "ref", "greeting"); + Object o = script.execute(null, "greeting", "Hello"); + String ctl = "Hello\nHello"; + Assertions.assertEquals(ctl, o); + } + + @Test + void testIssue455e() { + final JexlEngine jexl = new JexlBuilder().create(); + // Evaluate nested immediate inside deferred at runtime using a parameterized script + final String src = "(name, suffix) -> `#{name} Hello ${name} ! #{suffix}`"; + final JexlScript script = jexl.createScript(src); + final Object result = script.execute(null, "World", "~"); + Assertions.assertEquals("World Hello World ! ~", result); + } + + @Test + void testIssue455f() { + final JexlEngine jexl = new JexlBuilder().create(); + // Evaluate nested immediate inside deferred at runtime using a parameterized script + final String src = "(name, suffix) -> `#{name + ' Hello'} ${name + ' !'} #{suffix}`"; + final JexlScript script = jexl.createScript(src); + final Object result = script.execute(null, "World", "~"); + Assertions.assertEquals("World Hello World ! ~", result); + } + + @Test + void testIssue455g() { + final JexlEngine jexl = new JexlBuilder().create(); + final JxltEngine jxlt = jexl.createJxltEngine(); + final JxltEngine.Template template = jxlt.createTemplate("${name} #{suffix}", "name", "suffix"); + final StringWriter writer = new StringWriter(); + // prepare requires immediate arguments; evaluate needs deferred arguments + template.prepare(null, "World", null).evaluate(null, writer, null, "~"); + Assertions.assertEquals("World ~", writer.toString()); + } + + @Test + void testIssue455h() { + final JexlEngine jexl = new JexlBuilder().create(); + final JxltEngine jxlt = jexl.createJxltEngine(); + final JxltEngine.Template template = jxlt.createTemplate("#{name + ' Hello'} ${name + ' !'} #{suffix}", "name", "suffix"); + final StringWriter writer = new StringWriter(); + // Prepare only the immediate name argument; evaluate needs both deferred arguments - name and suffix + template.prepare(null, "World").evaluate(null, writer, "World", "~"); + Assertions.assertEquals("World Hello World ! ~", writer.toString()); + } + }