You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by he...@apache.org on 2023/03/08 19:15:47 UTC
[commons-jexl] branch master updated: JEXL: getting ready for 3.3; - Various javadoc and documentation, fuller example; - Refined permissions to ease overload;
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 887ca515 JEXL: getting ready for 3.3; - Various javadoc and documentation, fuller example; - Refined permissions to ease overload;
887ca515 is described below
commit 887ca51567126a902a5d2f557bd3c9bfa0abc9e8
Author: henrib <he...@apache.org>
AuthorDate: Wed Mar 8 20:15:40 2023 +0100
JEXL: getting ready for 3.3;
- Various javadoc and documentation, fuller example;
- Refined permissions to ease overload;
---
.../org/apache/commons/jexl3/JexlArithmetic.java | 4 +-
.../org/apache/commons/jexl3/JexlException.java | 19 +--
.../jexl3/internal/introspection/ClassMap.java | 4 +
.../jexl3/internal/introspection/Permissions.java | 50 +++----
.../jexl3/introspection/JexlPermissions.java | 110 +++++++++++++++
src/site/xdoc/index.xml | 151 +++++++++++++++++----
.../org/apache/commons/jexl3/ClassPermissions.java | 82 +++++++++++
.../org/apache/commons/jexl3/Issues300Test.java | 24 ++--
.../apache/commons/jexl3/examples/StreamTest.java | 118 ++++++++++++++++
.../apache/commons/jexl3/jexl342/OptionalTest.java | 24 +++-
10 files changed, 491 insertions(+), 95 deletions(-)
diff --git a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
index 75f50ec3..4121cc11 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlArithmetic.java
@@ -62,7 +62,7 @@ public class JexlArithmetic {
/** Marker class for null operand exceptions. */
public static class NullOperand extends ArithmeticException {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 4720876194840764770L;
}
/** Double.MAX_VALUE as BigDecimal. */
@@ -1900,7 +1900,7 @@ public class JexlArithmetic {
while(arithmeticClass != JexlArithmetic.class) {
try {
Method cmp = arithmeticClass.getDeclaredMethod("compare", Object.class, Object.class, String.class);
- if (cmp != null && cmp.getDeclaringClass() != JexlArithmetic.class) {
+ if (cmp.getDeclaringClass() != JexlArithmetic.class) {
return true;
}
} catch (NoSuchMethodException xany) {
diff --git a/src/main/java/org/apache/commons/jexl3/JexlException.java b/src/main/java/org/apache/commons/jexl3/JexlException.java
index c0c69e7c..3e3862bb 100644
--- a/src/main/java/org/apache/commons/jexl3/JexlException.java
+++ b/src/main/java/org/apache/commons/jexl3/JexlException.java
@@ -214,7 +214,7 @@ public class JexlException extends RuntimeException {
}
/**
- * Merge the node info and the cause info to obtain best possible location.
+ * Merge the node info and the cause info to obtain the best possible location.
*
* @param info the node
* @param cause the cause
@@ -390,10 +390,10 @@ public class JexlException extends RuntimeException {
/**
* Removes a slice from a source.
* @param src the source
- * @param froml the begin line
- * @param fromc the begin column
- * @param tol the to line
- * @param toc the to column
+ * @param froml the beginning line
+ * @param fromc the beginning column
+ * @param tol the ending line
+ * @param toc the ending column
* @return the source with the (begin) to (to) zone removed
*/
public static String sliceSource(final String src, final int froml, final int fromc, final int tol, final int toc) {
@@ -989,11 +989,7 @@ public class JexlException extends RuntimeException {
* @since 3.0
*/
public static class Cancel extends JexlException {
- /**
- *
- */
- private static final long serialVersionUID = 1L;
-
+ private static final long serialVersionUID = 7735706658499597964L;
/**
* Creates a new instance of Cancel.
*
@@ -1031,7 +1027,7 @@ public class JexlException extends RuntimeException {
/**
* Creates a new instance of Continue.
*
- * @param node the continue
+ * @param node the continue-node
*/
public Continue(final JexlNode node) {
super(node, "continue loop", null, false);
@@ -1072,7 +1068,6 @@ public class JexlException extends RuntimeException {
/**
* Detailed info message about this error.
* Format is "debug![begin,end]: string \n msg" where:
- *
* - debug is the debugging information if it exists (@link JexlEngine.setDebug)
* - begin, end are character offsets in the string for the precise location of the error
* - string is the string representation of the offending expression
diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java
index 78f0dd76..0fabac30 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/ClassMap.java
@@ -321,6 +321,10 @@ final class ClassMap {
try {
final Method[] methods = clazz.getDeclaredMethods();
for (final Method mi : methods) {
+ // method must be public, not a bridge, not synthetic
+ if (!Modifier.isPublic(mi.getModifiers()) || mi.isBridge() || mi.isSynthetic()) {
+ continue;
+ }
// add method to byKey cache; do not override
final MethodKey key = new MethodKey(mi);
final Method pmi = cache.byKey.putIfAbsent(key, permissions.allow(mi) ? mi : CACHE_MISS);
diff --git a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java
index 90574151..cbf5d987 100644
--- a/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java
+++ b/src/main/java/org/apache/commons/jexl3/internal/introspection/Permissions.java
@@ -20,7 +20,6 @@ package org.apache.commons.jexl3.internal.introspection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -282,7 +281,7 @@ public class Permissions implements JexlPermissions {
}
/**
- * Whether the wilcard set of packages allows a given class to be introspected.
+ * Whether the wildcard set of packages allows a given class to be introspected.
* @param clazz the package name (not null)
* @return true if allowed, false otherwise
*/
@@ -392,7 +391,7 @@ public class Permissions implements JexlPermissions {
*/
@Override
public boolean allow(final Package pack) {
- return pack != null && !deny(pack);
+ return validate(pack) && !deny(pack);
}
/**
@@ -403,7 +402,8 @@ public class Permissions implements JexlPermissions {
*/
@Override
public boolean allow(final Class<?> clazz) {
- if (clazz == null) {
+ // clazz must be not null
+ if (!validate(clazz)) {
return false;
}
// class must be allowed
@@ -433,11 +433,8 @@ public class Permissions implements JexlPermissions {
*/
@Override
public boolean allow(final Constructor<?> ctor) {
- if (ctor == null) {
- return false;
- }
- // field must be public
- if (!Modifier.isPublic(ctor.getModifiers())) {
+ // method must be not null, public
+ if (!validate(ctor)) {
return false;
}
// check declared restrictions
@@ -460,11 +457,8 @@ public class Permissions implements JexlPermissions {
*/
@Override
public boolean allow(final Field field) {
- if (field == null) {
- return false;
- }
// field must be public
- if (!Modifier.isPublic(field.getModifiers())) {
+ if (!validate(field)) {
return false;
}
// check declared restrictions
@@ -489,27 +483,24 @@ public class Permissions implements JexlPermissions {
*/
@Override
public boolean allow(final Method method) {
- if (method == null) {
- return false;
- }
- // method must be public
- if (!Modifier.isPublic(method.getModifiers())) {
+ // method must be not null, public, not synthetic, not bridge
+ if (!validate(method)) {
return false;
}
// method must be allowed
- if (!allowMethod(method)) {
+ if (denyMethod(method)) {
return false;
}
Class<?> clazz = method.getDeclaringClass();
// gather if any implementation of the method is explicitly allowed by the packages
final boolean[] explicit = { wildcardAllow(clazz) };
- // lets walk all interfaces
+ // let's walk all interfaces
for (final Class<?> inter : clazz.getInterfaces()) {
if (!allow(inter, method, explicit)) {
return false;
}
}
- // lets walk all super classes
+ // let's walk all super classes
clazz = clazz.getSuperclass();
// walk all superclasses
while (clazz != null) {
@@ -522,18 +513,13 @@ public class Permissions implements JexlPermissions {
}
/**
- * Checks whether a method is allowed.
+ * Checks whether a method is denied.
* @param method the method
- * @return true if it has not been disallowed through annotation or declaration
+ * @return true if it has been disallowed through annotation or declaration
*/
- private boolean allowMethod(final Method method) {
- // check declared restrictions
- if (deny(method)) {
- return false;
- }
- final Class<?> clazz = method.getDeclaringClass();
- // class must not be denied
- return !deny(clazz);
+ private boolean denyMethod(final Method method) {
+ // check declared restrictions, class must not be denied
+ return deny(method) || deny(method.getDeclaringClass());
}
/**
@@ -548,7 +534,7 @@ public class Permissions implements JexlPermissions {
// check if method in that class is declared ie overrides
final Method override = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
// should not be possible...
- if (!allowMethod(override)) {
+ if (denyMethod(override)) {
return false;
}
// explicit |= ...
diff --git a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
index 045cdefc..c4b7d276 100644
--- a/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
+++ b/src/main/java/org/apache/commons/jexl3/introspection/JexlPermissions.java
@@ -21,6 +21,7 @@ import org.apache.commons.jexl3.internal.introspection.PermissionsParser;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
/**
* This interface describes permissions used by JEXL introspection that constrain which
@@ -246,4 +247,113 @@ public interface JexlPermissions {
"java.nio { Path { } Paths { } Files { } }",
"java.rmi"
);
+
+ /**
+ * Checks that a package is valid for permission check.
+ * @param pack the palcaga
+ * @return true if the class is not null, false otherwise
+ */
+ default boolean validate(final Package pack) {
+ return pack != null;
+ }
+
+ /**
+ * Checks that a class is valid for permission check.
+ * @param clazz the class
+ * @return true if the class is not null, false otherwise
+ */
+ default boolean validate(final Class<?> clazz) {
+ return clazz != null;
+ }
+
+ /**
+ * Checks that a constructor is valid for permission check.
+ * @param ctor the constructor
+ * @return true if constructor is not null and public, false otherwise
+ */
+ default boolean validate(final Constructor<?> ctor) {
+ if (ctor == null) {
+ return false;
+ }
+ // field must be public
+ if (!Modifier.isPublic(ctor.getModifiers())) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks that a method is valid for permission check.
+ * @param method the method
+ * @return true if method is not null, public, ,ot-synthetic, not-bridge, false otherwise
+ */
+ default boolean validate(final Method method) {
+ if (method == null) {
+ return false;
+ }
+ // method must be public
+ if (!Modifier.isPublic(method.getModifiers()) || method.isBridge() || method.isSynthetic()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks that a field is valid for permission check.
+ * @param field the constructor
+ * @return true if field is not null and public, false otherwise
+ */
+ default boolean validate(final Field field) {
+ if (field == null) {
+ return false;
+ }
+ // field must be public
+ if (!Modifier.isPublic(field.getModifiers())) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * A base for permission delegation allowing greater functional malleability.
+ * Overloads should call the appropriate validate() method early in their body.
+ */
+ class Delegate implements JexlPermissions {
+ /** The permissions we delegate to. */
+ protected final JexlPermissions base;
+
+ protected Delegate(JexlPermissions delegate) {
+ base = delegate;
+ }
+
+ @Override
+ public boolean allow(Package pack) {
+ return base.allow(pack);
+ }
+
+ @Override
+ public boolean allow(Class<?> clazz) {
+ return base.allow(clazz);
+ }
+
+ @Override
+ public boolean allow(Constructor<?> ctor) {
+ return base.allow(ctor);
+ }
+
+ @Override
+ public boolean allow(Method method) {
+ return base.allow(method);
+ }
+
+ @Override
+ public boolean allow(Field field) {
+ return base.allow(field);
+ }
+
+ @Override
+ public JexlPermissions compose(String... src) {
+ return new Delegate(base.compose(src));
+ }
+ }
}
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
index 039a1434..d2ff2ed0 100644
--- a/src/site/xdoc/index.xml
+++ b/src/site/xdoc/index.xml
@@ -33,6 +33,8 @@ constructs seen in shell-script or ECMAScript.
<br/>
Its goal is to expose scripting features usable by technical operatives or consultants
working with enterprise platforms.
+In many use cases, JEXL allows end-users of an application to code their own scripts or expressions
+and ensure their execution within controlled functional constraints.
</p>
<p>
The library exposes a small footprint API
@@ -81,20 +83,21 @@ working with enterprise platforms.
introspection to expose property getters and setters. It also considers public class fields
as properties and allows to invoke any accessible method.
</p>
- <p>
- JEXL attempts to bring some of the lessons learned by the Velocity
- community about expression languages in templating to a wider audience.
- <a href="https://commons.apache.org/jelly">Commons Jelly</a> needed
- Velocity-ish method access, it just had to have it.
- </p>
- <p>
- It must be noted that JEXL is <strong>not</strong> a compatible implementation of EL as defined
- in JSTL 1.1 (JSR-052) or JSP 2.0 (JSR-152). For a compatible implementation of
- these specifications, see the <a href="https://commons.apache.org/el">Commons EL</a> project.
- </p>
</section>
- <section name="A Brief Example">
+ <section name="A Detailed Example">
+ <p>
+ To create expressions and scripts, a
+ <a href="apidocs/org/apache/commons/jexl3/JexlEngine.html">JexlEngine</a> is required.
+ To instantiate one, a <a href="apidocs/org/apache/commons/jexl3/JexlBuilder.html">JexlBuilder</a>
+ is needed to describe the allowed <a href="apidocs/org/apache/commons/jexl3/introspection/JexlPermissions.html">JexlPermissions</a>
+ and <a href="apidocs/org/apache/commons/jexl3/JexlFeatures.html">JexlFeatures</a> that will determine
+ which classes and methods scripts can access and call and which syntactic elements
+ scripts are allowed to use. Do not overlook this configuration aspect,
+ especially the permissions since <strong>security of your application</strong> might depend on it.
+ Once built, the JEXL engine should be stored, shared and reused.
+ It is thread-safe ; so are the scripts during evaluation.
+ </p>
<p>
When evaluating expressions, JEXL merges an
<a href="apidocs/org/apache/commons/jexl3/JexlExpression.html">JexlExpression</a>
@@ -102,34 +105,122 @@ working with enterprise platforms.
<a href="apidocs/org/apache/commons/jexl3/JexlScript.html">JexlScript</a>
with a
<a href="apidocs/org/apache/commons/jexl3/JexlContext.html">JexlContext</a>.
- An Expression is created using
- <a href="apidocs/org/apache/commons/jexl3/JexlEngine.html#createExpression(java.lang.String)">JexlEngine#createExpression()</a>,
+ In its simplest form, a script is created using
+ <a href="apidocs/org/apache/commons/jexl3/JexlEngine.html#createScript(java.lang.String)">JexlEngine#createExpression()</a>,
passing a String containing valid JEXL syntax. A simple JexlContext can be created by instantiating a
<a href="apidocs/org/apache/commons/jexl3/MapContext.html">MapContext</a>;
a map of variables that will be internally wrapped can be optionally provided through its constructor.
- The following example, takes a variable named foo, and invokes the bar() method on the property innerFoo:
</p>
+ <p>
+ JEXL's intention is a tight integration with its hosting platform; the scripting syntax is very close
+ to JScript but leverages (potentially) any public class or method that Java exposes. How tight and how
+ rich this integration is up to you; deriving JEXL API classes - most notably JexlPermissions, JexlContext,
+ JexlArithmetic - are the means to that end.
+ </p>
+ <p>The following example illustrate these aspects. It uses a specific set of permissions to allow using
+ URI class and a tailored context to expose streams in a convenient manner.</p>
<source><![CDATA[
- // Create or retrieve an engine
- JexlEngine jexl = new JexlBuilder().create();
-
- // Create an expression
- String jexlExp = "foo.innerFoo.bar()";
- JexlExpression e = jexl.createExpression( jexlExp );
-
- // Create a context and add data
- JexlContext jc = new MapContext();
- jc.set("foo", new Foo() );
-
- // Now evaluate the expression, getting the result
- Object o = e.evaluate(jc);]]></source>
+
+/**
+ * A test around scripting streams.
+ */
+public class StreamTest {
+ /** Our engine instance. */
+ private final JexlEngine jexl;
+
+ public StreamTest() {
+ // Restricting features; no loops, no side effects
+ JexlFeatures features = new JexlFeatures()
+ .loops(false)
+ .sideEffectGlobal(false)
+ .sideEffect(false);
+ // Restricted permissions to a safe set but with URI allowed
+ JexlPermissions permissions = new ClassPermissions(java.net.URI.class);
+ // Create the engine
+ jexl = new JexlBuilder().permissions(permissions).create();
+ }
+
+ /**
+ * A MapContext that can operate on streams.
+ */
+ public static class StreamContext extends MapContext {
+ /**
+ * This allows using a JEXL lambda as a mapper.
+ * @param stream the stream
+ * @param mapper the lambda to use as mapper
+ * @return the mapped stream
+ */
+ public Stream<?> map(Stream<?> stream, final JexlScript mapper) {
+ return stream.map( x -> mapper.execute(this, x));
+ }
+
+ /**
+ * This allows using a JEXL lambda as a filter.
+ * @param stream the stream
+ * @param filter the lambda to use as filter
+ * @return the filtered stream
+ */
+ public Stream<?> filter(Stream<?> stream, final JexlScript filter) {
+ return stream.filter(x -> x =! null && TRUE.equals(filter.execute(this, x)));
+ }
+ }
+
+ @Test
+ public void testURIStream() throws Exception {
+ // let's assume a collection of uris need to be processed and transformed to be simplified ;
+ // we want only http/https ones, only the host part and forcing an https scheme
+ List<URI> uris = Arrays.asList(
+ URI.create("http://user@www.apache.org:8000?qry=true"),
+ URI.create("https://commons.apache.org/releases/prepare.html"),
+ URI.create("mailto:henrib@apache.org")
+ );
+ // Create the test control, the expected result of our script evaluation
+ List<?> control = uris.stream()
+ .map(uri -> uri.getScheme().startsWith("http")? "https://" + uri.getHost() : null)
+ .filter(x -> x != null)
+ .collect(Collectors.toList());
+ Assert.assertEquals(2, control.size());
+
+ // Create scripts:
+ // uri is the name of the variable used as parameter; the beans are exposed as properties
+ // note the starts-with operator =^
+ // note that uri is also used in the back-quoted string that performs variable interpolation
+ JexlScript mapper = jexl.createScript("uri.scheme =^ 'http'? `https://${uri.host}` : null", "uri");
+ // using the bang-bang / !! - JScript like - is the way to coerce to boolean in the filter
+ JexlScript transform = jexl.createScript(
+ "list.stream().map(mapper).filter(x -> !!x).collect(Collectors.toList())", "list");
+
+ // Execute scripts:
+ JexlContext sctxt = new StreamContext();
+ // expose the static methods of Collectors; java.util.* is allowed by permissions
+ sctxt.set("Collectors", Collectors.class);
+ // expose the mapper script as a global variable in the context
+ sctxt.set("mapper", mapper);
+
+ Object transformed = transform.execute(sctxt, uris);
+ Assert.assertTrue(transformed instanceof List<?>);
+ Assert.assertEquals(control, transformed);
+ }
+}
+ ]]></source>
</section>
<section name="Extensions to JSTL Expression Language">
<p>
- While JEXL is similar to the expression language defined in JSTL, it has improved
- upon the syntax in a few areas:
+ JEXL attempts to bring some of the lessons learned by the Velocity
+ community about expression languages in templating to a wider audience.
+ <a href="https://commons.apache.org/jelly">Commons Jelly</a> needed
+ Velocity-ish method access, it just had to have it.
+ </p>
+ <p>
+ It must be noted that JEXL is <strong>not</strong> a compatible implementation of EL as defined
+ in JSTL 1.1 (JSR-052) or JSP 2.0 (JSR-152). For a compatible implementation of
+ these specifications, see the <a href="https://commons.apache.org/el">Commons EL</a> project.
+ </p>
+ <p>
+ While JEXL 3.3 is now closer to JScript (without prototypes), its roots are the expression language defined in JSTL
+ and its has improved upon its syntax in a few areas:
</p>
<ul>
<li>Support for invocation of any accessible method (see example above).</li>
diff --git a/src/test/java/org/apache/commons/jexl3/ClassPermissions.java b/src/test/java/org/apache/commons/jexl3/ClassPermissions.java
new file mode 100644
index 00000000..252d1c9e
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/ClassPermissions.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import org.apache.commons.jexl3.introspection.JexlPermissions;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * An example of permission delegation that augments the RESTRICTED permission with an explicit
+ * set of classes.
+ * <p>Typical use case is to deny access to a package - and thus all its classes - but allow
+ * a few specific classes.</p>
+ */
+public class ClassPermissions extends JexlPermissions.Delegate {
+ /** The set of explicitly allowed classes, overriding the delegate permissions. */
+ private final Set<String> allowedClasses;
+
+ /**
+ * Creates permissions based on the RESTRICTED set but allowing an explicit set.
+ * @param allow the set of allowed classes
+ */
+ public ClassPermissions(Class... allow) {
+ this(JexlPermissions.RESTRICTED, allow != null
+ ? Arrays.asList(allow).stream().map(Class::getCanonicalName).collect(Collectors.toList())
+ : null);
+ }
+
+ /**
+ * Required for compose().
+ * @param delegate the base to delegate to
+ * @param allow the list of class canonical names
+ */
+ public ClassPermissions(JexlPermissions delegate, Collection<String> allow) {
+ super(delegate);
+ if (allow != null && !allow.isEmpty()) {
+ allowedClasses = new HashSet<>();
+ allow.forEach(c -> allowedClasses.add(c));
+ } else {
+ allowedClasses = Collections.emptySet();
+ }
+ }
+
+ private boolean isClassAllowed(Class<?> clazz) {
+ return allowedClasses.contains(clazz.getCanonicalName());
+ }
+
+ @Override
+ public boolean allow(Class<?> clazz) {
+ return (validate(clazz) && isClassAllowed(clazz)) || super.allow(clazz);
+ }
+
+ @Override
+ public boolean allow(Method method) {
+ return (validate(method) && isClassAllowed(method.getDeclaringClass())) || super.allow(method);
+ }
+
+ @Override
+ public JexlPermissions compose(String... src) {
+ return new ClassPermissions(base.compose(src), allowedClasses);
+ }
+}
diff --git a/src/test/java/org/apache/commons/jexl3/Issues300Test.java b/src/test/java/org/apache/commons/jexl3/Issues300Test.java
index 3ec1d6b6..e83dfa01 100644
--- a/src/test/java/org/apache/commons/jexl3/Issues300Test.java
+++ b/src/test/java/org/apache/commons/jexl3/Issues300Test.java
@@ -1262,18 +1262,16 @@ public class Issues300Test {
"if (m < 3) { --y }\n" +
"(y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;\n" +
"}";
- JexlEngine jexl = new JexlBuilder()
- .safe(false)
- .strict(true)
- .create();
- JexlScript script = jexl.createScript(src);
- Object r = script.execute(null, 2023, 3, 1);
- Assert.assertTrue(r instanceof Number);
- Number dow = (Number) r;
- Assert.assertEquals(3, dow.intValue());
- r = script.execute(null, 1969, 7, 20);
- Assert.assertTrue(r instanceof Number);
- dow = (Number) r;
- Assert.assertEquals(0, dow.intValue());
+ JexlEngine jexl = new JexlBuilder().create();
+ JexlScript script = jexl.createScript(src);
+ Object r = script.execute(null, 2023, 3, 1);
+ Assert.assertTrue(r instanceof Number);
+ Number dow = (Number) r;
+ Assert.assertEquals(3, dow.intValue());
+ r = script.execute(null, 1969, 7, 20);
+ Assert.assertTrue(r instanceof Number);
+ dow = (Number) r;
+ Assert.assertEquals(0, dow.intValue());
}
+
}
diff --git a/src/test/java/org/apache/commons/jexl3/examples/StreamTest.java b/src/test/java/org/apache/commons/jexl3/examples/StreamTest.java
new file mode 100644
index 00000000..e5fb7fe2
--- /dev/null
+++ b/src/test/java/org/apache/commons/jexl3/examples/StreamTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.examples;
+
+import org.apache.commons.jexl3.ClassPermissions;
+import org.apache.commons.jexl3.JexlArithmetic;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.JexlFeatures;
+import org.apache.commons.jexl3.JexlScript;
+import org.apache.commons.jexl3.MapContext;
+import org.apache.commons.jexl3.introspection.JexlPermissions;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.lang.Boolean.TRUE;
+
+/**
+ * A test around scripting streams.
+ */
+public class StreamTest {
+ /** Our engine instance. */
+ private final JexlEngine jexl;
+
+ public StreamTest() {
+ // Restricting features; no loops, no side effects
+ JexlFeatures features = new JexlFeatures()
+ .loops(false)
+ .sideEffectGlobal(false)
+ .sideEffect(false);
+ // Restricted permissions to a safe set but with URI allowed
+ JexlPermissions permissions = new ClassPermissions(java.net.URI.class);
+ // Create the engine
+ jexl = new JexlBuilder().permissions(permissions).create();
+ }
+
+ /**
+ * A MapContext that can operate on streams.
+ */
+ public static class StreamContext extends MapContext {
+ /**
+ * This allows using a JEXL lambda as a mapper.
+ * @param stream the stream
+ * @param mapper the lambda to use as mapper
+ * @return the mapped stream
+ */
+ public Stream<?> map(Stream<?> stream, final JexlScript mapper) {
+ return stream.map( x -> mapper.execute(this, x));
+ }
+
+ /**
+ * This allows using a JEXL lambda as a filter.
+ * @param stream the stream
+ * @param filter the lambda to use as filter
+ * @return the filtered stream
+ */
+ public Stream<?> filter(Stream<?> stream, final JexlScript filter) {
+ return stream.filter(x -> x != null && TRUE.equals(filter.execute(this, x)));
+ }
+ }
+
+ @Test
+ public void testURIStream() throws Exception {
+ // let's assume a collection of uris need to be processed and transformed to be simplified ;
+ // we want only http/https ones, only the host part and using an https scheme
+ List<URI> uris = Arrays.asList(
+ URI.create("http://user@www.apache.org:8000?qry=true"),
+ URI.create("https://commons.apache.org/releases/prepare.html"),
+ URI.create("mailto:henrib@apache.org")
+ );
+ // Create the test control, the expected result of our script evaluation
+ List<?> control = uris.stream()
+ .map(uri -> uri.getScheme().startsWith("http")? "https://" + uri.getHost() : null)
+ .filter(x -> x != null)
+ .collect(Collectors.toList());
+ Assert.assertEquals(2, control.size());
+
+ // Create scripts:
+ // uri is the name of the variable used as parameter; the beans are exposed as properties
+ // note that it is also used in the backquoted string
+ JexlScript mapper = jexl.createScript("uri.scheme =^ 'http'? `https://${uri.host}` : null", "uri");
+ // using the bang-bang / !! - JScript like - is the way to coerce to boolean in the filter
+ JexlScript transform = jexl.createScript(
+ "list.stream().map(mapper).filter(x -> !!x).collect(Collectors.toList())", "list");
+
+ // Execute scripts:
+ JexlContext sctxt = new StreamContext();
+ // expose the static methods of Collectors; java.util.* is allowed by permissions
+ sctxt.set("Collectors", Collectors.class);
+ // expose the mapper script as a global variable in the context
+ sctxt.set("mapper", mapper);
+
+ Object transformed = transform.execute(sctxt, uris);
+ Assert.assertTrue(transformed instanceof List<?>);
+ Assert.assertEquals(control, transformed);
+ }
+}
diff --git a/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java b/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java
index b8c74c9e..152deda7 100644
--- a/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java
+++ b/src/test/java/org/apache/commons/jexl3/jexl342/OptionalTest.java
@@ -57,27 +57,39 @@ public class OptionalTest {
return c.stream().map(a->s.execute(context, a));
}
public Object reduce(Stream<Object> stream, JexlScript script) {
- return stream.reduce((identity, element)->{
+ Object reduced = stream.reduce((identity, element)->{
JexlContext context = JexlEngine.getThreadContext();
return script.execute(context, identity, element);
});
+ return reduced instanceof Optional<?>
+ ? ((Optional<?>) reduced).get()
+ : reduced;
}
}
@Test
- public void testStream() {
- String src = "[1, 2, 3, ...].map(x -> x * x).reduce((acc, x)->acc + x)";
+ public void testStream0() {
+ String src = "$0.map(x -> x * x).reduce((a, x) -> a + x)";
JexlBuilder builder = new JexlBuilder();
JexlUberspect uber = builder.create().getUberspect();
JexlArithmetic jexla = new OptionalArithmetic(true);
JexlEngine jexl = builder.uberspect(new ReferenceUberspect(uber)).arithmetic(jexla).safe(false).create();
JexlInfo info = new JexlInfo("testStream", 1, 1);
MapContext context = new StreamContext();
- JexlScript script = jexl.createScript(src, "list");
+ JexlScript script = jexl.createScript(src, "$0");
Object result = script.execute(context, Arrays.asList(1, 2, 3));
Assert.assertEquals(14, result);
- //Optional<?> result = (Optional<?>) script.execute(context, Arrays.asList(1, 2, 3));
- //Assert.assertEquals(14, result.get());
+ }
+
+ @Test
+ public void testStream1() {
+ String src = "$0.map(x -> x * x).reduce((a, x) -> a + x)";
+ JexlEngine jexl = new JexlBuilder().safe(false).create();
+ JexlInfo info = new JexlInfo("testStream", 1, 1);
+ MapContext context = new StreamContext();
+ JexlScript script = jexl.createScript(src, "$0");
+ Object result = script.execute(context, Arrays.asList(1, 2d, "3"));
+ Assert.assertEquals(14.0d, (double) result , 0.00001d);
}
@Test