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