/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* Licensed 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.iternine.jeppetto.enhance;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Method;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ClassLoadingUtilTest {
//-------------------------------------------------------------
// Variables - Private - Static
//-------------------------------------------------------------
private static ClassPool pool;
private static AtomicInteger count;
//-------------------------------------------------------------
// Methods - Public - Static
//-------------------------------------------------------------
@BeforeClass
public static void setupTest() {
pool = ClassPool.getDefault();
count = new AtomicInteger(0);
}
//-------------------------------------------------------------
// Tests
//-------------------------------------------------------------
@Test
public void basicSubclassWorks()
throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
CtClass superClass = pool.get(TestClassAlpha.class.getName());
CtClass subClass = pool.makeClass(makeNewName());
subClass.setSuperclass(superClass);
CtMethod toStringMethod = superClass.getDeclaredMethod("toString");
CtMethod override = CtNewMethod.copy(toStringMethod, "toString", subClass, null);
override.setBody("return ($r) super.toString().concat(\"Beta!\");");
subClass.addMethod(override);
Class<TestClassAlpha> betaClass = ClassLoadingUtil.toClass(subClass);
TestClassAlpha beta = betaClass.newInstance();
assertEquals("AlphaBeta!", beta.toString());
}
// This test attempts to verify that concurrent usage of the utility doesn't leave the ClassLoader.defineClass
// method in a wonky state.
@Test
public void utilityAppearsThreadSafe()
throws Exception {
int threads = 10;
int iterations = 500;
ExecutorService exec = Executors.newFixedThreadPool(threads);
final Method defineClass1 = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
final Method defineClass2 = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ProtectionDomain.class);
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch done = new CountDownLatch(threads * iterations);
final List<Class<?>> newClasses = Collections.synchronizedList(new ArrayList<Class<?>>(threads));
for (int i = 0; i < threads * iterations; i++) {
exec.submit(new Runnable() {
public void run() {
try {
String newName = makeNewName();
CtClass newClass = pool.makeClass(newName);
start.await();
// aggressively test isAccessible on the methods. they should only be accessible to
// one thread at a time, only within the toClass method, for a very brief time
assertFalse(defineClass1.isAccessible());
assertFalse(defineClass2.isAccessible());
Class<Object> theNewClass = ClassLoadingUtil.toClass(newClass);
assertFalse(defineClass1.isAccessible());
assertFalse(defineClass2.isAccessible());
newClasses.add(theNewClass);
} catch (Exception e) {
// ignore
} finally {
done.countDown();
}
}
});
}
start.countDown();
done.await();
assertEquals("Bug in test", 0, exec.shutdownNow().size());
assertEquals(threads * iterations, newClasses.size());
assertFalse(defineClass1.isAccessible());
assertFalse(defineClass2.isAccessible());
}
@Test
public void enhanceGamma()
throws Exception {
CtClass gamma = pool.get(TestClassGamma.class.getName());
CtClass dirtyInterface = pool.get(Persistent.class.getName());
CtClass enhanced = pool.makeClass(makeNewName());
enhanced.setSuperclass(gamma);
enhanced.addInterface(dirtyInterface);
CtMethod isDirtyMethod = CtNewMethod.make("public boolean isDirty() { return getFoo() != null || getBars() != null; }", enhanced);
enhanced.addMethod(isDirtyMethod);
Class<TestClassGamma> cls = ClassLoadingUtil.toClass(enhanced);
TestClassGamma instance = cls.newInstance();
assertNotNull(instance);
assertNull(instance.getBars());
assertNull(instance.getFoo());
assertDirty(instance, false);
instance.setFoo(toString());
instance.setBars(Collections.singletonList("bar"));
assertDirty(instance, true);
assertEquals(toString(), instance.getFoo());
assertEquals(Collections.singletonList("bar"), instance.getBars());
instance.setFoo(null);
instance.setBars(null);
assertNull(instance.getBars());
assertNull(instance.getFoo());
assertDirty(instance, false);
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private static String makeNewName() {
return String.format("%s$$Sub%d", ClassLoadingUtilTest.class.getName(), count.incrementAndGet());
}
private void assertDirty(Object obj, boolean isDirty) {
assertTrue(obj instanceof Persistent);
assertEquals(isDirty, ((Persistent) obj).isDirty());
}
//-------------------------------------------------------------
// Inner Class - TestClassAlpha
//-------------------------------------------------------------
public static class TestClassAlpha {
//-------------------------------------------------------------
// Methods - Canonical
//-------------------------------------------------------------
@Override
public String toString() {
return "Alpha";
}
}
//-------------------------------------------------------------
// Inner Class - TestClassGamma
//-------------------------------------------------------------
public static class TestClassGamma {
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private String foo;
private List<String> bars;
//-------------------------------------------------------------
// Methods - Getter/Setter
//-------------------------------------------------------------
public String getFoo() {
return foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
public List<String> getBars() {
return bars;
}
public void setBars(List<String> bars) {
this.bars = (bars == null) ? null : new ArrayList<String>(bars);
}
}
}