Apache Commons JEXL 3.6.0 API
Apache Commons JEXL
Introduction
JEXL is a library intended to facilitate the implementation of dynamic and scripting features in applications and frameworks written in Java.
JEXL implements an Expression Language based on some extensions to the JSTL Expression Language supporting most of the constructs seen in shell-script or ECMAScript.
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.
The library exposes a small footprint API - the core features fit in 3 classes and 10 methods - that can be used in various conditions:
- Scripting features:
- Your application lets (advanced) users evaluate or define some simple expressions like computation formulas.
- Module or component configuration:
- Your application has configuration files (eventually generated by a design module) consumed by the end-user module that would benefit from variables and expressions.
- When it would be convenient to use IOC but overall complexity doesn't require (or can't depend upon) a full-blown library (Spring, Guice...).
- Loose-coupling of interfaces and implementations or duck-typing:
- You have optional classes that your code cant consider as compilation dependencies.
- You have to integrate and call "legacy" code or use components that you don't want to strongly depend upon.
- Simple template capabilities:
- Your application has basic template requirements and JSPs or Velocity would be overkill or too inconvenient to deploy.
JEXL name stands for Java EXpression Language, a simple expression language originally inspired by Apache Velocity and the Expression Language defined in the JavaServer Pages Standard Tag Library version 1.1 (JSTL) and JavaServer Pages version 2.0 (JSP). JEXL 2.0 added features inspired by Unified EL. The syntax is now close to a mix of ECMAScript and "shell-script" making it easy to master by technical operatives or consultants. The objects exposed and their behavior obviously need to be documented though...
The API and the expression language exploit Java-beans naming patterns through introspection to expose property getters and setters. It also considers public class fields as properties and allows to invoke any accessible method.
A Detailed Example
StreamTest.java
package org.apache.commons.jexl3.examples;
import static java.lang.Boolean.TRUE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.apache.commons.jexl3.introspection.JexlPermissions.ClassPermissions;
import org.junit.jupiter.api.Test;
/**
* A test around scripting streams.
*/
class StreamTest {
/**
* A MapContext that can operate on streams and collections.
*/
public static class CollectionContext extends MapContext {
/**
* This allows using a JEXL lambda as a filter.
*
* @param collection the collection
* @param filter the lambda to use as filter
* @return the filtered result as a list
*/
public List<?> filter(final Collection<?> collection, final JexlScript filter) {
return collection.stream()
.filter(x -> x != null && TRUE.equals(filter.execute(this, x)))
.collect(Collectors.toList());
}
/**
* This allows using a JEXL lambda as a mapper.
*
* @param collection the collection
* @param mapper the lambda to use as mapper
* @return the mapped result as a list
*/
public List<?> map(final Collection<?> collection, final JexlScript mapper) {
return collection.stream()
.map(x -> mapper.execute(this, x))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
/**
* A MapContext that can operate on streams and collections.
*/
public static class StreamContext extends MapContext {
/**
* 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(final Stream<?> stream, final JexlScript filter) {
return stream.filter(x -> x != null && TRUE.equals(filter.execute(this, x)));
}
/**
* 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(final Stream<?> stream, final JexlScript mapper) {
return stream.map(x -> mapper.execute(this, x));
}
}
/** Our engine instance. */
private final JexlEngine jexl;
public StreamTest() {
// Restricting features; no loops, no side effects
final JexlFeatures features = new JexlFeatures()
.loops(false)
.sideEffectGlobal(false)
.sideEffect(false);
// Restricted permissions to a safe set but with URI allowed
final JexlPermissions permissions = new ClassPermissions(java.net.URI.class);
// Create the engine
jexl = new JexlBuilder()
.features(features)
.permissions(permissions)
.namespaces(Collections.singletonMap("URI", java.net.URI.class))
.create();
}
@Test
void testURICollection() {
// A collection map/filter aware context
final JexlContext sctxt = new CollectionContext();
// Some uris
final 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")
);
// filter, all results schemes start with 'http'
final JexlScript filter = jexl.createScript(
"list.filter(uri -> uri.scheme =^ 'http')",
"list");
final Object filtered = filter.execute(sctxt, uris);
assertInstanceOf(List.class, filtered);
List<URI> result = (List<URI>) filtered;
assertEquals(2, result.size());
for(final URI uri : result) {
assertTrue(uri.getScheme().startsWith("http"));
}
// map, all results scheme now 'https'
final JexlScript mapper = jexl.createScript(
"list.map(uri -> uri.scheme =^ 'http'? URI:create(`https://${uri.host}`) : null)",
"list");
final Object transformed = mapper.execute(sctxt, uris);
assertInstanceOf(List.class, transformed);
result = (List<URI>) transformed;
assertEquals(2, result.size());
for (final URI uri : result) {
assertEquals("https", uri.getScheme());
}
}
@Test
void testURIStream() {
// 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 a https scheme
final 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
final List<?> control = uris.stream()
.map(uri -> uri.getScheme().startsWith("http")? "https://" + uri.getHost() : null)
.filter(Objects::nonNull)
.collect(Collectors.toList());
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
final 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
final JexlScript transform = jexl.createScript(
"list.stream().map(mapper).filter(x -> !!x).collect(Collectors.toList())", "list");
// Execute scripts:
final 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);
final Object transformed = transform.execute(sctxt, uris);
assertInstanceOf(List.class, transformed);
assertEquals(control, transformed);
}
}
Extensions to JSTL Expression Language
JEXL attempts to bring some of the lessons learned by the Velocity community about expression languages in templating to a wider audience. Commons Jelly needed Velocity-ish method access, it just had to have it.
It must be noted that JEXL is not 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 Commons EL project.
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:
- Support for invocation of any accessible method (see example above).
- Support for setting/getting any accessible public field.
- A general new() method allowing to instantiate objects.
- A general size() method, which works on:
- String - returns length
- Map - returns number of keys
- List - returns number of elements.
- A general empty() method, which works on Collections and Strings.
- Support for the ternary operator 'a ? b : c' - and its GNU-C / "Elvis" variant 'a ?: c'.
- Support for the Perl-like regex matching operators '=~' and '!~'
- Support for the CSS3-inspired 'startsWith' and 'endsWith' operators '=^' and '=$'
- Support for user-defined functions.
- Misc : '+' has been overloaded to be use as a String concatenation operator
Related Resources
JEXL is not a product of the Java Community Process (JCP), but it provides a similar expression syntax. For more information about JSP 2.0 EL and JSTL 1.1 EL:
- JSP 2.0 is covered by Java Specification Requests (JSR) JSR-152: JavaServer Pages 2.0 Specification.
- Apache has an implementation of the expression language for JSP 2.0, called EL
- JSTL 1.1 is covered by JSR 52: A Standard Tag Library for JavaServer Pages. See the JSTL API.
- Apache has a JSTL Implementation.
Velocity
Apache Velocity implements a similar expression language.
In particular the References section of the User Guide has some good information on properties and method which correlate directly to JEXL.
Projects Using Commons JEXL
Requirements
- Java 8 or above.
- If using OSGi, R7 or above.