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- * 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());
+ }
+
}