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;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotNull;
21  import static org.junit.jupiter.api.Assertions.assertNotSame;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertSame;
24  
25  import java.io.File;
26  import java.lang.ref.Reference;
27  import java.lang.ref.ReferenceQueue;
28  import java.lang.ref.SoftReference;
29  import java.lang.ref.WeakReference;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  
35  import org.apache.commons.lang3.SystemProperties;
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.junit.jupiter.api.AfterEach;
39  import org.junit.jupiter.api.BeforeEach;
40  import org.junit.jupiter.api.Test;
41  
42  /**
43   * Basic check on automated class creation
44   */
45  @SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
46  class ClassCreatorTest extends JexlTestCase {
47      public static class BigObject {
48          @SuppressWarnings("unused")
49          private final byte[] space = new byte[MEGA];
50          private final int id;
51  
52          public BigObject(final int id) {
53              this.id = id;
54          }
55  
56          public int getId() {
57              return id;
58          }
59      }
60      // A weak reference on class
61      static final class ClassReference extends WeakReference<Class<?>> {
62          ClassReference(final Class<?> clazz, final ReferenceQueue<Object> queue) {
63              super(clazz, queue);
64          }
65      }
66      public static class ContextualCtor {
67          int value = -1;
68  
69          public ContextualCtor(final JexlContext ctxt) {
70              value = (Integer) ctxt.get("value");
71          }
72  
73          public ContextualCtor(final JexlContext ctxt, final int v) {
74              value = (Integer) ctxt.get("value") + v;
75          }
76  
77          public int getValue() {
78              return value;
79          }
80      }
81      // A soft reference on instance
82      static final class InstanceReference extends SoftReference<Object> {
83          InstanceReference(final Object obj, final ReferenceQueue<Object> queue) {
84              super(obj, queue);
85          }
86      }
87  
88      public static class NsTest implements JexlContext.NamespaceFunctor {
89          private final String className;
90  
91          public NsTest(final String cls) {
92              className = cls;
93          }
94          @Override
95          public Object createFunctor(final JexlContext context) {
96              final JexlEngine jexl = JexlEngine.getThreadEngine();
97              return jexl.newInstance(className, context);
98          }
99  
100     }
101 
102     public static class TwoCtors {
103         int value;
104 
105         public TwoCtors(final int v) {
106             this.value = v;
107         }
108 
109         public TwoCtors(final Number x) {
110             this.value = -x.intValue();
111         }
112 
113         public int getValue() {
114             return value;
115         }
116     }
117 
118     static final Log logger = LogFactory.getLog(JexlTestCase.class);
119 
120     static final int LOOPS = 8;
121 
122     // A space hog class
123     static final int MEGA = 1024 * 1024;
124 
125     private File base;
126 
127     private JexlEngine jexl;
128 
129     public ClassCreatorTest() {
130         super("ClassCreatorTest");
131     }
132 
133     private void deleteDirectory(final File dir) {
134         if (dir.isDirectory()) {
135             for (final File file : dir.listFiles()) {
136                 if (file.isFile()) {
137                     file.delete();
138                 }
139             }
140         }
141         dir.delete();
142     }
143 
144     void functorTwo(final Object nstest) throws Exception {
145         // create jexl2 with a 'test' namespace
146         final Map<String, Object> ns = new HashMap<>();
147         ns.put("test", nstest);
148         final JexlEngine jexl2 = new JexlBuilder().namespaces(ns).create();
149         final JexlContext ctxt = new MapContext();
150         ctxt.set("value", 1000);
151 
152         // inject 'foo2' as test namespace functor class
153         final ClassCreator cctor = new ClassCreator(jexl, base);
154         cctor.setSeed(2);
155         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
156         Class<?> foo1 = cctor.createClass(true);
157         assertSame(foo1.getClassLoader(), cctor.getClassLoader());
158         assertEquals("foo2", foo1.getSimpleName());
159         Object result = cctor.newInstance(foo1, ctxt);
160         assertEquals(foo1, result.getClass());
161         jexl2.setClassLoader(cctor.getClassLoader());
162         cctor.clear();
163 
164         // check the namespace functor behavior
165         final JexlScript script = jexl2.createScript("test:getValue()");
166         result = script.execute(ctxt, foo1.getName());
167         assertEquals(1010, result);
168 
169         // change the body
170         cctor.setSeed(2);
171         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
172         final Class<?> foo11 = cctor.createClass(true);
173         assertEquals("foo2", foo1.getSimpleName());
174         assertNotSame(foo11, foo1);
175         foo1 = foo11;
176         result = cctor .newInstance(foo1, ctxt);
177         assertEquals(foo1, result.getClass());
178         // drum rolll....
179         jexl2.setClassLoader(foo1.getClassLoader());
180         result = script.execute(ctxt, foo1.getName());
181         // tada!
182         assertEquals(1099, result);
183     }
184 
185     @BeforeEach
186     @Override
187     public void setUp() throws Exception {
188         base = new File(SystemProperties.getJavaIoTmpdir(), "jexl" + System.currentTimeMillis());
189         jexl = JEXL;
190 
191     }
192 
193     @AfterEach
194     @Override
195     public void tearDown() {
196         deleteDirectory(base);
197     }
198 
199     @Test
200     void testBasicCtor() {
201         final JexlScript s = jexl.createScript("(c, v)->{ var ct2 = new(c, v); ct2.value; }");
202         Object r = s.execute(null, TwoCtors.class, 10);
203         assertEquals(10, r);
204         r = s.execute(null, TwoCtors.class, 5 + 5);
205         assertEquals(10, r);
206         r = s.execute(null, TwoCtors.class, 10d);
207         assertEquals(-10, r);
208         r = s.execute(null, TwoCtors.class, 100f);
209         assertEquals(-100, r);
210     }
211 
212     @Test
213     void testContextualCtor() {
214         final MapContext ctxt = new MapContext();
215         ctxt.set("value", 42);
216         JexlScript s = jexl.createScript("(c)->{ new(c).value }");
217         Object r = s.execute(ctxt, ContextualCtor.class);
218         assertEquals(42, r);
219         s = jexl.createScript("(c, v)->{ new(c, v).value }");
220         r = s.execute(ctxt, ContextualCtor.class, 100);
221         assertEquals(142, r);
222     }
223 
224     @Test
225     void testFunctor2Class() throws Exception {
226         functorTwo(new NsTest(ClassCreator.GEN_CLASS + "foo2"));
227     }
228 
229     @Test
230     void testFunctor2Name() throws Exception {
231         functorTwo(ClassCreator.GEN_CLASS + "foo2");
232     }
233 
234     @Test
235     void testFunctorOne() throws Exception {
236         final JexlContext ctxt = new MapContext();
237         ctxt.set("value", 1000);
238 
239         // create a class foo1 with a ctor whose body gets a value
240         // from the context to initialize its value
241         final ClassCreator cctor = new ClassCreator(jexl, base);
242         cctor.setSeed(1);
243         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
244         Class<?> foo1 = cctor.createClass(true);
245         assertSame(foo1.getClassLoader(), cctor.getClassLoader());
246         assertEquals("foo1", foo1.getSimpleName());
247         Object result = cctor.newInstance(foo1, ctxt);
248         assertEquals(foo1, result.getClass());
249         jexl.setClassLoader(cctor.getClassLoader());
250         cctor.clear();
251 
252         // check we can invoke that ctor using its name or class
253         final JexlScript script = jexl.createScript("(c)->{ new(c).value; }");
254         result = script.execute(ctxt, foo1);
255         assertEquals(1010, result);
256         result = script.execute(ctxt, foo1.getName());
257         assertEquals(1010, result);
258 
259         // re-create foo1 with a different body!
260         cctor.setSeed(1);
261         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
262         final Class<?> foo11 = cctor.createClass(true);
263         assertEquals("foo1", foo1.getSimpleName());
264         assertNotSame(foo11, foo1);
265         foo1 = foo11;
266         result = cctor.newInstance(foo1, ctxt);
267         assertEquals(foo1, result.getClass());
268         // drum rolll....
269         jexl.setClassLoader(foo1.getClassLoader());
270         result = script.execute(ctxt, foo1.getName());
271         // tada!
272         assertEquals(1099, result);
273         result = script.execute(ctxt, foo1);
274         assertEquals(1099, result);
275     }
276 
277     @Test
278     void testFunctorThree() throws Exception {
279         final JexlContext ctxt = new MapContext();
280         ctxt.set("value", 1000);
281 
282         final ClassCreator cctor = new ClassCreator(jexl, base);
283         cctor.setSeed(2);
284         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
285         Class<?> foo1 = cctor.createClass(true);
286         assertSame(foo1.getClassLoader(), cctor.getClassLoader());
287         assertEquals("foo2", foo1.getSimpleName());
288         Object result = cctor.newInstance(foo1, ctxt);
289         assertEquals(foo1, result.getClass());
290         jexl.setClassLoader(cctor.getClassLoader());
291         cctor.clear();
292 
293         final Map<String, Object> ns = new HashMap<>();
294         ns.put("test", foo1);
295         final JexlEngine jexl2 = new JexlBuilder().namespaces(ns).create();
296 
297         final JexlScript script = jexl2.createScript("test:getValue()");
298         result = script.execute(ctxt, foo1.getName());
299         assertEquals(1010, result);
300 
301         cctor.setSeed(2);
302         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
303         final Class<?> foo11 = cctor.createClass(true);
304         assertEquals("foo2", foo1.getSimpleName());
305         assertNotSame(foo11, foo1);
306         foo1 = foo11;
307         result = cctor.newInstance(foo1, ctxt);
308         assertEquals(foo1, result.getClass());
309         // drum rolll....
310         jexl2.setClassLoader(foo1.getClassLoader());
311         result = script.execute(ctxt, foo1.getName());
312         // tada!
313         assertEquals(1099, result);
314     }
315 
316     @Test
317     void testMany() throws Exception {
318         // abort test if class creator cannot run
319         if (!ClassCreator.canRun) {
320             return;
321         }
322         int pass = 0;
323         int gced = -1;
324         final ReferenceQueue<Object> queue = new ReferenceQueue<>();
325         final List<Reference<?>> stuff = new ArrayList<>();
326         // keeping a reference on methods prevent classes from being GCed
327 //        List<Object> mm = new ArrayList<Object>();
328         final JexlExpression expr = jexl.createExpression("foo.value");
329         final JexlExpression newx = jexl.createExpression("foo = new(clazz)");
330         final JexlEvalContext context = new JexlEvalContext();
331         final JexlOptions options = context.getEngineOptions();
332         options.setStrict(false);
333         options.setSilent(true);
334 
335         final ClassCreator cctor = new ClassCreator(jexl, base);
336         for (int i = 0; i < LOOPS && gced < 0; ++i) {
337             cctor.setSeed(i);
338             Class<?> clazz;
339             if (pass == 0) {
340                 clazz = cctor.createClass();
341             } else {
342                 clazz = cctor.getClassInstance();
343                 if (clazz == null) {
344                     assertEquals(i, gced);
345                     break;
346                 }
347             }
348             // this code verifies the assumption that holding a strong reference to a method prevents
349             // its owning class from being GCed
350 //          Method m = clazz.getDeclaredMethod("getValue", new Class<?>[0]);
351 //          mm.add(m);
352             // we should not be able to create foox since it is unknown to the JEXL classloader
353             context.set("clazz", cctor.getClassName());
354             context.set("foo", null);
355             Object z = newx.evaluate(context);
356             assertNull(z);
357             // check with the class itself
358             context.set("clazz", clazz);
359             z = newx.evaluate(context);
360             assertNotNull(z, clazz + ": class " + i + " could not be instantiated on pass " + pass);
361             assertEquals(Integer.valueOf(i), expr.evaluate(context));
362             // with the proper class loader, attempt to create an instance from the class name
363             jexl.setClassLoader(cctor.getClassLoader());
364             z = newx.evaluate(context);
365             assertEquals(z.getClass(), clazz);
366             assertEquals(Integer.valueOf(i), expr.evaluate(context));
367             cctor.clear();
368             jexl.setClassLoader(null);
369 
370             // on pass 0, attempt to force GC to run and collect generated classes
371             if (pass == 0) {
372                 // add a weak reference on the class
373                 stuff.add(new ClassReference(clazz, queue));
374                 // add a soft reference on an instance
375                 stuff.add(new InstanceReference(clazz.getConstructor().newInstance(), queue));
376 
377                 // attempt to force GC:
378                 // while we still have a MB free, create & store big objects
379                 for (int b = 0; b < 1024 && Runtime.getRuntime().freeMemory() > MEGA; ++b) {
380                     final BigObject big = new BigObject(b);
381                     stuff.add(new InstanceReference(big, queue));
382                 }
383                 // hint it...
384                 System.gc();
385                 // let's see if some weak refs got collected
386                 boolean qr = false;
387                 while (queue.poll() != null) {
388                     final Reference<?> ref = queue.remove(1);
389                     if (ref instanceof ClassReference) {
390                         gced = i;
391                         qr = true;
392                     }
393                 }
394                 if (qr) {
395                     //logger.warn("may have GCed class around " + i);
396                     pass = 1;
397                     i = 0;
398                 }
399             }
400         }
401 
402         if (gced < 0) {
403             logger.warn("unable to force GC");
404             //assertTrue(gced > 0);
405         }
406     }
407 
408     @Test
409     void testOne() throws Exception {
410         // abort test if class creator cannot run
411         if (!ClassCreator.canRun) {
412             logger.warn("unable to create classes");
413             return;
414         }
415         final ClassCreator cctor = new ClassCreator(jexl, base);
416         cctor.setSeed(1);
417         final Class<?> foo1 = cctor.createClass();
418         assertEquals("foo1", foo1.getSimpleName());
419         cctor.clear();
420     }
421     @Test
422     void test432() throws Exception {
423         final ClassCreator cctor = new ClassCreator(jexl, base);
424         cctor.setSeed(2);
425         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
426         Class<?> foo1 = cctor.createClass(true);
427         assertSame(foo1.getClassLoader(), cctor.getClassLoader());
428         assertEquals("foo2", foo1.getSimpleName());
429         final Map<String, Object> ns = new HashMap<>();
430         ns.put("test", foo1.getName());
431         // use cache
432         final JexlEngine jexl2 = new JexlBuilder().namespaces(ns).cache(16).create();
433         jexl2.setClassLoader(cctor.getClassLoader());
434         cctor.clear();
435         final JexlContext ctxt = new MapContext();
436         ctxt.set("value", 1000);
437         final JexlScript script = jexl2.createScript("test:getValue()");
438         Object result = script.execute(ctxt);
439         assertEquals(1010, result);        cctor.setSeed(2);
440         cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
441         final Class<?> foo11 = cctor.createClass(true);
442         assertEquals("foo2", foo1.getSimpleName());
443         assertNotSame(foo11, foo1);
444         foo1 = foo11;
445         // drum rolll....
446         jexl2.setClassLoader(foo1.getClassLoader());
447         result = script.execute(ctxt);
448         // tada!
449         assertEquals(1099, result);
450     }
451 }