This is an automated email from the ASF dual-hosted git repository. henrib pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-jexl.git
The following commit(s) were added to refs/heads/master by this push: new e65235ba JEXL-379: updated new syntax; new 5e0f56b8 Merge remote-tracking branch 'origin/master' e65235ba is described below commit e65235ba16d0a32b2e2591a590798de7600d0cfd Author: henrib <hen...@apache.org> AuthorDate: Sun Aug 21 10:06:47 2022 +0200 JEXL-379: updated new syntax; --- .../java/org/apache/commons/jexl3/JexlContext.java | 13 ++ .../apache/commons/jexl3/internal/Debugger.java | 6 + .../apache/commons/jexl3/internal/Interpreter.java | 13 ++ .../commons/jexl3/internal/ScriptVisitor.java | 5 + .../jexl3/internal/introspection/ClassTool.java | 13 +- .../introspection/SimpleClassNameSolver.java | 157 +++++++++++++++++++++ .../jexl3/parser/ASTQualifiedIdentifier.java | 50 +++++++ .../apache/commons/jexl3/parser/JexlParser.java | 14 +- .../org/apache/commons/jexl3/parser/Parser.jjt | 11 +- .../apache/commons/jexl3/parser/ParserVisitor.java | 2 + .../org/apache/commons/jexl3/Issues300Test.java | 10 ++ .../java/org/apache/commons/jexl3/ScriptTest.java | 129 ++++++++++++++--- src/test/scripts/httpPost.jexl | 59 ++++---- 13 files changed, 428 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/apache/commons/jexl3/JexlContext.java b/src/main/java/org/apache/commons/jexl3/JexlContext.java index 67dc118f..06d7704f 100644 --- a/src/main/java/org/apache/commons/jexl3/JexlContext.java +++ b/src/main/java/org/apache/commons/jexl3/JexlContext.java @@ -90,6 +90,19 @@ public interface JexlContext { Object resolveNamespace(String name); } + /** + * A marker interface that solves a simple class name into a fully-qualified one. + * @since 3.3 + */ + interface ClassNameResolver { + /** + * Resolves a class name. + * @param name the simple class name + * @return the fully qualified class name + */ + String resolveClassName(String name); + } + /** * A marker interface of the JexlContext, NamespaceFunctor allows creating an instance * to delegate namespace methods calls to. diff --git a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java index 196c0fcf..5031607f 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Debugger.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Debugger.java @@ -1020,6 +1020,12 @@ public class Debugger extends ParserVisitor implements JexlInfo.Detail { return data; } + @Override + protected Object visit(ASTQualifiedIdentifier node, Object data) { + String img = node.getName(); + return this.check(node, img, data); + } + @Override protected Object visit(ASTStringLiteral node, Object data) { String img = StringParser.escapeString(node.getLiteral(), '\''); 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 4ba5113b..a9a6a4e3 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java +++ b/src/main/java/org/apache/commons/jexl3/internal/Interpreter.java @@ -1125,6 +1125,19 @@ public class Interpreter extends InterpreterBase { return object; } + @Override + protected Object visit(final ASTQualifiedIdentifier node, final Object data) { + String name = node.getName(); + if (context instanceof JexlContext.ClassNameResolver) { + JexlContext.ClassNameResolver resolver = (JexlContext.ClassNameResolver) context; + String fqcn = resolver.resolveClassName(name); + if (fqcn != null) { + return fqcn; + } + } + return name; + } + /** * Evaluates an access identifier based on the 2 main implementations; * static (name or numbered identifier) or dynamic (jxlt). diff --git a/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java b/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java index 9da2182d..70f26f30 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java +++ b/src/main/java/org/apache/commons/jexl3/internal/ScriptVisitor.java @@ -472,4 +472,9 @@ public class ScriptVisitor extends ParserVisitor { protected Object visit(final ASTAnnotatedStatement node, final Object data) { return visitNode(node, data); } + + @Override + protected Object visit(final ASTQualifiedIdentifier node, final Object data) { + return visitNode(node, data); + } } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassTool.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassTool.java index fb531b35..b4f93233 100644 --- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassTool.java +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassTool.java @@ -30,8 +30,9 @@ class ClassTool { private static final MethodHandle GET_PKGNAME; /** The Module.isExported(String packageName) method. */ private static final MethodHandle IS_EXPORTED; + static { - final MethodHandles.Lookup LOOKUP= MethodHandles.lookup(); + final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); MethodHandle getModule = null; MethodHandle getPackageName = null; MethodHandle isExported = null; @@ -64,11 +65,12 @@ class ClassTool { * </code> * This is required since some classes and methods may not be exported thus not callable through * reflection. + * * @param declarator the class * @return true if class is exported or no module support exists */ static boolean isExported(Class<?> declarator) { - if (IS_EXPORTED != null) { + if (IS_EXPORTED != null) { try { final Object module = GET_MODULE.invoke(declarator); if (module != null) { @@ -84,6 +86,7 @@ class ClassTool { /** * Gets the package name of a class (class.getPackage() may return null). + * * @param clz the class * @return the class package name */ @@ -94,13 +97,13 @@ class ClassTool { if (GET_PKGNAME != null) { try { return (String) GET_PKGNAME.invoke(clz); - } catch(Throwable xany) { + } catch (Throwable xany) { return ""; } } // remove array Class<?> clazz = clz; - while(clazz.isArray()) { + while (clazz.isArray()) { clazz = clazz.getComponentType(); } // mimic getPackageName() @@ -109,7 +112,7 @@ class ClassTool { } // remove enclosing Class<?> walk = clazz.getEnclosingClass(); - while(walk != null) { + while (walk != null) { clazz = walk; walk = walk.getEnclosingClass(); } diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/SimpleClassNameSolver.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/SimpleClassNameSolver.java new file mode 100644 index 00000000..918ae4d7 --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/SimpleClassNameSolver.java @@ -0,0 +1,157 @@ +/* + * 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.internal.introspection; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Helper solving a simple class name into a fully-qualified class name using packages. + */ +public class SimpleClassNameSolver { + /** + * The class loader. + */ + private final ClassLoader loader; + /** + * A lock for RW concurrent ops. + */ + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + /** + * The set of packages to be used as import. + */ + private final Set<String> imports = new LinkedHashSet<>(); + /** + * The set of solved fqcns based on imports. + */ + private final Map<String, String> fqcns = new HashMap<>(); + /** + * Optional parent solver. + */ + private final SimpleClassNameSolver parent; + + /** + * Creates a class name solver. + * + * @param loader the optional class loader + * @param packages the optional package names + */ + public SimpleClassNameSolver(ClassLoader loader, List<String> packages) { + this.loader = loader == null ? SimpleClassNameSolver.class.getClassLoader() : loader; + if (packages != null) { + imports.addAll(packages); + } + this.parent = null; + } + + /** + * Creates a class name solver. + * @param solver the parent solver + * @throws NullPointerException if parent solver is null + */ + public SimpleClassNameSolver(SimpleClassNameSolver solver) { + if (solver == null) { + throw new NullPointerException("parent solver can not be null"); + } + this.parent = solver; + this.loader = solver.loader; + } + + /** + * Checks is a package is imported by this solver of one of its ascendants. + * @param pkg the package name + * @return true if an import exists for this package, false otherwise + */ + boolean isImporting(String pkg) { + if (parent != null && parent.isImporting(pkg)) { + return true; + } + lock.readLock().lock(); + try { + return imports.contains(pkg); + } finally { + lock.readLock().unlock(); + } + } + + /** + * Adds a list of packages as solving roots. + * + * @param packages the packages + */ + public void addPackages(Collection<String> packages) { + if (packages != null) { + lock.writeLock().lock(); + try { + if (parent == null) { + imports.addAll(packages); + } else { + for(String pkg : packages) { + if (!parent.isImporting(pkg)) { + imports.add(pkg); + } + } + } + } finally { + lock.writeLock().unlock(); + } + } + } + + /** + * Gets a fully qualified class name from a simple class name and imports. + * + * @param name the simple name + * @return the fqcn + */ + public String getQualifiedName(String name) { + String fqcn; + if (parent != null && (fqcn = parent.getQualifiedName(name)) != null) { + return fqcn; + } + lock.readLock().lock(); + try { + fqcn = fqcns.get(name); + } finally { + lock.readLock().unlock(); + } + if (fqcn == null) { + Class<?> clazz; + for (String pkg : imports) { + try { + clazz = loader.loadClass(pkg + "." + name); + lock.writeLock().lock(); + try { + fqcns.put(name, fqcn = clazz.getName()); + break; + } finally { + lock.writeLock().unlock(); + } + } catch (ClassNotFoundException e) { + // nope + } + } + } + return fqcn; + } +} diff --git a/src/main/java/org/apache/commons/jexl3/parser/ASTQualifiedIdentifier.java b/src/main/java/org/apache/commons/jexl3/parser/ASTQualifiedIdentifier.java new file mode 100644 index 00000000..a5610b4c --- /dev/null +++ b/src/main/java/org/apache/commons/jexl3/parser/ASTQualifiedIdentifier.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * Identifiers, variables, ie symbols. + */ +public class ASTQualifiedIdentifier extends JexlNode { + protected String name = null; + + ASTQualifiedIdentifier(final int id) { + super(id); + } + + ASTQualifiedIdentifier(final Parser p, final int id) { + super(p, id); + } + + @Override + public String toString() { + return name; + } + + public void setName(String qualified) { + this.name = qualified; + } + + public String getName() { + return name; + } + + @Override + public Object jjtAccept(final ParserVisitor visitor, final Object data) { + return visitor.visit(this, data); + } +} 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 2edef122..5ade580d 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java +++ b/src/main/java/org/apache/commons/jexl3/parser/JexlParser.java @@ -30,6 +30,7 @@ import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -517,7 +518,18 @@ public abstract class JexlParser extends StringParser { namespaces.add(nsname); } } - pragmas.put(key, value); + Object previous = pragmas.put(key, value); + if (previous != null) { + Set<Object> values; + if (previous instanceof Set<?>) { + values = (Set<Object>) previous; + } else { + values = new LinkedHashSet<Object>(); + pragmas.put(key, values); + values.add(previous); + } + values.add(value); + } } /** diff --git a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt index 074c8471..763ef4a5 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt +++ b/src/main/java/org/apache/commons/jexl3/parser/Parser.jjt @@ -916,9 +916,18 @@ void FunctionCall() #void : {} LOOKAHEAD(<IDENTIFIER> <LPAREN>) Identifier(true) Arguments() #FunctionNode(2) } +void QualifiedIdentifier() #QualifiedIdentifier : { + LinkedList<String> lstr = new LinkedList<String>(); +} +{ + pragmaKey(lstr) { jjtThis.setName(stringify(lstr));} +} + void Constructor() #ConstructorNode : {} { - <NEW> <LPAREN> Expression() ( <COMMA> Expression() )* <RPAREN> + LOOKAHEAD(2) <NEW> <LPAREN> Expression() ( <COMMA> Expression() )* <RPAREN> + | + <NEW> QualifiedIdentifier() <LPAREN> [ Expression() ( <COMMA> Expression() )* ] <RPAREN> } void Parameter() #void : diff --git a/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java b/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java index e457c417..66479e23 100644 --- a/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java +++ b/src/main/java/org/apache/commons/jexl3/parser/ParserVisitor.java @@ -207,4 +207,6 @@ public abstract class ParserVisitor { protected abstract Object visit(ASTAnnotation node, Object data); protected abstract Object visit(ASTAnnotatedStatement node, Object data); + + protected abstract Object visit(final ASTQualifiedIdentifier node, final Object data); } diff --git a/src/test/java/org/apache/commons/jexl3/Issues300Test.java b/src/test/java/org/apache/commons/jexl3/Issues300Test.java index 12187785..e223080d 100644 --- a/src/test/java/org/apache/commons/jexl3/Issues300Test.java +++ b/src/test/java/org/apache/commons/jexl3/Issues300Test.java @@ -874,4 +874,14 @@ public class Issues300Test { DOMICILE } + + @Test + public void test377() { + String text = "function add(x, y) { x + y } add(a, b)"; + JexlEngine jexl = new JexlBuilder().safe(true).create(); + JexlScript script = jexl.createScript(text, "a", "b"); + Object result = script.execute(null, 20, 22); + Assert.assertEquals(42, result); + } + } diff --git a/src/test/java/org/apache/commons/jexl3/ScriptTest.java b/src/test/java/org/apache/commons/jexl3/ScriptTest.java index fb9f0e65..0cc79c75 100644 --- a/src/test/java/org/apache/commons/jexl3/ScriptTest.java +++ b/src/test/java/org/apache/commons/jexl3/ScriptTest.java @@ -16,16 +16,27 @@ */ package org.apache.commons.jexl3; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.apache.commons.jexl3.internal.introspection.SimpleClassNameSolver; +import org.junit.Assert; +import org.junit.Test; + import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.InetSocketAddress; import java.net.URL; - -import org.junit.Assert; -import org.junit.Test; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.function.Function; /** * Tests for JexlScript @@ -83,26 +94,81 @@ public class ScriptTest extends JexlTestCase { Assert.assertEquals("getText is wrong", code, s.getSourceText()); } + static class ImportContext extends MapContext implements JexlContext.ClassNameResolver, JexlContext.PragmaProcessor { + private final SimpleClassNameSolver solver; + ImportContext(SimpleClassNameSolver parent) { + solver = new SimpleClassNameSolver(parent); + } + + @Override + public String resolveClassName(String name) { + return name.indexOf('.') < 0 ? solver.getQualifiedName(name) : name; + } + + @Override + public void processPragma(String key, Object value) { + processPragma(null, key, value); + } + + @Override + public void processPragma(JexlOptions opts, String key, Object value) { + if ("jexl.import".equals(key)) { + if (value instanceof Collection<?>) { + for(Object pkg : ((Collection<?>) value)) { + solver.addPackages(Collections.singletonList(pkg.toString())); + } + } else { + solver.addPackages(Collections.singletonList(value.toString())); + } + } + } + } + @Test public void testScriptJsonFromFileJexl() { - final File testScript = new File(TEST_JSON); - final JexlScript s = JEXL.createScript(testScript); - final JexlContext jc = new MapContext(); - jc.set("httpr", new HttpPostRequest()); - Object result = s.execute(jc); - Assert.assertNotNull(result); - Assert.assertEquals("{ \"id\": 101}", result); + SimpleClassNameSolver baseSolver = new SimpleClassNameSolver(null, Arrays.asList("java.lang")); + HttpServer server = null; + try { + final String response = "{ \"id\": 101}"; + server = createJsonServer(h -> response); + final File httprFile = new File(TEST_JSON); + final JexlScript httprScript = JEXL.createScript(httprFile); + final JexlContext jc = new ImportContext(baseSolver); + Object httpr = httprScript.execute(jc); + final JexlScript s = JEXL.createScript("(httpr,url)->httpr.execute(url, null)"); + //jc.set("httpr", new HttpPostRequest()); + Object result = s.execute(jc, httpr, "http://localhost:8001/test"); + Assert.assertNotNull(result); + Assert.assertEquals(response, result); + } catch(IOException xio) { + Assert.fail(xio.getMessage()); + } finally { + if (server != null) { + server.stop(0); + } + } } @Test public void testScriptJsonFromFileJava() { - final String testScript ="httpr.execute('https://jsonplaceholder.typicode.com/posts', null)"; - final JexlScript s = JEXL.createScript(testScript); - final JexlContext jc = new MapContext(); - jc.set("httpr", new HttpPostRequest()); - Object result = s.execute(jc); - Assert.assertNotNull(result); - Assert.assertEquals("{ \"id\": 101}", result); + HttpServer server = null; + try { + final String response = "{ \"id\": 101}"; + server = createJsonServer(h -> response); + final String testScript = "httpr.execute('http://localhost:8001/test', null)"; + final JexlScript s = JEXL.createScript(testScript); + final JexlContext jc = new MapContext(); + jc.set("httpr", new HttpPostRequest()); + Object result = s.execute(jc); + Assert.assertNotNull(result); + Assert.assertEquals(response, result); + } catch(IOException xio) { + Assert.fail(xio.getMessage()); + } finally { + if (server != null) { + server.stop(0); + } + } } /** @@ -151,6 +217,35 @@ public class ScriptTest extends JexlTestCase { return response.toString(); } + /** + * Creates a simple local http server. + * <p>Only handles POST request on /test</p> + * @return the server + * @throws IOException + */ + static HttpServer createJsonServer(final Function<HttpExchange, String> responder) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress("localhost", 8001), 0); + ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + server.createContext("/test", new HttpHandler() { + @Override + public void handle(HttpExchange httpExchange) throws IOException { + if ("POST".equals(httpExchange.getRequestMethod())) { + OutputStream outputStream = httpExchange.getResponseBody(); + String json = responder.apply(httpExchange); + httpExchange.sendResponseHeaders(200, json.length()); + outputStream.write(json.toString().getBytes()); + outputStream.flush(); + outputStream.close(); + } else { + // error + httpExchange.sendResponseHeaders(500, 0); + } + } + }); + server.setExecutor(threadPoolExecutor); + server.start(); + return server; + } @Test public void testScriptFromFile() { diff --git a/src/test/scripts/httpPost.jexl b/src/test/scripts/httpPost.jexl index 5f106203..7c6bbe6b 100644 --- a/src/test/scripts/httpPost.jexl +++ b/src/test/scripts/httpPost.jexl @@ -17,37 +17,36 @@ //------------------------------------------------------------------- // send a POST Request //------------------------------------------------------------------- +#pragma jexl.import java.net +#pragma jexl.import java.io +{ + "execute" : (sURL, jsonData) -> { + let url = new URL(sURL); + let con = url.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Accept", "application/json"); -var httpPostRequest = (sURL, jsonData) -> { - var url = new("java.net.URL", sURL); - var con = url.openConnection(); - con.setRequestMethod("POST"); - con.setRequestProperty("Accept", "application/json"); + // send data + if (jsonData != null) { + con.setRequestProperty("Content-Type", "application/json; utf-8"); + con.setDoOutput(true); + let outputStream = con.getOutputStream(); + let input = jsonData.getBytes("utf-8"); + outputStream.write(input, 0, size(input)); + } - // send data - if ( jsonData != null ) { - con.setRequestProperty("Content-Type", "application/json; utf-8"); - con.setDoOutput(true); - - var outputStream = con.getOutputStream(); - var input = jsonData.getBytes("utf-8"); - outputStream.write(input, 0, size(input)); - } - - // read response - var responseCode = con.getResponseCode(); - var inputStream = null; - inputStream = con.getInputStream(); - var response = new("java.lang.StringBuffer"); - if (inputStream != null) { - var in = new("java.io.BufferedReader", new("java.io.InputStreamReader", inputStream)); - var inputLine = ""; - while ((inputLine = in.readLine()) != null) { - response.append(inputLine); + // read response + let responseCode = con.getResponseCode(); + let inputStream = con.getInputStream(); + let response = new StringBuffer(); + if (inputStream != null) { + let in = new BufferedReader(new InputStreamReader(inputStream)); + var inputLine = ""; + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + } + response.toString(); } - in.close(); - } - response.toString(); } - -httpPostRequest("https://jsonplaceholder.typicode.com/posts");