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