This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch JEXL-438 in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
commit 2af3d8bcdbdfd10dfe6181df48ea54ed9726bcf9 Author: Henrib <hbies...@gmail.com> AuthorDate: Mon Apr 7 20:11:02 2025 +0200 JEXL-438: allows declaration of a JEXL script parser factory so that new syntaxes can be supported more easily; --- .../java/org/apache/commons/jexl3/JexlBuilder.java | 30 ++++- .../org/apache/commons/jexl3/JexlFeatures.java | 2 + .../java/org/apache/commons/jexl3/JexlOptions.java | 4 +- .../org/apache/commons/jexl3/internal/Engine.java | 17 ++- .../org/apache/commons/jexl3/internal/Scope.java | 30 +++-- .../apache/commons/jexl3/parser/JexlParser.java | 8 +- .../commons/jexl3/parser/JexlScriptParser.java | 39 +++++++ .../org/apache/commons/jexl3/Issues400Test.java | 122 +++++++++++++++++++++ .../org/apache/commons/jexl3/internal/Util.java | 10 +- 9 files changed, 231 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java index a1fa0d5b..f1db0356 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlBuilder.java +++ b/src/main/java/org/apache/commons/jexl3/JexlBuilder.java @@ -22,12 +22,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.function.IntFunction; +import java.util.function.Supplier; import org.apache.commons.jexl3.internal.Engine; import org.apache.commons.jexl3.internal.SoftCache; import org.apache.commons.jexl3.introspection.JexlPermissions; import org.apache.commons.jexl3.introspection.JexlSandbox; import org.apache.commons.jexl3.introspection.JexlUberspect; +import org.apache.commons.jexl3.parser.JexlScriptParser; import org.apache.commons.logging.Log; /** @@ -137,6 +139,9 @@ public class JexlBuilder { /** The cache class factory. */ private IntFunction<JexlCache<?,?>> cacheFactory = SoftCache::new; + /** The parser class factory. */ + private Supplier<JexlScriptParser> parserFactory = null; + /** The stack overflow limit. */ private int stackOverflow = Integer.MAX_VALUE; @@ -254,7 +259,7 @@ public class JexlBuilder { } /** - * Gets the expression factory the engine will use. + * Gets the expression-cache factory the engine will use. * @return the cache factory */ public IntFunction<JexlCache<?, ?>> cacheFactory() { @@ -262,7 +267,7 @@ public class JexlBuilder { } /** - * Sets the expression factory the engine will use. + * Sets the expression-cache factory the engine will use. * * @param factory the function to produce a cache. * @return this builder @@ -272,6 +277,25 @@ public class JexlBuilder { return this; } + /** + * Gets the Jexl script parser factory the engine will use. + * @return the cache factory + */ + public Supplier<JexlScriptParser> parserFactory() { + return this.parserFactory; + } + + /** + * Sets the Jexl script parser factory the engine will use. + * + * @param factory the function to produce a cache. + * @return this builder + */ + public JexlBuilder parserFactory(final Supplier<JexlScriptParser> factory) { + this.parserFactory = factory; + return this; + } + /** * Gets the maximum length for an expression to be cached. * @return the cache threshold @@ -487,7 +511,7 @@ public class JexlBuilder { } /** - * Is lexical shading is enabled? + * Checks whether lexical shading is enabled. * @see JexlOptions#isLexicalShade() * @return whether lexical shading is enabled * @deprecated 3.4.1 diff --git a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java index a687b296..c6afc783 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlFeatures.java +++ b/src/main/java/org/apache/commons/jexl3/JexlFeatures.java @@ -125,6 +125,8 @@ public final class JexlFeatures { public static final int CONST_CAPTURE = 23; /** Captured variables are reference. */ public static final int REF_CAPTURE = 24; + /** Captured variables are reference. */ + public static final int STRICT_STATEMENT = 25; /** * All features. * Ensure this is updated if additional features are added. diff --git a/src/main/java/org/apache/commons/jexl3/JexlOptions.java b/src/main/java/org/apache/commons/jexl3/JexlOptions.java index b7cb6b56..bcc8c93b 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlOptions.java +++ b/src/main/java/org/apache/commons/jexl3/JexlOptions.java @@ -284,7 +284,7 @@ public final class JexlOptions { } /** - * Gets sharing state + * Gets sharing state. * @return false if a copy of these options is used during execution, * true if those can potentially be modified */ @@ -320,7 +320,7 @@ public final class JexlOptions { } /** - * Gets strict interpolation status + * Gets the strict-interpolation flag of this options instance. * @return true if interpolation strings always return string, false otherwise */ public boolean isStrictInterpolation() { 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 2f16eae6..a0ac1966 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Engine.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Engine.java @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.IntFunction; import java.util.function.Predicate; +import java.util.function.Supplier; import org.apache.commons.jexl3.JexlArithmetic; import org.apache.commons.jexl3.JexlBuilder; @@ -60,6 +61,7 @@ import org.apache.commons.jexl3.parser.ASTMethodNode; import org.apache.commons.jexl3.parser.ASTNumberLiteral; import org.apache.commons.jexl3.parser.ASTStringLiteral; import org.apache.commons.jexl3.parser.JexlNode; +import org.apache.commons.jexl3.parser.JexlScriptParser; import org.apache.commons.jexl3.parser.Parser; import org.apache.commons.jexl3.parser.StringProvider; import org.apache.commons.logging.Log; @@ -267,6 +269,10 @@ public class Engine extends JexlEngine { * The default charset. */ protected final Charset charset; + /** + * The Jexl script parser factory. + */ + protected final Supplier<JexlScriptParser> parserFactory; /** * The atomic parsing flag; true whilst parsing. */ @@ -275,7 +281,7 @@ public class Engine extends JexlEngine { * The {@link Parser}; when parsing expressions, this engine uses the parser if it * is not already in use otherwise it will create a new temporary one. */ - protected final Parser parser = new Parser(new StringProvider(";")); //$NON-NLS-1$ + protected final JexlScriptParser parser; //$NON-NLS-1$ /** * The expression max length to hit the cache. */ @@ -363,11 +369,15 @@ public class Engine extends JexlEngine { // caching: final IntFunction<JexlCache<?, ?>> factory = conf.cacheFactory(); this.cacheFactory = factory == null ? SoftCache::new : factory; - this.cache = (JexlCache<Source, ASTJexlScript>) (conf.cache() > 0 ? factory.apply(conf.cache()) : null); + this.cache = (JexlCache<Source, ASTJexlScript>) (conf.cache() > 0 ? cacheFactory.apply(conf.cache()) : null); this.cacheThreshold = conf.cacheThreshold(); if (uberspect == null) { throw new IllegalArgumentException("uberspect cannot be null"); } + this.parserFactory = conf.parserFactory() == null ? + () -> new Parser(new StringProvider(";")) + : conf.parserFactory(); + this.parser = parserFactory.get(); } @Override @@ -811,8 +821,7 @@ public class Engine extends JexlEngine { } } else { // ...otherwise parser was in use, create a new temporary one - final Parser lparser = new Parser(new StringProvider(";")); - script = lparser.parse(ninfo, features, src, scope); + script = parserFactory.get().parse(ninfo, features, src, scope); } if (source != null) { cache.put(source, script); diff --git a/src/main/java/org/apache/commons/jexl3/internal/Scope.java b/src/main/java/org/apache/commons/jexl3/internal/Scope.java index d9c752a4..b2ea9e6b 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Scope.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Scope.java @@ -141,22 +141,20 @@ public final class Scope { * <p> * This method creates an new entry in the symbol map. * </p> - * @param name the parameter name + * @param param the parameter name * @return the register index storing this variable */ - public int declareParameter(final String name) { + public int declareParameter(final String param) { if (namedVariables == null) { namedVariables = new LinkedHashMap<>(); } else if (vars > 0) { throw new IllegalStateException("cant declare parameters after variables"); } - Integer register = namedVariables.get(name); - if (register == null) { - register = namedVariables.size(); - namedVariables.put(name, register); + return namedVariables.computeIfAbsent(param, name -> { + int register = namedVariables.size(); parms += 1; - } - return register; + return register; + }); } /** @@ -164,17 +162,15 @@ public final class Scope { * <p> * This method creates an new entry in the symbol map. * </p> - * @param name the variable name + * @param varName the variable name * @return the register index storing this variable */ - public int declareVariable(final String name) { + public int declareVariable(final String varName) { if (namedVariables == null) { namedVariables = new LinkedHashMap<>(); } - Integer register = namedVariables.get(name); - if (register == null) { - register = namedVariables.size(); - namedVariables.put(name, register); + return namedVariables.computeIfAbsent(varName, name -> { + int register = namedVariables.size(); vars += 1; // check if local is redefining captured if (parent != null) { @@ -186,8 +182,8 @@ public final class Scope { capturedVariables.put(register, pr); } } - } - return register; + return register; + }); } /** @@ -222,7 +218,7 @@ public final class Scope { */ public int getCaptureDeclaration(final int symbol) { final Integer declared = capturedVariables != null ? capturedVariables.get(symbol) : null; - return declared != null ? declared.intValue() : -1; + return declared != null ? declared : -1; } /** diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java index e408aa3f..f73328cf 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -39,7 +39,7 @@ import org.apache.commons.jexl3.internal.Scope; /** * The base class for parsing, manages the parameter/local variable frame. */ -public abstract class JexlParser extends StringParser { +public abstract class JexlParser extends StringParser implements JexlScriptParser { /** * A lexical unit is the container defining local symbols and their * visibility boundaries. @@ -706,11 +706,11 @@ public abstract class JexlParser extends StringParser { throw new JexlException.Assignment(xinfo, msg).clean(); } if (lv instanceof ASTIdentifier && !(lv instanceof ASTVar)) { - final ASTIdentifier var = (ASTIdentifier) lv; - if (isConstant(var.getSymbol())) { // if constant, fail... + final ASTIdentifier varName = (ASTIdentifier) lv; + if (isConstant(varName.getSymbol())) { // if constant, fail... JexlInfo xinfo = lv.jexlInfo(); xinfo = info.at(xinfo.getLine(), xinfo.getColumn()); - throw new JexlException.Assignment(xinfo, var.getName()).clean(); + throw new JexlException.Assignment(xinfo, varName.getName()).clean(); } } } diff --git a/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java b/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java new file mode 100644 index 00000000..4c413755 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlScriptParser.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.jexl3.parser; + +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlFeatures; +import org.apache.commons.jexl3.JexlInfo; +import org.apache.commons.jexl3.internal.Scope; + +/** + * The interface that produces a JEXL script AST from a source. + */ +public interface JexlScriptParser { + /** + * Parses a script or expression. + * + * @param info information structure + * @param features the set of parsing features + * @param src the expression to parse + * @param scope the script frame + * @return the parsed tree + * @throws JexlException if any error occurred during parsing + */ + ASTJexlScript parse(final JexlInfo info, final JexlFeatures features, final String src, final Scope scope); +} diff --git a/src/test/java/org/apache/commons/jexl3/Issues400Test.java b/src/test/java/org/apache/commons/jexl3/Issues400Test.java index 7dffef67..15edc17d 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues400Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues400Test.java @@ -32,6 +32,7 @@ import java.lang.reflect.Method; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -40,7 +41,15 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.jexl3.internal.Debugger; +import org.apache.commons.jexl3.internal.Scope; import org.apache.commons.jexl3.introspection.JexlPermissions; +import org.apache.commons.jexl3.parser.ASTJexlScript; +import org.apache.commons.jexl3.parser.JexlScriptParser; +import org.apache.commons.jexl3.parser.Parser; +import org.apache.commons.jexl3.parser.ParserTokenManager; +import org.apache.commons.jexl3.parser.Provider; +import org.apache.commons.jexl3.parser.StringProvider; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** @@ -608,4 +617,117 @@ public class Issues400Test { assertThrows(JexlException.Operator.class, () -> script.execute(ctxt)); } } + + /** The set of characters that may be followed by a '='.*/ + static final char[] EQ_FRIEND; + static { + char[] eq = {'!', ':', '<', '>', '^', '|', '&', '+', '-', '/', '*', '~', '='}; + Arrays.sort(eq); + EQ_FRIEND = eq; + } + + /** + * Transcodes a SQL-inspired expression to a JEXL expression. + * @param expr the expression to transcode + * @return the resulting expression + */ + private static String transcodeSQLExpr(final CharSequence expr) { + final StringBuilder strb = new StringBuilder(expr.length()); + final int end = expr.length(); + char previous = 0; + for (int i = 0; i < end; ++i) { + char c = expr.charAt(i); + if (previous == '<') { + // previous char a '<' now followed by '>' + if (c == '>') { + // replace '<>' with '!=' + strb.append("!="); + previous = c; + continue; + } else { + strb.append('<'); + } + } + if (c != '<') { + if (c == '=') { + // replace '=' with '==' when it does not follow a 'friend' + if (Arrays.binarySearch(EQ_FRIEND, previous) >= 0) { + strb.append(c); + } else { + strb.append("=="); + } + } else { + strb.append(c); + if (c == '"' || c == '\'') { + // read string, escape '\' + boolean escape = false; + for (i += 1; i < end; ++i) { + final char ec = expr.charAt(i); + strb.append(ec); + if (ec == '\\') { + escape = !escape; + } else if (escape) { + escape = false; + } else if (ec == c) { + break; + } + } + } + } + } + previous = c; + } + return strb.toString(); + } + + public static class SQLParser implements JexlScriptParser { + final Parser parser; + + public SQLParser() { + parser = new Parser(new StringProvider(";")); + } + + @Override + public ASTJexlScript parse(JexlInfo info, JexlFeatures features, String src, Scope scope) { + return parser.parse(info, features, transcodeSQLExpr(src), scope); + } + } + + + @Test + void testSQLTranspose() { + String[] e = { "a<>b", "a = 2", "a.b.c <> '1<>0'" }; + String[] j = { "a!=b", "a == 2", "a.b.c != '1<>0'" }; + for(int i = 0; i < e.length; ++i) { + String je = transcodeSQLExpr(e[i]); + Assertions.assertEquals(j[i], je); + } + } + + @Test + void testSQLNoChange() { + String[] e = { "a <= 2", "a >= 2", "a := 2", "a + 3 << 4 > 5", }; + for(int i = 0; i < e.length; ++i) { + String je = transcodeSQLExpr(e[i]); + Assertions.assertEquals(e[i], je); + } + } + + @Test + void test438() {// no local, no lambda, no loops, no-side effects + final JexlFeatures f = new JexlFeatures() + .localVar(false) + .lambda(false) + .loops(false) + .sideEffect(false) + .sideEffectGlobal(false); + JexlBuilder builder = new JexlBuilder().parserFactory(SQLParser::new).cache(32).features(f); + JexlEngine sqle = builder.create(); + Assertions.assertTrue((boolean) sqle.createScript("a <> 25", "a").execute(null, 24)); + Assertions.assertFalse((boolean) sqle.createScript("a <> 25", "a").execute(null, 25)); + Assertions.assertFalse((boolean) sqle.createScript("a = 25", "a").execute(null, 24)); + Assertions.assertTrue((boolean) sqle.createScript("a != 25", "a").execute(null, 24)); + Assertions.assertTrue((boolean) sqle.createScript("a = 25", "a").execute(null, 25)); + Assertions.assertFalse((boolean) sqle.createScript("a != 25", "a").execute(null, 25)); + } } diff --git a/src/test/java/org/apache/commons/jexl3/internal/Util.java b/src/test/java/org/apache/commons/jexl3/internal/Util.java index c5509527..b34f414e 100644 --- a/src/test/java/org/apache/commons/jexl3/internal/Util.java +++ b/src/test/java/org/apache/commons/jexl3/internal/Util.java @@ -25,6 +25,8 @@ import org.apache.commons.jexl3.JexlFeatures; import org.apache.commons.jexl3.JexlScript; import org.apache.commons.jexl3.parser.ASTJexlScript; import org.apache.commons.jexl3.parser.JexlNode; +import org.apache.commons.jexl3.parser.JexlScriptParser; +import org.apache.commons.jexl3.parser.Parser; /** * Helper methods for validate sessions. @@ -77,7 +79,13 @@ public class Util { return; } final Engine jdbg = new Engine(); - jdbg.parser.allowRegisters(true); + JexlScriptParser jexlp = jdbg.parser;; + if (!(jexlp instanceof Parser)) { + // jexl-438 escape + return; + } + Parser parser = (Parser) jexlp; + parser.allowRegisters(true); final Debugger dbg = new Debugger(); // iterate over all expression in for (final Map.Entry<Source, ASTJexlScript> entry : jexl.cache.entries()) {