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 <hen...@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://u...@www.apache.org:8000?qry=true"), + URI.create("https://commons.apache.org/releases/prepare.html"), + URI.create("mailto:hen...@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://u...@www.apache.org:8000?qry=true"), + URI.create("https://commons.apache.org/releases/prepare.html"), + URI.create("mailto:hen...@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