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