/*
* Copyright 2006-2011 Brian S O'Neill
*
* 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.cojen.dirmi.trace;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Map;
import org.cojen.classfile.attribute.Annotation;
import org.cojen.classfile.ClassFile;
import org.cojen.classfile.CodeAssembler;
import org.cojen.classfile.CodeBuilder;
import org.cojen.classfile.CodeDisassembler;
import org.cojen.classfile.DelegatedCodeAssembler;
import org.cojen.classfile.Label;
import org.cojen.classfile.LocalVariable;
import org.cojen.classfile.MethodInfo;
import org.cojen.classfile.Modifiers;
import org.cojen.classfile.Opcode;
import org.cojen.classfile.TypeDesc;
import org.cojen.classfile.constant.ConstantIntegerInfo;
import org.cojen.classfile.constant.ConstantStringInfo;
import org.cojen.classfile.constant.ConstantUTFInfo;
import org.cojen.dirmi.Trace;
import static org.cojen.dirmi.trace.TraceMode.*;
import static org.cojen.dirmi.trace.TraceModes.*;
/**
*
*
* @author Brian S O'Neill
*/
class Transformer implements ClassFileTransformer {
private static final boolean DEBUG;
private static final String HANDLER_FIELD_PREFIX;
static {
DEBUG = Boolean.getBoolean("org.cojen.dirmi.trace.Transformer.DEBUG");
HANDLER_FIELD_PREFIX = Transformer.class.getName().replace('.', '$') + "$handler$";
}
private static int cHandlerFieldCounter = 0;
private static synchronized String handlerFieldName() {
String name = HANDLER_FIELD_PREFIX + cHandlerFieldCounter;
cHandlerFieldCounter++;
return name;
}
private final TraceAgent mAgent;
final String mHandlerFieldName;
Transformer(TraceAgent agent) {
mAgent = agent;
mHandlerFieldName = handlerFieldName();
}
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException
{
if (loader == null) {
// Cannot transform classes loaded by bootstrap loader.
return null;
}
// Test if loader can access TraceAgent.
try {
loader.loadClass(TraceAgent.class.getName());
} catch (ClassNotFoundException e) {
return null;
}
className = className.replace('/', '.');
// Handle special cases...
if (className.startsWith("org.cojen.dirmi.trace.") ||
className.startsWith("sun.reflect."))
{
return null;
}
TraceModes modes = mAgent.getTraceModes(className);
if (modes == null || modes == ALL_OFF) {
return null;
}
ClassFile cf;
try {
cf = ClassFile.readFrom(new ByteArrayInputStream(classfileBuffer));
} catch (Exception e) {
IllegalClassFormatException e2 = new IllegalClassFormatException();
e2.initCause(e);
throw e2;
}
if (cf.getModifiers().isInterface()) {
// Short-circuit. Nothing to transform.
return null;
}
Map<MethodInfo, MidAndOp> transformedMethods = new HashMap<MethodInfo, MidAndOp>();
for (MethodInfo mi : cf.getMethods()) {
tryTransform(modes, transformedMethods, mi);
}
for (MethodInfo mi : cf.getConstructors()) {
tryTransform(modes, transformedMethods, mi);
}
if (transformedMethods.size() == 0) {
// Class is unchanged.
return null;
}
// Add field for holding reference to handler.
cf.addField(Modifiers.PRIVATE.toStatic(true),
mHandlerFieldName, TypeDesc.forClass(TraceHandler.class));
// Add or prepend static initializer for getting handler and
// registering methods.
{
MethodInfo clinit = cf.getInitializer();
CodeDisassembler dis;
if (clinit == null) {
dis = null;
clinit = cf.addInitializer();
} else {
dis = new CodeDisassembler(clinit);
}
CodeBuilder b = new CodeBuilder(clinit);
TypeDesc agentType = TypeDesc.forClass(TraceAgent.class);
LocalVariable agentVar = b.createLocalVariable("agent", agentType);
b.loadConstant(mAgent.getAgentId());
b.invokeStatic(agentType, "getTraceAgent",
agentType, new TypeDesc[] {TypeDesc.LONG});
b.storeLocal(agentVar);
TypeDesc handlerType = TypeDesc.forClass(TraceHandler.class);
b.loadLocal(agentVar);
b.invokeVirtual(agentType, "getTraceHandler", handlerType, null);
b.storeStaticField(mHandlerFieldName, handlerType);
// Finish registering each method.
TypeDesc classType = TypeDesc.forClass(Class.class);
TypeDesc classArrayType = classType.toArrayType();
for (Map.Entry<MethodInfo, MidAndOp> entry : transformedMethods.entrySet()) {
MethodInfo mi = entry.getKey();
MidAndOp midAndOp = entry.getValue();
// For use below when calling registerTraceMethod.
b.loadLocal(agentVar);
b.loadConstant(midAndOp.mid);
b.loadConstant(midAndOp.operation);
b.loadConstant(cf.getType());
b.loadConstant(mi.getName().equals("<init>") ? null : mi.getName());
TypeDesc returnType = mi.getMethodDescriptor().getReturnType();
if (returnType == null || returnType == TypeDesc.VOID) {
b.loadNull();
} else {
b.loadConstant(returnType);
}
int hasThis = mi.getModifiers().isStatic() ? 0 : 1;
TypeDesc[] types = mi.getMethodDescriptor().getParameterTypes();
if (hasThis + types.length == 0) {
b.loadNull();
} else {
b.loadConstant(hasThis + types.length);
b.newObject(classArrayType);
for (int i=0; i<types.length; i++) {
// dup array
b.dup();
b.loadConstant(hasThis + i);
b.loadConstant(types[i]);
b.storeToArray(classType);
}
}
b.invokeVirtual(agentType, "registerTraceMethod", null, new TypeDesc[]
{TypeDesc.INT, TypeDesc.STRING,
classType, TypeDesc.STRING, classType, classArrayType});
}
if (dis == null) {
b.returnVoid();
} else {
dis.disassemble(b);
}
}
// Define the newly transformed class.
if (DEBUG) {
File file = new File(cf.getClassName().replace('.', '/') + ".class");
try {
File tempDir = new File(System.getProperty("java.io.tmpdir"));
file = new File(tempDir, file.getPath());
} catch (SecurityException e) {
}
try {
file.getParentFile().mkdirs();
System.out.println("Dirmi trace Transformer writing to " + file);
OutputStream out = new FileOutputStream(file);
cf.writeTo(out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
cf.writeTo(out);
out.close();
} catch (Exception e) {
IllegalClassFormatException e2 = new IllegalClassFormatException();
e2.initCause(e);
throw e2;
}
return out.toByteArray();
}
private String getStringParam(Map<String, Annotation.MemberValue> memberValues,
String paramName)
{
Annotation.MemberValue mv = memberValues.get(paramName);
Object constant;
if (mv != null && (constant = mv.getValue()) != null) {
if (constant instanceof ConstantUTFInfo) {
return ((ConstantUTFInfo) constant).getValue();
}
if (constant instanceof ConstantStringInfo) {
return ((ConstantStringInfo) constant).getValue();
}
}
return null;
}
private Boolean getBooleanParam(Map<String, Annotation.MemberValue> memberValues,
String paramName)
{
Annotation.MemberValue mv = memberValues.get(paramName);
Object constant;
if (mv != null && (constant = mv.getValue()) != null
&& constant instanceof ConstantIntegerInfo) {
return ((ConstantIntegerInfo) constant).getValue() != 0;
}
return null;
}
private boolean getBooleanParam(Map<String, Annotation.MemberValue> memberValues,
String paramName,
boolean defaultValue)
{
Boolean value = getBooleanParam(memberValues, paramName);
return value == null ? defaultValue : value;
}
private void tryTransform(TraceModes modes,
Map<MethodInfo, MidAndOp> transformedMethods,
MethodInfo mi)
{
if (mi.getModifiers().isAbstract() || mi.getModifiers().isNative()) {
return;
}
Annotation traceAnnotation = null;
findTrace: {
Annotation[] annotations = mi.getRuntimeVisibleAnnotations();
for (Annotation ann : annotations) {
if (ann.getType().getFullName().equals(Trace.class.getName())) {
traceAnnotation = ann;
break findTrace;
}
}
}
String operation;
boolean args, result, exception, time, root, graft;
if (traceAnnotation == null) {
// If no user trace annotation exists, check if any trace mode is
// on which will turn on trace anyhow.
operation = null;
args = modes.getTraceArguments() == ON;
result = modes.getTraceResult() == ON;
exception = modes.getTraceException() == ON;
time = modes.getTraceTime() == ON;
if (!args && !result && !exception && !time) {
if (modes.getTraceCalls() != ON) {
// No features forced on, so don't trace.
return;
}
}
root = false;
graft = false;
} else {
// Extract trace parameters. Defaults copied from Trace annotation.
Map<String, Annotation.MemberValue> memberValues = traceAnnotation.getMemberValues();
operation = getStringParam(memberValues, "operation");
if ("".equals(operation)) {
operation = null;
}
args = getBooleanParam(memberValues, "args", false);
if (modes.getTraceArguments() != USER) {
args = modes.getTraceArguments() == ON;
}
result = getBooleanParam(memberValues, "result", false);
if (modes.getTraceResult() != USER) {
result = modes.getTraceResult() == ON;
}
exception = getBooleanParam(memberValues, "exception", false);
if (modes.getTraceException() != USER) {
exception = modes.getTraceException() == ON;
}
time = getBooleanParam(memberValues, "time", true);
if (modes.getTraceTime() != USER) {
time = modes.getTraceTime() == ON;
}
if (!args && !result && !exception && !time) {
if (modes.getTraceCalls() == OFF) {
// No features on, so don't trace.
return;
}
}
root = getBooleanParam(memberValues, "root", false);
graft = getBooleanParam(memberValues, "graft", false);
}
int mid = transform(mi, operation, args, result, exception, time, root, graft);
transformedMethods.put(mi, new MidAndOp(mid, operation));
}
/**
* @param operation optional trace operation name
* @param args when true, pass method arguments to trace handler
* @param result when true, pass method return value to trace handler
* @param exception when true, pass thrown exception to trace handler
* @param time when true, pass method execution time to trace handler
* @param root when true, indicate to trace handler that method should be reported as root
* @param graft when true, indicate to trace handler that method should be reported as graft
* @return method id
*/
private int transform(MethodInfo mi,
String operation,
boolean args,
boolean result,
boolean exception,
boolean time,
boolean root,
boolean graft)
{
if (mi.getMethodDescriptor().getReturnType() == TypeDesc.VOID) {
result = false;
}
int mid = mAgent.reserveMethod(root, graft);
CodeDisassembler dis = new CodeDisassembler(mi);
CodeBuilder b = new CodeBuilder(mi);
// Call enterMethod
{
b.loadStaticField(mHandlerFieldName, TypeDesc.forClass(TraceHandler.class));
b.loadConstant(mid);
TypeDesc[] params;
if (!args) {
params = new TypeDesc[] {TypeDesc.INT};
} else {
int argCount = mi.getMethodDescriptor().getParameterCount();
int hasThis = mi.getModifiers().isStatic() ? 0 : 1;
argCount += hasThis;
if (argCount == 0) {
params = new TypeDesc[] {TypeDesc.INT};
} else if (argCount == 1) {
params = new TypeDesc[] {TypeDesc.INT, TypeDesc.OBJECT};
if (hasThis != 0) {
b.loadThis();
} else {
b.loadLocal(b.getParameter(0));
b.convert(b.getParameter(0).getType(), TypeDesc.OBJECT);
}
} else {
params = new TypeDesc[] {TypeDesc.INT, TypeDesc.OBJECT.toArrayType()};
b.loadConstant(argCount);
b.newObject(TypeDesc.OBJECT.toArrayType());
if (hasThis != 0) {
// dup array
b.dup();
b.loadConstant(0);
b.loadThis();
b.storeToArray(TypeDesc.OBJECT);
argCount--;
}
for (int i=0; i<argCount; i++) {
// dup array
b.dup();
b.loadConstant(hasThis + i);
b.loadLocal(b.getParameter(i));
b.convert(b.getParameter(i).getType(), TypeDesc.OBJECT);
b.storeToArray(TypeDesc.OBJECT);
}
}
}
b.invokeInterface(TypeDesc.forClass(TraceHandler.class), "enterMethod", null, params);
}
LocalVariable startTime = null;
if (time) {
startTime = b.createLocalVariable("startTime", TypeDesc.LONG);
b.invokeStatic(TypeDesc.forClass(System.class), "nanoTime", TypeDesc.LONG, null);
b.storeLocal(startTime);
}
Label tryStart = b.createLabel().setLocation();
Label tryEnd = b.createLabel();
dis.disassemble(b, null, tryEnd);
tryEnd.setLocation();
// Fall to this point for normal exit.
{
// Save result in local variable to pass to exitMethod (if result passing enabled)
LocalVariable resultVar = null;
if (result) {
resultVar = b.createLocalVariable
("result", mi.getMethodDescriptor().getReturnType());
b.storeLocal(resultVar);
}
// Prepare call to exit method
b.loadStaticField(mHandlerFieldName, TypeDesc.forClass(TraceHandler.class));
b.loadConstant(mid);
TypeDesc[] exitMethodParams;
if (time) {
if (result) {
exitMethodParams = new TypeDesc[] {
TypeDesc.INT, TypeDesc.OBJECT, TypeDesc.LONG
};
b.loadLocal(resultVar);
b.convert(resultVar.getType(), TypeDesc.OBJECT);
} else {
exitMethodParams = new TypeDesc[] {TypeDesc.INT, TypeDesc.LONG};
}
} else if (result) {
exitMethodParams = new TypeDesc[] {TypeDesc.INT, TypeDesc.OBJECT};
b.loadLocal(resultVar);
b.convert(resultVar.getType(), TypeDesc.OBJECT);
} else {
exitMethodParams = new TypeDesc[] {TypeDesc.INT};
}
if (time) {
b.invokeStatic(TypeDesc.forClass(System.class), "nanoTime", TypeDesc.LONG, null);
b.loadLocal(startTime);
b.math(Opcode.LSUB);
// Leave on stack for exitMethod call.
}
// Call exitMethod
b.invokeInterface(TypeDesc.forClass(TraceHandler.class), "exitMethod",
null, exitMethodParams);
if (result) {
b.loadLocal(resultVar);
}
b.returnValue(mi.getMethodDescriptor().getReturnType());
}
b.exceptionHandler(tryStart, tryEnd, null);
// Fall to this point for exception exit.
{
// Save exception in local variable to pass to exitMethod (if
// exception passing enabled)
TypeDesc throwableType = TypeDesc.forClass(Throwable.class);
LocalVariable exceptionVar = null;
if (exception) {
exceptionVar = b.createLocalVariable("e", throwableType);
b.storeLocal(exceptionVar);
}
b.loadStaticField(mHandlerFieldName, TypeDesc.forClass(TraceHandler.class));
b.loadConstant(mid);
TypeDesc[] exitMethodParams;
if (time) {
if (exception) {
exitMethodParams = new TypeDesc[] {TypeDesc.INT, throwableType, TypeDesc.LONG};
b.loadLocal(exceptionVar);
} else {
exitMethodParams = new TypeDesc[] {TypeDesc.INT, TypeDesc.LONG};
}
} else if (exception) {
exitMethodParams = new TypeDesc[] {TypeDesc.INT, throwableType};
b.loadLocal(exceptionVar);
} else {
exitMethodParams = new TypeDesc[] {TypeDesc.INT};
}
if (time) {
b.invokeStatic(TypeDesc.forClass(System.class), "nanoTime", TypeDesc.LONG, null);
b.loadLocal(startTime);
b.math(Opcode.LSUB);
// Leave on stack for exitMethod call.
}
// Call exitMethod
b.invokeInterface(TypeDesc.forClass(TraceHandler.class), "exitMethod",
null, exitMethodParams);
if (exception) {
b.loadLocal(exceptionVar);
}
b.throwObject();
}
return mid;
}
private static class MidAndOp {
public final int mid;
public final String operation;
MidAndOp(int mid, String operation) {
this.mid = mid;
this.operation = operation;
}
}
}