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    *      http://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.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
22  import static org.junit.jupiter.api.Assertions.assertTrue;
23  
24  import java.net.URI;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Objects;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  import org.apache.commons.jexl3.JexlBuilder;
34  import org.apache.commons.jexl3.JexlContext;
35  import org.apache.commons.jexl3.JexlEngine;
36  import org.apache.commons.jexl3.JexlFeatures;
37  import org.apache.commons.jexl3.JexlScript;
38  import org.apache.commons.jexl3.MapContext;
39  import org.apache.commons.jexl3.introspection.JexlPermissions;
40  import org.apache.commons.jexl3.introspection.JexlPermissions.ClassPermissions;
41  import org.junit.jupiter.api.Test;
42  
43  /**
44   * A test around scripting streams.
45   */
46  public class StreamTest {
47  
48      /**
49       * A MapContext that can operate on streams and collections.
50       */
51      public static class CollectionContext extends MapContext {
52          /**
53           * This allows using a JEXL lambda as a filter.
54           * @param collection the collection
55           * @param filter the lambda to use as filter
56           * @return the filtered result as a list
57           */
58          public List<?> filter(final Collection<?> collection, final JexlScript filter) {
59              return collection.stream()
60                  .filter(x -> x != null && TRUE.equals(filter.execute(this, x)))
61                  .collect(Collectors.toList());
62          }
63  
64          /**
65           * This allows using a JEXL lambda as a mapper.
66           * @param collection the collection
67           * @param mapper the lambda to use as mapper
68           * @return the mapped result as a list
69           */
70          public List<?> map(final Collection<?> collection, final JexlScript mapper) {
71              return collection.stream()
72                  .map(x -> mapper.execute(this, x))
73                  .filter(Objects::nonNull)
74                  .collect(Collectors.toList());
75          }
76      }
77  
78      /**
79       * A MapContext that can operate on streams and collections.
80       */
81      public static class StreamContext extends MapContext {
82          /**
83           * This allows using a JEXL lambda as a filter.
84           * @param stream the stream
85           * @param filter the lambda to use as filter
86           * @return the filtered stream
87           */
88          public Stream<?> filter(final Stream<?> stream, final JexlScript filter) {
89              return stream.filter(x -> x != null && TRUE.equals(filter.execute(this, x)));
90          }
91  
92          /**
93           * This allows using a JEXL lambda as a mapper.
94           * @param stream the stream
95           * @param mapper the lambda to use as mapper
96           * @return the mapped stream
97           */
98          public Stream<?> map(final Stream<?> stream, final JexlScript mapper) {
99              return stream.map(x -> mapper.execute(this, x));
100         }
101     }
102 
103     /** Our engine instance. */
104     private final JexlEngine jexl;
105 
106     public StreamTest() {
107         // Restricting features; no loops, no side effects
108         final JexlFeatures features = new JexlFeatures()
109                 .loops(false)
110                 .sideEffectGlobal(false)
111                 .sideEffect(false);
112         // Restricted permissions to a safe set but with URI allowed
113         final JexlPermissions permissions = new ClassPermissions(java.net.URI.class);
114         // Create the engine
115         jexl = new JexlBuilder()
116             .features(features)
117             .permissions(permissions)
118             .namespaces(Collections.singletonMap("URI", java.net.URI.class))
119             .create();
120     }
121 
122     @Test
123     public void testURICollection() {
124         // A collection map/filter aware context
125         final JexlContext sctxt = new CollectionContext();
126         // Some uris
127         final List<URI> uris = Arrays.asList(
128             URI.create("http://user@www.apache.org:8000?qry=true"),
129             URI.create("https://commons.apache.org/releases/prepare.html"),
130             URI.create("mailto:henrib@apache.org")
131         );
132 
133         // filter, all results schemes start with 'http'
134         final JexlScript filter = jexl.createScript(
135             "list.filter(uri -> uri.scheme =^ 'http')",
136             "list");
137         final Object filtered = filter.execute(sctxt, uris);
138         assertInstanceOf(List.class, filtered);
139         List<URI> result = (List<URI>) filtered;
140         assertEquals(2, result.size());
141         for(final URI uri : result) {
142             assertTrue(uri.getScheme().startsWith("http"));
143         }
144 
145         // map, all results scheme now 'https'
146         final JexlScript mapper = jexl.createScript(
147             "list.map(uri -> uri.scheme =^ 'http'? URI:create(`https://${uri.host}`) : null)",
148             "list");
149         final Object transformed = mapper.execute(sctxt, uris);
150         assertInstanceOf(List.class, transformed);
151         result = (List<URI>) transformed;
152         assertEquals(2, result.size());
153         for(final URI uri : result) {
154           assertEquals("https", uri.getScheme());
155         }
156     }
157 
158     @Test
159     public void testURIStream() {
160         // let's assume a collection of uris need to be processed and transformed to be simplified ;
161         // we want only http/https ones, only the host part and using a https scheme
162         final List<URI> uris = Arrays.asList(
163                 URI.create("http://user@www.apache.org:8000?qry=true"),
164                 URI.create("https://commons.apache.org/releases/prepare.html"),
165                 URI.create("mailto:henrib@apache.org")
166         );
167         // Create the test control, the expected result of our script evaluation
168         final List<?> control =  uris.stream()
169                 .map(uri -> uri.getScheme().startsWith("http")? "https://" + uri.getHost() : null)
170                 .filter(Objects::nonNull)
171                 .collect(Collectors.toList());
172         assertEquals(2, control.size());
173 
174         // Create scripts:
175         // uri is the name of the variable used as parameter; the beans are exposed as properties
176         // note that it is also used in the backquoted string
177         final JexlScript mapper = jexl.createScript("uri.scheme =^ 'http'? `https://${uri.host}` : null", "uri");
178         // using the bang-bang / !! - JScript like -  is the way to coerce to boolean in the filter
179         final JexlScript transform = jexl.createScript(
180                 "list.stream().map(mapper).filter(x -> !!x).collect(Collectors.toList())", "list");
181 
182         // Execute scripts:
183         final JexlContext sctxt = new StreamContext();
184         // expose the static methods of Collectors; java.util.* is allowed by permissions
185         sctxt.set("Collectors", Collectors.class);
186         // expose the mapper script as a global variable in the context
187         sctxt.set("mapper", mapper);
188 
189         final Object transformed = transform.execute(sctxt, uris);
190         assertInstanceOf(List.class, transformed);
191         assertEquals(control, transformed);
192     }
193 }