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