blob: fe6cbae0e31f08bebf87fafd358baab5680bee44 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.jexl3;
import java.io.File;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* Basic check on automated class creation
*/
@SuppressWarnings({"UnnecessaryBoxing", "AssertEqualsBetweenInconvertibleTypes"})
public class ClassCreatorTest extends JexlTestCase {
static final Log logger = LogFactory.getLog(JexlTestCase.class);
static final int LOOPS = 8;
private File base = null;
private JexlEngine jexl = null;
public ClassCreatorTest() {
super("ClassCreatorTest");
}
@Before
@Override
public void setUp() throws Exception {
base = new File(System.getProperty("java.io.tmpdir"), "jexl" + System.currentTimeMillis());
jexl = JEXL;
}
@After
@Override
public void tearDown() throws Exception {
deleteDirectory(base);
}
private void deleteDirectory(final File dir) {
if (dir.isDirectory()) {
for (final File file : dir.listFiles()) {
if (file.isFile()) {
file.delete();
}
}
}
dir.delete();
}
// A space hog class
static final int MEGA = 1024 * 1024;
public static class BigObject {
@SuppressWarnings("unused")
private final byte[] space = new byte[MEGA];
private final int id;
public BigObject(final int id) {
this.id = id;
}
public int getId() {
return id;
}
}
// A weak reference on class
static final class ClassReference extends WeakReference<Class<?>> {
ClassReference(final Class<?> clazz, final ReferenceQueue<Object> queue) {
super(clazz, queue);
}
}
// A soft reference on instance
static final class InstanceReference extends SoftReference<Object> {
InstanceReference(final Object obj, final ReferenceQueue<Object> queue) {
super(obj, queue);
}
}
@Test
public void testOne() throws Exception {
// abort test if class creator can not run
if (!ClassCreator.canRun) {
logger.warn("unable to create classes");
return;
}
final ClassCreator cctor = new ClassCreator(jexl, base);
cctor.setSeed(1);
final Class<?> foo1 = cctor.createClass();
Assert.assertEquals("foo1", foo1.getSimpleName());
cctor.clear();
}
@Test
public void testFunctorOne() throws Exception {
final JexlContext ctxt = new MapContext();
ctxt.set("value", 1000);
// create a class foo1 with a ctor whose body gets a value
// from the context to initialize its value
final ClassCreator cctor = new ClassCreator(jexl, base);
cctor.setSeed(1);
cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
Class<?> foo1 = cctor.createClass(true);
Assert.assertSame(foo1.getClassLoader(), cctor.getClassLoader());
Assert.assertEquals("foo1", foo1.getSimpleName());
Object result = cctor.newInstance(foo1, ctxt);
Assert.assertEquals(foo1, result.getClass());
jexl.setClassLoader(cctor.getClassLoader());
cctor.clear();
// check we can invoke that ctor using its name or class
final JexlScript script = jexl.createScript("(c)->{ new(c).value; }");
result = script.execute(ctxt, foo1);
Assert.assertEquals(1010, result);
result = script.execute(ctxt, foo1.getName());
Assert.assertEquals(1010, result);
// re-create foo1 with a different body!
cctor.setSeed(1);
cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
final Class<?> foo11 = cctor.createClass(true);
Assert.assertEquals("foo1", foo1.getSimpleName());
Assert.assertNotSame(foo11, foo1);
foo1 = foo11;
result = cctor.newInstance(foo1, ctxt);
Assert.assertEquals(foo1, result.getClass());
// drum rolll....
jexl.setClassLoader(foo1.getClassLoader());
result = script.execute(ctxt, foo1.getName());
// tada!
Assert.assertEquals(1099, result);
result = script.execute(ctxt, foo1);
Assert.assertEquals(1099, result);
}
public static class NsTest implements JexlContext.NamespaceFunctor {
private final String className;
public NsTest(final String cls) {
className = cls;
}
@Override
public Object createFunctor(final JexlContext context) {
final JexlEngine jexl = JexlEngine.getThreadEngine();
return jexl.newInstance(className, context);
}
}
@Test
public void testFunctor2Name() throws Exception {
functorTwo(ClassCreator.GEN_CLASS + "foo2");
}
@Test
public void testFunctor2Class() throws Exception {
functorTwo(new NsTest(ClassCreator.GEN_CLASS + "foo2"));
}
void functorTwo(final Object nstest) throws Exception {
// create jexl2 with a 'test' namespace
final Map<String, Object> ns = new HashMap<String, Object>();
ns.put("test", nstest);
final JexlEngine jexl2 = new JexlBuilder().namespaces(ns).create();
final JexlContext ctxt = new MapContext();
ctxt.set("value", 1000);
// inject 'foo2' as test namespace functor class
final ClassCreator cctor = new ClassCreator(jexl, base);
cctor.setSeed(2);
cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
Class<?> foo1 = cctor.createClass(true);
Assert.assertSame(foo1.getClassLoader(), cctor.getClassLoader());
Assert.assertEquals("foo2", foo1.getSimpleName());
Object result = cctor.newInstance(foo1, ctxt);
Assert.assertEquals(foo1, result.getClass());
jexl2.setClassLoader(cctor.getClassLoader());
cctor.clear();
// check the namespace functor behavior
final JexlScript script = jexl2.createScript("test:getValue()");
result = script.execute(ctxt, foo1.getName());
Assert.assertEquals(1010, result);
// change the body
cctor.setSeed(2);
cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
final Class<?> foo11 = cctor.createClass(true);
Assert.assertEquals("foo2", foo1.getSimpleName());
Assert.assertNotSame(foo11, foo1);
foo1 = foo11;
result = cctor .newInstance(foo1, ctxt);
Assert.assertEquals(foo1, result.getClass());
// drum rolll....
jexl2.setClassLoader(foo1.getClassLoader());
result = script.execute(ctxt, foo1.getName());
// tada!
Assert.assertEquals(1099, result);
}
@Test
public void testFunctorThree() throws Exception {
final JexlContext ctxt = new MapContext();
ctxt.set("value", 1000);
final ClassCreator cctor = new ClassCreator(jexl, base);
cctor.setSeed(2);
cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 10;");
Class<?> foo1 = cctor.createClass(true);
Assert.assertSame(foo1.getClassLoader(), cctor.getClassLoader());
Assert.assertEquals("foo2", foo1.getSimpleName());
Object result = cctor.newInstance(foo1, ctxt);
Assert.assertEquals(foo1, result.getClass());
jexl.setClassLoader(cctor.getClassLoader());
cctor.clear();
final Map<String, Object> ns = new HashMap<String, Object>();
ns.put("test", foo1);
final JexlEngine jexl2 = new JexlBuilder().namespaces(ns).create();
final JexlScript script = jexl2.createScript("test:getValue()");
result = script.execute(ctxt, foo1.getName());
Assert.assertEquals(1010, result);
cctor.setSeed(2);
cctor.setCtorBody("value = (Integer) ctxt.get(\"value\") + 99;");
final Class<?> foo11 = cctor.createClass(true);
Assert.assertEquals("foo2", foo1.getSimpleName());
Assert.assertNotSame(foo11, foo1);
foo1 = foo11;
result = cctor.newInstance(foo1, ctxt);
Assert.assertEquals(foo1, result.getClass());
// drum rolll....
jexl2.setClassLoader(foo1.getClassLoader());
result = script.execute(ctxt, foo1.getName());
// tada!
Assert.assertEquals(1099, result);
}
@Test
public void testMany() throws Exception {
// abort test if class creator can not run
if (!ClassCreator.canRun) {
return;
}
int pass = 0;
int gced = -1;
final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
final List<Reference<?>> stuff = new ArrayList<Reference<?>>();
// keeping a reference on methods prevent classes from being GCed
// List<Object> mm = new ArrayList<Object>();
final JexlExpression expr = jexl.createExpression("foo.value");
final JexlExpression newx = jexl.createExpression("foo = new(clazz)");
final JexlEvalContext context = new JexlEvalContext();
final JexlOptions options = context.getEngineOptions();
options.setStrict(false);
options.setSilent(true);
final ClassCreator cctor = new ClassCreator(jexl, base);
for (int i = 0; i < LOOPS && gced < 0; ++i) {
cctor.setSeed(i);
Class<?> clazz;
if (pass == 0) {
clazz = cctor.createClass();
} else {
clazz = cctor.getClassInstance();
if (clazz == null) {
Assert.assertEquals(i, gced);
break;
}
}
// this code verifies the assumption that holding a strong reference to a method prevents
// its owning class from being GCed
// Method m = clazz.getDeclaredMethod("getValue", new Class<?>[0]);
// mm.add(m);
// we should not be able to create foox since it is unknown to the JEXL classloader
context.set("clazz", cctor.getClassName());
context.set("foo", null);
Object z = newx.evaluate(context);
Assert.assertNull(z);
// check with the class itself
context.set("clazz", clazz);
z = newx.evaluate(context);
Assert.assertNotNull(clazz + ": class " + i + " could not be instantiated on pass " + pass, z);
Assert.assertEquals(new Integer(i), expr.evaluate(context));
// with the proper class loader, attempt to create an instance from the class name
jexl.setClassLoader(cctor.getClassLoader());
z = newx.evaluate(context);
Assert.assertEquals(z.getClass(), clazz);
Assert.assertEquals(new Integer(i), expr.evaluate(context));
cctor.clear();
jexl.setClassLoader(null);
// on pass 0, attempt to force GC to run and collect generated classes
if (pass == 0) {
// add a weak reference on the class
stuff.add(new ClassReference(clazz, queue));
// add a soft reference on an instance
stuff.add(new InstanceReference(clazz.newInstance(), queue));
// attempt to force GC:
// while we still have a MB free, create & store big objects
for (int b = 0; b < 1024 && Runtime.getRuntime().freeMemory() > MEGA; ++b) {
final BigObject big = new BigObject(b);
stuff.add(new InstanceReference(big, queue));
}
// hint it...
System.gc();
// let's see if some weak refs got collected
boolean qr = false;
while (queue.poll() != null) {
final Reference<?> ref = queue.remove(1);
if (ref instanceof ClassReference) {
gced = i;
qr = true;
}
}
if (qr) {
//logger.warn("may have GCed class around " + i);
pass = 1;
i = 0;
}
}
}
if (gced < 0) {
logger.warn("unable to force GC");
//Assert.assertTrue(gced > 0);
}
}
public static class TwoCtors {
int value;
public TwoCtors(final int v) {
this.value = v;
}
public TwoCtors(final Number x) {
this.value = -x.intValue();
}
public int getValue() {
return value;
}
}
@Test
public void testBasicCtor() throws Exception {
final JexlScript s = jexl.createScript("(c, v)->{ var ct2 = new(c, v); ct2.value; }");
Object r = s.execute(null, TwoCtors.class, 10);
Assert.assertEquals(10, r);
r = s.execute(null, TwoCtors.class, 5 + 5);
Assert.assertEquals(10, r);
r = s.execute(null, TwoCtors.class, 10d);
Assert.assertEquals(-10, r);
r = s.execute(null, TwoCtors.class, 100f);
Assert.assertEquals(-100, r);
}
public static class ContextualCtor {
int value = -1;
public ContextualCtor(final JexlContext ctxt) {
value = (Integer) ctxt.get("value");
}
public ContextualCtor(final JexlContext ctxt, final int v) {
value = (Integer) ctxt.get("value") + v;
}
public int getValue() {
return value;
}
}
@Test
public void testContextualCtor() throws Exception {
final MapContext ctxt = new MapContext();
ctxt.set("value", 42);
JexlScript s = jexl.createScript("(c)->{ new(c).value }");
Object r = s.execute(ctxt, ContextualCtor.class);
Assert.assertEquals(42, r);
s = jexl.createScript("(c, v)->{ new(c, v).value }");
r = s.execute(ctxt, ContextualCtor.class, 100);
Assert.assertEquals(142, r);
}
}