From 36dc64dfb9195a97bba67c99d7bf863140dc539f Mon Sep 17 00:00:00 2001 From: Henrib Date: Wed, 4 Feb 2026 12:35:38 +0100 Subject: [PATCH 01/10] JEXL-455: ignore whitespaces when creating embedded expressions (interpolations, templates); --- .../jexl3/internal/TemplateEngine.java | 95 ++++++++++--------- .../apache/commons/jexl3/Issues400Test.java | 33 +++++++ 2 files changed, 83 insertions(+), 45 deletions(-) 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..b4be891d9 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java @@ -1073,7 +1073,7 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final strb.delete(0, Integer.MAX_VALUE); state = ParseState.CONST; } - } else { + } else if (!isIgnorable(c)) { if (c == '{') { immediate1 += 1; } @@ -1086,54 +1086,54 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final // 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; - strb.append(c); - } - continue; - case '}': - // balance nested immediate - if (deferred1 > 0) { - deferred1 -= 1; + case '"': + case '\'': 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)), + 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 if (!isIgnorable(c)) { + // 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); - } else { - dexpr = new DeferredExpression( - src, - jexl.jxltParse(srcInfo, noscript, src, scope), - null); + } + 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: + // do buildup expr + column = append(strb, expr, column, c); + break; } break; case ESCAPE: @@ -1184,6 +1184,11 @@ private String escapeString(final CharSequence str) { return StringParser.escapeString(str, (char) 0); } + private static boolean isIgnorable(char c) { + return c == '\n' || c == '\r' || c == '\t' || c == '\f' || c == '\b'; + } + + /** * Reads lines of a template grouping them by typed blocks. * diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 2deb4ab91..28b252e84 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; @@ -798,6 +799,38 @@ void testIssue441() { Assertions.assertEquals(ctl, o); } + @Test + void testIssue455a() { + 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 = "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 testIssue455() { + 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+ name}\n"); + final StringWriter writer = new StringWriter(); + template.evaluate(context, writer); + assertEquals("\n\tHelloHello\n", writer.toString()); + } + @Test void testIssue442() { final JexlEngine jexl = new JexlBuilder().create(); From 19633656a23abbf9d55dce39fc9ab964003ee342 Mon Sep 17 00:00:00 2001 From: Henrib Date: Wed, 4 Feb 2026 12:58:43 +0100 Subject: [PATCH 02/10] JEXL-455: fix #pr comments; --- .../apache/commons/jexl3/internal/TemplateEngine.java | 9 +++++---- .../java/org/apache/commons/jexl3/Issues400Test.java | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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 b4be891d9..53eb6c043 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java @@ -1108,7 +1108,7 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final strb.append(c); } else if (inner1 > 0) { inner1 -= 1; - } else if (!isIgnorable(c)) { + } else { // materialize the nested/deferred expr final String src = escapeString(strb); final JexlInfo srcInfo = info.at(lineno, column); @@ -1131,8 +1131,10 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final } break; default: - // do buildup expr - column = append(strb, expr, column, c); + if (!isIgnorable(c)) { + // do buildup expr + column = append(strb, expr, column, c); + } break; } break; @@ -1188,7 +1190,6 @@ private static boolean isIgnorable(char c) { return c == '\n' || c == '\r' || c == '\t' || c == '\f' || c == '\b'; } - /** * Reads lines of a template grouping them by typed blocks. * diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 28b252e84..5bbaec0fa 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -802,7 +802,7 @@ void testIssue441() { @Test void testIssue455a() { final JexlEngine jexl = new JexlBuilder().create(); - String code = "name -> `${name +\n name}`"; + String code = "name -> `${name +\n\t\b\f\r name}`"; JexlScript script = jexl.createScript(code); Object o = script.execute(null, "Hello"); String ctl = "HelloHello"; From 5bfc3f339f8323b9094e6ffcd24dfa6df230532e Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 00:01:30 +0100 Subject: [PATCH 03/10] JEXL-455: fix #pr comments; - fixed lurking bugs involving resolution of local variables in template expressions; - added tests; --- .../org/apache/commons/jexl3/JxltEngine.java | 14 ++- .../commons/jexl3/internal/Interpreter.java | 20 +-- .../jexl3/internal/TemplateEngine.java | 57 ++++----- .../jexl3/internal/TemplateInterpreter.java | 35 ++++-- .../jexl3/internal/TemplateScript.java | 10 +- .../apache/commons/jexl3/Issues400Test.java | 118 +++++++++++++----- 6 files changed, 159 insertions(+), 95 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JxltEngine.java b/src/main/java/org/apache/commons/jexl3/JxltEngine.java index ce684dd75..8d21ab7fc 100644 --- a/src/main/java/org/apache/commons/jexl3/JxltEngine.java +++ b/src/main/java/org/apache/commons/jexl3/JxltEngine.java @@ -309,7 +309,19 @@ public interface Template { * @param context the context to prepare against * @return the prepared version of the template */ - Template prepare(JexlContext context); + default Template prepare(JexlContext context) { + return prepare(context, (Object[]) null); + } + + /** + * Prepares this template by expanding any contained deferred TemplateExpression with optional arguments. + * This allows currying template parameters at prepare-time. + * + * @param context the context to prepare against + * @param args the arguments to bind (optional) + * @return the prepared version of the template + */ + Template prepare(JexlContext context, Object... args); } /** 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 53eb6c043..7b65966e1 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,7 +1053,8 @@ 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; @@ -1082,7 +1068,7 @@ 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) { @@ -1115,14 +1101,13 @@ TemplateExpression parseExpression(final JexlInfo info, final String expr, final TemplateExpression dexpr; if (nested) { dexpr = new NestedExpression( - escapeString(expr.substring(inested, column + 1)), - jexl.jxltParse(srcInfo, noscript, src, scope), - null); + escapeString(expr.substring(inested, column + 1)), + jexl.jxltParse(srcInfo, noscript, src, scope), + scope); } else { dexpr = new DeferredExpression( src, - jexl.jxltParse(srcInfo, noscript, src, scope), - null); + jexl.jxltParse(srcInfo, noscript, src, scope)); } builder.add(dexpr); strb.delete(0, Integer.MAX_VALUE); @@ -1187,7 +1172,7 @@ private String escapeString(final CharSequence str) { } private static boolean isIgnorable(char c) { - return c == '\n' || c == '\r' || c == '\t' || c == '\f' || c == '\b'; + return c == '\n' || c == '\r' || c == '\t' || c == '\f'; } /** 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..d177deeaa 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,25 @@ 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() { + if (frame != null && frame.stack != null) { + return frame.stack.clone(); + } + return 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..2276fca76 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java @@ -318,20 +318,20 @@ public Set> getVariables() { } @Override - public TemplateScript prepare(final JexlContext context) { + 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 +343,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/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 5bbaec0fa..8848228ca 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -799,38 +799,6 @@ void testIssue441() { Assertions.assertEquals(ctl, o); } - @Test - void testIssue455a() { - final JexlEngine jexl = new JexlBuilder().create(); - String code = "name -> `${name +\n\t\b\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 testIssue455() { - 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+ name}\n"); - final StringWriter writer = new StringWriter(); - template.evaluate(context, writer); - assertEquals("\n\tHelloHello\n", writer.toString()); - } - @Test void testIssue442() { final JexlEngine jexl = new JexlBuilder().create(); @@ -1013,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 + public 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 + public 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()); + } + } From 22b99fa3bf7b3a13775afbca464a2959ed18e9dc Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 07:56:01 +0100 Subject: [PATCH 04/10] JEXL-455: fixing compatibility issue, new prepare signature in TemplateScript; --- .../java/org/apache/commons/jexl3/JxltEngine.java | 14 ++++++++------ .../commons/jexl3/internal/TemplateScript.java | 5 +++++ .../apache/commons/jexl3/parser/ASTJexlScript.java | 6 +++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JxltEngine.java b/src/main/java/org/apache/commons/jexl3/JxltEngine.java index 8d21ab7fc..a5e2b720c 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(); @@ -309,19 +309,21 @@ public interface Template { * @param context the context to prepare against * @return the prepared version of the template */ - default Template prepare(JexlContext context) { - return prepare(context, (Object[]) null); - } + Template prepare(JexlContext context); /** * Prepares this template by expanding any contained deferred TemplateExpression with optional arguments. - * This allows currying template parameters at prepare-time. + *

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 */ - Template prepare(JexlContext context, Object... args); + default Template prepare(JexlContext context, Object... args) { + throw new UnsupportedOperationException("No default implementation"); + } } /** 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 2276fca76..c03952679 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateScript.java @@ -317,6 +317,11 @@ public Set> getVariables() { return collector.collected(); } + @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(); 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]; } /** From 7ecfc2fb079f873d76d6ca9cc581456d30aaead5 Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 07:59:11 +0100 Subject: [PATCH 05/10] JEXL-455: fixing #pr comment; --- .../apache/commons/jexl3/internal/TemplateInterpreter.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 d177deeaa..074e90c88 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateInterpreter.java @@ -306,10 +306,7 @@ protected Interpreter createInterpreter(final JexlContext context, final Frame l } private Object[] saveStack() { - if (frame != null && frame.stack != null) { - return frame.stack.clone(); - } - return null; + return frame != null && frame.stack != null? frame.stack.clone() : null; } private void restoreStack(Object[] stack) { From 76c5debbce429ebdfdfb201f4759ae5829a7242e Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 08:01:07 +0100 Subject: [PATCH 06/10] JEXL-455: fixing #pr comment; --- src/test/java/org/apache/commons/jexl3/Issues400Test.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 8848228ca..4ebdce4d0 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -1026,7 +1026,7 @@ void testIssue455d() { } @Test - public void testIssue455e() { + 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}`"; @@ -1036,7 +1036,7 @@ public void testIssue455e() { } @Test - public void testIssue455f() { + 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}`"; From a95b4bc7fbae6e0e5c59c50cb28ae575f1d3672c Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 08:57:05 +0100 Subject: [PATCH 07/10] JEXL-455: fixing Jacoco ratio - default method in interface not meant to be called drops coverage by 1%; --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 84ebc441c1500d337d1189873c326600dfd95555 Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 11:38:03 +0100 Subject: [PATCH 08/10] JEXL-455: java8 fails seemingly due to a null class loader; - hardening code again, class loader can never be null --- .../java/org/apache/commons/jexl3/internal/Engine.java | 3 ++- .../jexl3/internal/introspection/Introspector.java | 7 +++++-- .../commons/jexl3/internal/introspection/Uberspect.java | 9 +++++---- 3 files changed, 12 insertions(+), 7 deletions(-) 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/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(); } From 39519faaa01107e02c93b6c82a521138a9b748a8 Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 13:07:06 +0100 Subject: [PATCH 09/10] Update src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../commons/jexl3/internal/TemplateEngine.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 7b65966e1..28b11a088 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/TemplateEngine.java @@ -1171,6 +1171,21 @@ 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'; } From 12297175aba9b3ac39802a1f5618c886936d62e7 Mon Sep 17 00:00:00 2001 From: Henrib Date: Thu, 5 Feb 2026 13:07:52 +0100 Subject: [PATCH 10/10] Update src/main/java/org/apache/commons/jexl3/JxltEngine.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/org/apache/commons/jexl3/JxltEngine.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/jexl3/JxltEngine.java b/src/main/java/org/apache/commons/jexl3/JxltEngine.java index a5e2b720c..c91153dcf 100644 --- a/src/main/java/org/apache/commons/jexl3/JxltEngine.java +++ b/src/main/java/org/apache/commons/jexl3/JxltEngine.java @@ -322,7 +322,9 @@ public interface Template { * @since 3.6.2 */ default Template prepare(JexlContext context, Object... args) { - throw new UnsupportedOperationException("No default implementation"); + throw new UnsupportedOperationException( + "This template implementation does not support prepare with arguments. " + + "Override this method to provide support."); } }