
org.callbackparams.internal.CallbackMethodProxyingRebyter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of callbackparams Show documentation
Show all versions of callbackparams Show documentation
CallbackParams is a JUnit-extension for writing parameterized tests.
It unlocks new innovative patterns that offer elegant solutions to many
obstacles that are traditionally associated with parameterized testing.
/*
* Copyright 2010 the original author or authors.
*
* 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.callbackparams.internal;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.EmptyVisitor;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Unknown;
import org.apache.bcel.classfile.Visitor;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.Type;
import org.callbackparams.AdaptiveRule;
import org.callbackparams.internal.template.AdaptiveRulesPackager;
import org.callbackparams.internal.template.AdaptiveRulesPackager.VisitorParentGenerator;
import org.callbackparams.internal.template.TestrunCallbacks;
import org.callbackparams.support.ClassBytecodeBuilder;
import org.callbackparams.support.MethodHashKey;
/**
* This is a thread-safe Rebyter implementation.
*
* @author Henrik Kaipe
*/
class CallbackMethodProxyingRebyter
implements CallbackTestClassReloader.Rebyter {
private static final String testrunCallbacksClassName =
TestrunCallbacks.class.getName();
private static final String testrunCallbacksArgumentArrayFieldName =
lookupArgumentArrayFieldName();
private static final Type testrunCallbacksArgumentArrayType =
Type.getType(new Object[0].getClass());
private final ClassLoader templateClassLoader;
private final Class adaptiveRuleSuperClass;
/**
* This is the class that will have its super-class changed to the
* rebyted TestrunCallbacks class.
* It will be set to a proper value during construction. The initial value
* is just a convenient dummy-value that release the code from null-checks.
*/
private Class topClassInRebytedPackage = getClass();
/**
* Keeps track of and performs all necessary constructor modifications.
*/
private final TestrunCallbacksConstructors constructors2Modify;
/**
* The keys in this map are names of all the classes that need to be rebyted
* in order to have their methods-with-callback-arguments proxied by a
* methodName$callbackProxy() method.
*/
private final NoargMethodProxyMap proxyMap;
private final Class testClass;
private boolean adaptiveRulesAreUsed = false;
private final Map/*MethodHashKey,Method*/ testMethodHash = new HashMap();
private final Map/*String,AdaptiveRulesPackager.VisitorParentGenerator*/
syntheticVisitorParents = new HashMap();
private final List/*Field*/ callbackInjectionFields = new ArrayList();
public CallbackMethodProxyingRebyter(
Class testClass,
CallbackRebyteInfo rebyteInfo) {
this.testClass = testClass;
this.templateClassLoader = testClass.getClassLoader();
this.proxyMap = new NoargMethodProxyMap(testClass);
setupClassesToRebyte(testClass, rebyteInfo);
if (false == adaptiveRulesAreUsed) {
/*
* Disable the test-method modifications for adaptive rules
*/
testMethodHash.clear();
syntheticVisitorParents.clear();
}
this.constructors2Modify =
new TestrunCallbacksConstructors(topClassInRebytedPackage);
this.adaptiveRuleSuperClass = rebyteInfo.adaptiveRuleSuperClass();
}
private static String lookupArgumentArrayFieldName() {
Class desiredFieldType = new Object[0].getClass();
final Field[] fields = TestrunCallbacks.class.getDeclaredFields();
for (int i = 0 ; i < fields.length ; ++i) {
final Field f = fields[i];
final int modifiers = f.getModifiers();
if (false == Modifier.isStatic(modifiers)
&& false == Modifier.isFinal(modifiers)
&& Modifier.isProtected(modifiers)
&& desiredFieldType == f.getType()
&& TestrunCallbacks.class == f.getDeclaringClass()) {
return f.getName();
}
}
throw new Error("Cannot find field with callback method arguments");
}
private void setupClassesToRebyte(
Class clazz,
CallbackRebyteInfo rebyteInfo) {
if (Object.class.getClassLoader() == clazz.getClassLoader()) {
return;
}
if (clazz.getPackage() == topClassInRebytedPackage.getPackage()){
this.topClassInRebytedPackage = clazz;
}
List/*Field*/ adaptiveRules = new ArrayList();
List/*Method*/ testMethods = new ArrayList();
final Field[] fields = clazz.getDeclaredFields();
for (int i = 0 ; i < fields.length ; ++i) {
final Field f = fields[i];
if (rebyteInfo.isCallbackInjectField(f)) {
this.callbackInjectionFields.add(f);
this.topClassInRebytedPackage = clazz;
}
if (rebyteInfo.isAdaptiveRuleField(f)) {
adaptiveRules.add(f);
adaptiveRulesAreUsed = true;
}
}
final Method[] methods = clazz.getDeclaredMethods();
for (int i = 0 ; i < methods.length ; ++i) {
final Method m = methods[i];
if (rebyteInfo.isCallbackProxiedMethod(m)) {
rebyteInfo.putNoargProxyMethod(
proxyMap.addMethodToBeProxied(m), m);
this.topClassInRebytedPackage = clazz;
}
if (rebyteInfo.isTestMethod(m)) {
testMethods.add(m);
testMethodHash.put(MethodHashKey.getHashKey(m), m);
this.topClassInRebytedPackage = clazz;
}
}
if (false == testMethods.isEmpty()
|| false == adaptiveRules.isEmpty()) {
VisitorParentGenerator generator = new VisitorParentGenerator(
clazz, adaptiveRules, testMethods);
syntheticVisitorParents.put(
generator.getInterfaceName(), generator);
}
if (rebyteInfo.isExplicitTopClass(clazz)) {
this.topClassInRebytedPackage = clazz;
return;
}
setupClassesToRebyte(clazz.getSuperclass(), rebyteInfo);
}
public ClassLoader getParentClassLoader() {
String topClassName = topClassInRebytedPackage.getName();
for (ClassLoader parent = topClassInRebytedPackage.getClassLoader()
.getParent() ; null != parent ; parent = parent.getParent()) {
try {
parent.loadClass(topClassName);
} catch (ClassNotFoundException expectedForProperParentLoader) {
return parent;
}
}
return null;
}
public byte[] newSyntheticClass(String className) {
if (syntheticVisitorParents.containsKey(className)) {
VisitorParentGenerator generator = (VisitorParentGenerator)
syntheticVisitorParents.remove(className);
return generator.generateByteCode();
} else {
return null;
}
}
public byte[] rebyte(
CallbackContext callbackContext,
JavaClass templateClassDef) {
final String className = templateClassDef.getClassName();
if (AdaptiveRule.class.getName().equals(className)) {
return rebyteAdaptiveRule();
}
if (AdaptiveRulesPackager.Visitor.class.getName().equals(className)) {
return AdaptiveRulesPackager.generateRebytedVisitor(
testClass, syntheticVisitorParents.keySet());
}
/* There might be fields to inject - regardless of whether
* there is a need to modify the byte-code. */
for (Iterator iterFields = callbackInjectionFields.iterator()
; iterFields.hasNext() ;) {
final Field f = (Field) iterFields.next();
if (className.equals(f.getDeclaringClass().getName())) {
callbackContext.addFieldToInject(f);
}
}
Map/*String,Method*/ methodsToProxy =
proxyMap.getProxyMethodsForClass(className);
if (false== constructors2Modify.needsConstructorModifications(className)
&& methodsToProxy.isEmpty()
&& testMethodHash.isEmpty()) {
/* No byte-code modifications are needed ... */
return templateClassDef.getBytes();
}
ClassBytecodeBuilder cbb;
try {
cbb = ClassBytecodeBuilder.newInstance(templateClassLoader
.loadClass(className));
if (false == Modifier.isPrivate(templateClassDef.getModifiers())) {
/* JUnit wants test-classes to be public so we make sure it is,
* since it should not cause any problems anyway. */
cbb.setPublic();
}
} catch (ClassNotFoundException ex) {
throw new Error(ex);
}
if (false == testMethodHash.isEmpty()) {
/*
* Rebyte test-methods to make sure they make the detour that
* evaluates the adaptive rules.
*/
final org.apache.bcel.classfile.Method[] methods =
templateClassDef.getMethods();
InstructionFactory factory = cbb.getInstructionFactory();
for (int i = 0;
i < methods.length && false == testMethodHash.isEmpty();
++i) {
org.apache.bcel.classfile.Method m = methods[i];
MethodHashKey methodHashKey = new MethodHashKey(
className, m.getName(), m.getSignature());
Method testMethod = (Method)
testMethodHash.remove(methodHashKey);
if (null != testMethod) {
cbb.prependMethod(m, AdaptiveRulesPackager
.defineTestMethodDetourCall(factory, testMethod));
}
}
}
for (Iterator iterEntr = methodsToProxy.entrySet().iterator()
; iterEntr.hasNext() ;) {
final Map.Entry proxyEntr = (Map.Entry) iterEntr.next();
proxyMethod(cbb, proxyEntr, callbackContext
.addMethodParams((Method)proxyEntr.getValue()));
}
constructors2Modify.modifyConstructors(className, cbb);
return cbb.getByteCode();
}
private void proxyMethod(ClassBytecodeBuilder cbb,
Map.Entry/*String,Method*/ proxyDef,
int callbackArgumentIndexStart) {
org.apache.bcel.classfile.Method oldMethod =
cbb.getMethod((Method)proxyDef.getValue());
InstructionFactory factory = cbb.getInstructionFactory();
InstructionList il = new InstructionList();
final MethodGen mg = new MethodGen(oldMethod.getAccessFlags(),
oldMethod.getReturnType(), Type.NO_ARGS, null,
(String)proxyDef.getKey(), factory.getClassGen().getClassName(),
il, factory.getConstantPool());
if (false == mg.isStatic()) {
il.append(InstructionFactory.ALOAD_0);
}
Instruction pushCallbackArgumentsArray = factory.createGetField(
testrunCallbacksClassName,
testrunCallbacksArgumentArrayFieldName,
testrunCallbacksArgumentArrayType);
for (int i = 0, length = oldMethod.getArgumentTypes().length
; i < length ; ++i) {
il.append(InstructionFactory.ALOAD_0);
il.append(pushCallbackArgumentsArray);
il.append(factory.createConstant(
new Integer(callbackArgumentIndexStart + i)));
il.append(InstructionFactory.AALOAD);
}
il.append(factory.createInvoke(
factory.getClassGen().getClassName(),
oldMethod.getName(),
oldMethod.getReturnType(),
oldMethod.getArgumentTypes(),
oldMethod.isStatic() ? Constants.INVOKESTATIC
: oldMethod.isPrivate() ? Constants.INVOKESPECIAL
: Constants.INVOKEVIRTUAL));
il.append(InstructionFactory.createReturn(oldMethod.getReturnType()));
/* Transfer annotations ... */
final List/*Attribute*/ attributesToKeep = new ArrayList();
Visitor annotationTransferer = new EmptyVisitor() {
// @Override
/** Unknown attributes are considered to be annotations.
* This works for version 5.2 of bcel but is not likely to work
* for the next feature relase version. */
public void visitUnknown(Unknown obj) {
mg.addAttribute(obj);
/* Remove annotation from the old-methods keep-list.
* (The annotation was the last one to be added.) */
attributesToKeep.remove(attributesToKeep.size() - 1);
}
};
final Attribute[] attributes = oldMethod.getAttributes();
for (int i = 0 ; i < attributes.length ; ++i) {
final Attribute a = attributes[i];
attributesToKeep.add(a); // Could be removed again by the visitor
a.accept(annotationTransferer);
}
mg.setMaxStack();
cbb.addMethod(mg.getMethod());
/* Set new attribute array for old method ... */
oldMethod.setAttributes((Attribute[]) attributesToKeep.toArray(
new Attribute[attributesToKeep.size()]));
}
private byte[] rebyteAdaptiveRule() {
ClassBytecodeBuilder cbb = rebuilderForClass(AdaptiveRule.class);
if (null != adaptiveRuleSuperClass) {
cbb.setNewSuperClass(adaptiveRuleSuperClass.getName());
}
return cbb.getByteCode();
}
private ClassBytecodeBuilder rebuilderForClass(Class classToRebyte) {
try {
return ClassBytecodeBuilder.newInstance(
templateClassLoader.loadClass(classToRebyte.getName()));
} catch (ClassNotFoundException ex) {
throw new Error(ex);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy