package org.hotswap.agent.annotation.handler;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.InvocationTargetException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.hotswap.agent.annotation.LoadEvent;
import org.hotswap.agent.annotation.OnClassLoadEvent;
import org.hotswap.agent.config.PluginManager;
import org.hotswap.agent.javassist.CannotCompileException;
import org.hotswap.agent.javassist.ClassPool;
import org.hotswap.agent.javassist.CtClass;
import org.hotswap.agent.javassist.LoaderClassPath;
import org.hotswap.agent.javassist.NotFoundException;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.util.AppClassLoaderExecutor;
import org.hotswap.agent.versions.DeploymentInfo;
public class PluginClassFileTransformer implements ClassFileTransformer {
protected static AgentLogger LOGGER = AgentLogger.getLogger(PluginClassFileTransformer.class);
private final OnClassLoadEvent onClassLoadAnnotation;
private final PluginAnnotation<OnClassLoadEvent> pluginAnnotation;
private final List<LoadEvent> events;
private final PluginManager pluginManager;
public PluginClassFileTransformer(PluginManager pluginManager, PluginAnnotation<OnClassLoadEvent> pluginAnnotation) {
this.pluginManager = pluginManager;
this.pluginAnnotation = pluginAnnotation;
this.onClassLoadAnnotation = pluginAnnotation.getAnnotation();
this.events = Arrays.asList(onClassLoadAnnotation.events());
}
public boolean isPluginDisabled(ClassLoader loader){
if(loader != null && pluginManager != null && pluginManager.getPluginConfiguration(loader) != null) {
return pluginManager.getPluginConfiguration(loader).isDisabledPlugin(pluginAnnotation.getPluginClass());
}
// can't tell
return false;
}
public boolean shouldCheckVersion(){
return pluginAnnotation.shouldCheckVersion();
}
public boolean isFallbackPlugin(){
return pluginAnnotation.isFallBack();
}
public String getPluginGroup() {
return pluginAnnotation.getGroup();
}
public boolean acceptsDefineEvent() {
return events.contains(LoadEvent.DEFINE);
}
public boolean versionMatches(ClassLoader loader){
if (pluginAnnotation.shouldCheckVersion()) {
DeploymentInfo info = DeploymentInfo.fromClassLoader(loader);
if (!pluginAnnotation.matches(info)) {
LOGGER.debug("SKIPPING METHOD: {}, Deployment info: {}\n did not match with {}\n or {}", pluginAnnotation.method, info, pluginAnnotation.methodMatcher, pluginAnnotation.pluginMatcher);
return false;
}
}
return true;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ((classBeingRedefined == null) ? !events.contains(LoadEvent.DEFINE) : !events.contains(LoadEvent.REDEFINE)) {
LOGGER.trace("Not a handled event!", events);
return classfileBuffer;
}
// check disabled plugins
// noinspection unchecked
if (pluginManager.getPluginConfiguration(loader).isDisabledPlugin(pluginAnnotation.getPluginClass())) {
LOGGER.trace("Plugin NOT enabled! {}", pluginAnnotation);
return classfileBuffer;
}
return transform(pluginManager, pluginAnnotation, loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
}
@Override
public String toString() {
return "\n\t\t\tPluginClassFileTransformer [pluginAnnotation=" + pluginAnnotation + "]";
}
/**
* Creats javaassist CtClass for bytecode manipulation. Add default
* classloader.
*
* @param bytes new class definition
* @param classLoader loader
* @return created class
* @throws NotFoundException
*/
private static CtClass createCtClass(byte[] bytes, ClassLoader classLoader) throws IOException {
ClassPool cp = new ClassPool();
cp.appendSystemPath();
cp.appendClassPath(new LoaderClassPath(classLoader));
return cp.makeClass(new ByteArrayInputStream(bytes));
}
/**
* Skip proxy and javassist synthetic classes.
*/
protected static boolean isSyntheticClass(String className) {
return className.contains("$$_javassist") || className.startsWith("com/sun/proxy");
}
/**
* Transformation callback as registered in initMethod:
* hotswapTransformer.registerTransformer(). Resolve method parameters to
* actual values, provide convenience parameters of javassist to streamline
* the transformation.
*/
private static byte[] transform(PluginManager pluginManager, PluginAnnotation<OnClassLoadEvent> pluginAnnotation, ClassLoader classLoader, String className, Class<?> redefiningClass, ProtectionDomain protectionDomain, byte[] bytes) {
LOGGER.trace("Transforming.... '{}' using: '{}'", className, pluginAnnotation);
// skip synthetic classes
if (pluginAnnotation.getAnnotation().skipSynthetic()) {
if (isSyntheticClass(className) || (redefiningClass != null && redefiningClass.isSynthetic())) {
return bytes;
}
}
// skip anonymous class
if (pluginAnnotation.getAnnotation().skipAnonymous()) {
if (className.matches("\\$\\d+$")) {
return bytes;
}
}
// ensure classloader initiated
if (classLoader != null) {
pluginManager.initClassLoader(classLoader, protectionDomain);
}
// default result
byte[] result = bytes;
// we may need to crate CtClass on behalf of the client and close it
// after invocation.
CtClass ctClass = null;
List<Object> args = new ArrayList<Object>();
for (Class<?> type : pluginAnnotation.getMethod().getParameterTypes()) {
if (type.isAssignableFrom(ClassLoader.class)) {
args.add(classLoader);
} else if (type.isAssignableFrom(String.class)) {
args.add(className);
} else if (type.isAssignableFrom(Class.class)) {
args.add(redefiningClass);
} else if (type.isAssignableFrom(ProtectionDomain.class)) {
args.add(protectionDomain);
} else if (type.isAssignableFrom(byte[].class)) {
args.add(bytes);
} else if (type.isAssignableFrom(ClassPool.class)) {
ClassPool classPool = new ClassPool();
classPool.appendSystemPath();
LOGGER.trace("Adding loader classpath " + classLoader);
classPool.appendClassPath(new LoaderClassPath(classLoader));
args.add(classPool);
} else if (type.isAssignableFrom(CtClass.class)) {
try {
ctClass = createCtClass(bytes, classLoader);
args.add(ctClass);
} catch (IOException e) {
LOGGER.error("Unable create CtClass for '" + className + "'.", e);
return result;
}
} else if (type.isAssignableFrom(LoadEvent.class)) {
args.add(redefiningClass == null ? LoadEvent.DEFINE : LoadEvent.REDEFINE);
} else if (type.isAssignableFrom(AppClassLoaderExecutor.class)) {
args.add(new AppClassLoaderExecutor(classLoader, protectionDomain));
} else {
LOGGER.error("Unable to call init method on plugin '" + pluginAnnotation.getPluginClass() + "'." + " Method parameter type '" + type + "' is not recognized for @Init annotation.");
return result;
}
}
try {
// call method on plugin (or if plugin null -> static method)
Object resultObject = pluginAnnotation.getMethod().invoke(pluginAnnotation.getPlugin(), args.toArray());
if (resultObject == null) {
// Ok, nothing has changed
} else if (resultObject instanceof byte[]) {
result = (byte[]) resultObject;
} else if (resultObject instanceof CtClass) {
result = ((CtClass) resultObject).toBytecode();
// detach on behalf of the clinet - only if this is another
// instance than we created (it is closed elsewhere)
if (resultObject != ctClass) {
((CtClass) resultObject).detach();
}
} else {
LOGGER.error("Unknown result of @OnClassLoadEvent method '" + result.getClass().getName() + "'.");
}
// close CtClass if created from here
if (ctClass != null) {
// if result not set from the method, use class
if (resultObject == null) {
result = ctClass.toBytecode();
}
ctClass.detach();
}
} catch (IllegalAccessException e) {
LOGGER.error("IllegalAccessException in transform method on plugin '" + pluginAnnotation.getPluginClass() + "' class '" + className + "'.", e);
} catch (InvocationTargetException e) {
LOGGER.error("InvocationTargetException in transform method on plugin '" + pluginAnnotation.getPluginClass() + "' class '" + className + "'.", e);
} catch (CannotCompileException e) {
LOGGER.error("Cannot compile class after manipulation on plugin '" + pluginAnnotation.getPluginClass() + "' class '" + className + "'.", e);
} catch (IOException e) {
LOGGER.error("IOException in transform method on plugin '" + pluginAnnotation.getPluginClass() + "' class '" + className + "'.", e);
}
return result;
}
}