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()) {

Reply via email to