View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.examples;
18  
19  import static java.lang.Boolean.TRUE;
20  import static org.apache.commons.jexl3.JexlTestCase.TEST_PERMS;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.lang.reflect.Method;
26  import java.net.URI;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.List;
31  import java.util.Objects;
32  import java.util.stream.Collectors;
33  import java.util.stream.Stream;
34  
35  import org.apache.commons.jexl3.JexlBuilder;
36  import org.apache.commons.jexl3.JexlContext;
37  import org.apache.commons.jexl3.JexlEngine;
38  import org.apache.commons.jexl3.JexlFeatures;
39  import org.apache.commons.jexl3.JexlScript;
40  import org.apache.commons.jexl3.MapContext;
41  import org.apache.commons.jexl3.introspection.JexlPermissions;
42  import org.apache.commons.jexl3.introspection.JexlPermissions.ClassPermissions;
43  import org.junit.jupiter.api.Assertions;
44  import org.junit.jupiter.api.BeforeAll;
45  import org.junit.jupiter.api.Test;
46  
47  /**
48   * A test around scripting streams.
49   */
50  public class StreamTest {
51      @BeforeAll
52      static void setUpClass() {
53          JexlBuilder.setDefaultPermissions(TEST_PERMS);
54      }
55  
56      /**
57       * A MapContext that can operate on streams and collections.
58       */
59      public static class CollectionContext extends MapContext {
60  
61          /**
62           * This allows using a JEXL lambda as a filter.
63           *
64           * @param collection The collection
65           * @param filter The lambda to use as filter
66           * @return The filtered result as a list
67           */
68          public List<?> filter(final Collection<?> collection, final JexlScript filter) {
69              return collection.stream()
70                  .filter(x -> x != null && TRUE.equals(filter.execute(this, x)))
71                  .collect(Collectors.toList());
72          }
73  
74          /**
75           * This allows using a JEXL lambda as a mapper.
76           *
77           * @param collection The collection
78           * @param mapper The lambda to use as mapper
79           * @return The mapped result as a list
80           */
81          public List<?> map(final Collection<?> collection, final JexlScript mapper) {
82              return collection.stream()
83                  .map(x -> mapper.execute(this, x))
84                  .filter(Objects::nonNull)
85                  .collect(Collectors.toList());
86          }
87      }
88  
89      /**
90       * A MapContext that can operate on streams and collections.
91       */
92      public static class StreamContext extends MapContext {
93  
94          /**
95           * This allows using a JEXL lambda as a filter.
96           *
97           * @param stream The stream
98           * @param filter The lambda to use as filter
99           * @return The filtered stream
100          */
101         public Stream<?> filter(final Stream<?> stream, final JexlScript filter) {
102             return stream.filter(x -> x != null && TRUE.equals(filter.execute(this, x)));
103         }
104 
105         /**
106          * This allows using a JEXL lambda as a mapper.
107          *
108          * @param stream The stream
109          * @param mapper The lambda to use as mapper
110          * @return The mapped stream
111          */
112         public Stream<?> map(final Stream<?> stream, final JexlScript mapper) {
113             return stream.map(x -> mapper.execute(this, x));
114         }
115     }
116 
117     /** Our engine instance. */
118     private final JexlEngine jexl;
119     private final JexlPermissions jexlPermissions;
120 
121     public StreamTest() throws Exception {
122         // Restricting features; no loops, no side effects
123         final JexlFeatures features = new JexlFeatures()
124             .loops(false)
125             .sideEffectGlobal(false)
126             .sideEffect(false);
127         // Restricted permissions to a safe set but with URI allowed
128         jexlPermissions = new ClassPermissions(java.net.URI.class, StreamContext.class, CollectionContext.class);
129        // Create the engine
130         jexl = new JexlBuilder()
131             .features(features)
132             .permissions(jexlPermissions)
133             .namespaces(Collections.singletonMap("URI", java.net.URI.class))
134             .create();
135     }
136 
137     @Test
138     void testURICollection() throws Exception {
139        Assertions.assertTrue(jexlPermissions.allow(java.net.URI.class));
140        Assertions.assertTrue(jexlPermissions.allow(java.util.stream.Stream.class));
141        for (final Method m : Arrays.stream(Stream.class.getMethods()).filter(m -> m.getName().equals("filter")).collect(Collectors.toSet())) {
142             Assertions.assertTrue(jexlPermissions.allow(m));
143         }
144         // A collection map/filter aware context
145         final JexlContext sctxt = new CollectionContext();
146         // Some uris
147         final List<URI> uris = Arrays.asList(
148             URI.create("http://user@www.apache.org:8000?qry=true"),
149             URI.create("https://commons.apache.org/releases/prepare.html"),
150             URI.create("mailto:henrib@apache.org")
151         );
152 
153         // filter, all results schemes start with 'http'
154         final JexlScript filter = jexl.createScript(
155             "list.filter(uri -> uri.scheme =^ 'http')",
156             "list");
157         final Object filtered = filter.execute(sctxt, uris);
158         assertInstanceOf(List.class, filtered);
159         List<URI> result = (List<URI>) filtered;
160         assertEquals(2, result.size());
161         for(final URI uri : result) {
162             assertTrue(uri.getScheme().startsWith("http"));
163         }
164 
165         // map, all results scheme now 'https'
166         final JexlScript mapper = jexl.createScript(
167             "list.map(uri -> uri.scheme =^ 'http'? URI:create(`https://${uri.host}`) : null)",
168             "list");
169         final Object transformed = mapper.execute(sctxt, uris);
170         assertInstanceOf(List.class, transformed);
171         result = (List<URI>) transformed;
172         assertEquals(2, result.size());
173         for (final URI uri : result) {
174             assertEquals("https", uri.getScheme());
175         }
176     }
177 
178     @Test
179     void testURIStream() throws Exception {
180         Assertions.assertTrue(jexlPermissions.allow(StreamContext.class.getMethod("filter", Stream.class, JexlScript.class)));
181 
182         // Assume a collection of uris need to be processed and transformed to be simplified ;
183         // we want only http/https ones, only the host part and using a https scheme
184         final List<URI> uris = Arrays.asList(
185                 URI.create("http://user@www.apache.org:8000?qry=true"),
186                 URI.create("https://commons.apache.org/releases/prepare.html"),
187                 URI.create("mailto:henrib@apache.org")
188         );
189         // Create the test control, the expected result of our script evaluation
190         final List<?> control =  uris.stream()
191                 .map(uri -> uri.getScheme().startsWith("http")? "https://" + uri.getHost() : null)
192                 .filter(Objects::nonNull)
193                 .collect(Collectors.toList());
194         assertEquals(2, control.size());
195 
196         // Create scripts:
197         // uri is the name of the variable used as parameter; the beans are exposed as properties
198         // note that it is also used in the backquoted string
199         final JexlScript mapper = jexl.createScript("uri.scheme =^ 'http'? `https://${uri.host}` : null", "uri");
200         // using the bang-bang / !! - JScript like -  is the way to coerce to boolean in the filter
201         final JexlScript transform = jexl.createScript(
202                 "list.stream().map(mapper).filter(x -> !!x).collect(Collectors.toList())", "list");
203 
204         // Execute scripts:
205         final JexlContext sctxt = new StreamContext();
206         // expose the static methods of Collectors; java.util.* is allowed by permissions
207         sctxt.set("Collectors", Collectors.class);
208         // expose the mapper script as a global variable in the context
209         sctxt.set("mapper", mapper);
210 
211         final Object transformed = transform.execute(sctxt, uris);
212         assertInstanceOf(List.class, transformed);
213         assertEquals(control, transformed);
214     }
215 }