package org.hotswap.agent.annotation.handler;
import org.hotswap.agent.annotation.FileEvent;
import org.hotswap.agent.annotation.OnClassFileEvent;
import org.hotswap.agent.annotation.OnResourceFileEvent;
import org.hotswap.agent.command.MergeableCommand;
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.IOUtils;
import org.hotswap.agent.watch.WatchFileEvent;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Command to schedule after resource change.
* <p/>
* Equals is declared on all command params to group same change events to a single onWatchEvent. For event
* only the URI is compared to group multiple event types.
*/
public class WatchEventCommand<T extends Annotation> extends MergeableCommand {
private static AgentLogger LOGGER = AgentLogger.getLogger(WatchEventCommand.class);
private final PluginAnnotation<T> pluginAnnotation;
private final WatchEventDTO watchEventDTO;
private final WatchFileEvent event;
private final ClassLoader classLoader;
public WatchEventCommand(PluginAnnotation<T> pluginAnnotation, WatchFileEvent event, ClassLoader classLoader) {
this.pluginAnnotation = pluginAnnotation;
this.watchEventDTO = WatchEventDTO.parse(pluginAnnotation.getAnnotation());
this.event = event;
this.classLoader = classLoader;
}
@Override
public void executeCommand() {
LOGGER.trace("Executing for pluginAnnotation={}, event={} at classloader {}", pluginAnnotation, event, classLoader);
onWatchEvent(pluginAnnotation, event, classLoader);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WatchEventCommand that = (WatchEventCommand) o;
if (classLoader != null ? !classLoader.equals(that.classLoader) : that.classLoader != null) return false;
if (event != null ? !event.equals(that.event) : that.event != null) return false;
if (pluginAnnotation != null ? !pluginAnnotation.equals(that.pluginAnnotation) : that.pluginAnnotation != null)
return false;
return true;
}
@Override
public int hashCode() {
int result = pluginAnnotation != null ? pluginAnnotation.hashCode() : 0;
result = 31 * result + (event != null ? event.hashCode() : 0);
result = 31 * result + (classLoader != null ? classLoader.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "WatchEventCommand{" +
"pluginAnnotation=" + pluginAnnotation +
", event=" + event +
", classLoader=" + classLoader +
'}';
}
/**
* Run plugin the method.
*/
public void onWatchEvent(PluginAnnotation<T> pluginAnnotation, WatchFileEvent event, ClassLoader classLoader) {
final T annot = pluginAnnotation.getAnnotation();
Object plugin = pluginAnnotation.getPlugin();
// regular files filter
if (watchEventDTO.isOnlyRegularFiles() && !event.isFile()) {
LOGGER.trace("Skipping URI {} because it is not a regular file.", event.getURI());
return;
}
// watch type filter
if (!Arrays.asList(watchEventDTO.getEvents()).contains(event.getEventType())) {
LOGGER.trace("Skipping URI {} because it is not a requested event.", event.getURI());
return;
}
// resource name filter regexp
if (watchEventDTO.getFilter() != null && watchEventDTO.getFilter().length() > 0) {
if (!event.getURI().toString().matches(watchEventDTO.getFilter())) {
LOGGER.trace("Skipping URI {} because it does not match filter.", event.getURI(), watchEventDTO.getFilter());
return;
}
}
//we may need to crate CtClass on behalf of the client and close it after invocation.
CtClass ctClass = null;
// class file regexp
if (watchEventDTO.isClassFileEvent()) {
try {
// TODO creating class only to check name may slow down if lot of handlers is in use.
ctClass = createCtClass(event.getURI(), classLoader);
} catch (Exception e) {
LOGGER.error("Unable create CtClass for URI '{}'.", e, event.getURI());
return;
}
// unable to create CtClass or it's name does not match
if (ctClass == null || !ctClass.getName().matches(watchEventDTO.getClassNameRegexp()))
return;
}
LOGGER.debug("Executing resource changed method {} on class {} for event {}",
pluginAnnotation.getMethod().getName(), plugin.getClass().getName(), event);
List<Object> args = new ArrayList<Object>();
for (Class<?> type : pluginAnnotation.getMethod().getParameterTypes()) {
if (type.isAssignableFrom(ClassLoader.class)) {
args.add(classLoader);
} else if (type.isAssignableFrom(URI.class)) {
args.add(event.getURI());
} else if (type.isAssignableFrom(URL.class)) {
try {
args.add(event.getURI().toURL());
} catch (MalformedURLException e) {
LOGGER.error("Unable to convert URI '{}' to URL.", e, event.getURI());
return;
}
} else if (type.isAssignableFrom(ClassPool.class)) {
args.add(ClassPool.getDefault());
} else if (type.isAssignableFrom(FileEvent.class)) {
args.add(event.getEventType());
} else if (watchEventDTO.isClassFileEvent() && type.isAssignableFrom(CtClass.class)) {
args.add(ctClass);
} else if (watchEventDTO.isClassFileEvent() && type.isAssignableFrom(String.class)) {
args.add(ctClass.getName());
} else {
LOGGER.error("Unable to call method {} on plugin {}. Method parameter type {} is not recognized.",
pluginAnnotation.getMethod().getName(), plugin.getClass().getName(), type);
return;
}
}
try {
pluginAnnotation.getMethod().invoke(plugin, args.toArray());
// close CtClass if created from here
if (ctClass != null) {
ctClass.detach();
}
} catch (IllegalAccessException e) {
LOGGER.error("IllegalAccessException in method {} on plugin {}", e,
pluginAnnotation.getMethod().getName(), plugin.getClass().getName());
} catch (InvocationTargetException e) {
LOGGER.error("InvocationTargetException in method {} on plugin {}", e,
pluginAnnotation.getMethod().getName(), plugin.getClass().getName());
}
}
/**
* Creats javaassist CtClass for bytecode manipulation. Add default classloader.
*
* @param uri uri
* @param classLoader loader
* @return created class
* @throws org.hotswap.agent.javassist.NotFoundException
*/
private CtClass createCtClass(URI uri, ClassLoader classLoader) throws NotFoundException, IOException {
ClassPool cp = new ClassPool();
cp.appendClassPath(new LoaderClassPath(classLoader));
return cp.makeClass(new ByteArrayInputStream(IOUtils.toByteArray(uri)));
}
}