package org.hotswap.agent.plugin.osgiequinox;
import java.io.File;
import java.io.FileInputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import org.hotswap.agent.annotation.Init;
import org.hotswap.agent.annotation.LoadEvent;
import org.hotswap.agent.annotation.OnClassLoadEvent;
import org.hotswap.agent.annotation.Plugin;
import org.hotswap.agent.command.Command;
import org.hotswap.agent.command.Scheduler;
import org.hotswap.agent.config.PluginConfiguration;
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.CtConstructor;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.util.PluginManagerInvoker;
import org.hotswap.agent.util.classloader.ClassLoaderHelper;
import org.hotswap.agent.watch.WatchEventListener;
import org.hotswap.agent.watch.WatchFileEvent;
import org.hotswap.agent.watch.Watcher;
/**
* OSGI Equinox hotswap plugin. It watches class changes on extraClasspath and loads changed classes into appropriate equinox class loaders
*
* @author Vladimir Dvorak
*/
@Plugin(name = "OsgiEquinox",
description = "Supports hotswapping in OSGI/Equinox class loaders therefore it can be used for hotswap in Eclipse RCP plugin development. ",
testedVersions = {""},
expectedVersions = {""})
public class OsgiEquinoxPlugin {
private static AgentLogger LOGGER = AgentLogger.getLogger(OsgiEquinoxPlugin.class);
@Init
Scheduler scheduler;
@Init
PluginManager pluginManager;
@Init
PluginConfiguration pluginConfiguration;
@Init
Watcher watcher;
// synchronize on this map to wait for previous processing
final Map<Class<?>, byte[]> reloadMap = new HashMap<Class<?>, byte[]>();
private AutoHotswapPathEventListener listener;
private Set<ClassLoader> registeredEquinoxClassLoaders = Collections.newSetFromMap(new WeakHashMap<ClassLoader, Boolean>());
// command to do actual hotswap. Single command to merge possible multiple reload actions.
private Command hotswapCommand;
private String extraClasspath;
private boolean isDebugMode;
@OnClassLoadEvent(classNameRegexp = "org.eclipse.osgi.launch.Equinox")
public static void patchEquinox(CtClass ctClass) throws CannotCompileException {
String initializePlugin = PluginManagerInvoker.buildInitializePlugin(OsgiEquinoxPlugin.class);
String initializeThis = PluginManagerInvoker.buildCallPluginMethod(OsgiEquinoxPlugin.class, "initOsgiEquinox");
for (CtConstructor constructor : ctClass.getDeclaredConstructors()) {
constructor.insertAfter(initializePlugin);
constructor.insertAfter(initializeThis);
}
}
public void initOsgiEquinox() {
if (hotswapCommand != null)
return;
LOGGER.debug("Init OsgiEquinoxPlugin.");
extraClasspath = pluginConfiguration.getProperty("extraClasspath");
if (extraClasspath != null) {
String debugMode = pluginConfiguration.getProperty("osgiEquinox.debugMode");
isDebugMode = "true".equals(debugMode);
if (!isDebugMode) {
URL resource = null;
try {
resource = resourceNameToURL(extraClasspath.trim());
URI uri = resource.toURI();
LOGGER.info("Initialize hotswap on URL {}.", uri);
listener = new AutoHotswapPathEventListener(this);
watcher.addEventListener(null, uri, listener);
} catch (URISyntaxException e) {
LOGGER.error("Unable to watch path '{}' for changes.", e, resource);
} catch (Exception e) {
LOGGER.warning("initOsgiEquinox() exception : {}", e.getMessage());
}
if (resource != null) {
hotswapCommand = new Command() {
@Override
public void executeCommand() {
pluginManager.hotswap(reloadMap);
}
@Override
public String toString() {
return "pluginManager.hotswap(" + Arrays.toString(reloadMap.keySet().toArray()) + ")";
}
};
}
}
}
}
@OnClassLoadEvent(classNameRegexp = "org.eclipse.osgi.internal.loader.EquinoxClassLoader")
public static void patchEquinoxClassLoader(CtClass ctClass) throws CannotCompileException {
String registerClassLoader = PluginManagerInvoker.buildCallPluginMethod(OsgiEquinoxPlugin.class, "registerEquinoxClassLoader", "this", "java.lang.Object");
for (CtConstructor constructor : ctClass.getDeclaredConstructors()) {
constructor.insertAfter(registerClassLoader);
}
}
public void registerEquinoxClassLoader(Object equinoxClassLoader) {
LOGGER.debug("RegisterEquinoxClassLoader : " + equinoxClassLoader.getClass().getName());
registeredEquinoxClassLoaders.add((ClassLoader)equinoxClassLoader);
}
@OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE)
public void classReload(CtClass ctClass) {
// Hotswap is realized by event listener in the RUNTIME mode
if (!isDebugMode)
return;
try {
URL url = ctClass.getURL();
// Write content of class to extraClasspath, so classLoader.loadClass can load actual class
ctClass.writeFile(extraClasspath);
loadClassToTargetClassLoaders(ctClass, url.toURI(), false);
} catch (Exception e) {
LOGGER.warning("classReload() exception : {}", e.getMessage());
}
}
private void scheduleHotswapCommand() {
scheduler.scheduleCommand(hotswapCommand, 100, Scheduler.DuplicateSheduleBehaviour.SKIP);
}
private boolean loadClassToTargetClassLoaders(CtClass ctClass, URI uri, boolean putToReloadMap) {
List<ClassLoader> targetClassLoaders = getTargetLoaders(ctClass);
if (targetClassLoaders == null) {
LOGGER.trace("Class {} not loaded yet, no need for autoHotswap, skipped file {}", ctClass.getName());
return false;
}
LOGGER.debug("Class {} will be reloaded from URL {}", ctClass.getName(), uri);
ClassLoader classLoader = null;
try {
byte[] bytecode = ctClass.toBytecode();
for (int i=0; i < targetClassLoaders.size(); i++) {
classLoader = targetClassLoaders.get(i);
Class clazz = classLoader.loadClass(ctClass.getName());
if (putToReloadMap) {
synchronized (reloadMap) {
reloadMap.put(clazz, bytecode);
}
}
}
} catch (ClassNotFoundException e) {
LOGGER.warning("OsgiEquinox tries to reload class {}, which is not known to Equinox classLoader {}.",
ctClass.getName(), classLoader);
return false;
} catch (Exception e) {
LOGGER.warning("loadClassToTargetClassLoaders() exception : {}", e.getMessage());
return false;
}
return true;
}
private List<ClassLoader> getTargetLoaders(CtClass ctClass) {
List<ClassLoader> ret = null;
synchronized (registeredEquinoxClassLoaders) {
for (ClassLoader classLoader: registeredEquinoxClassLoaders) {
if (ClassLoaderHelper.isClassLoaded(classLoader, ctClass.getName())) {
if (ret == null)
ret = new ArrayList<ClassLoader>();
ret.add(classLoader);
}
}
}
return ret;
}
private URL resourceNameToURL(String resource) throws Exception {
try {
// Try to format as a URL?
return new URL(resource);
} catch (MalformedURLException e) {
// try to locate a file
if (resource.startsWith("./"))
resource = resource.substring(2);
File file = new File(resource).getCanonicalFile();
return file.toURI().toURL();
}
}
// AutoHotswapPathEventListener
private static class AutoHotswapPathEventListener implements WatchEventListener {
private OsgiEquinoxPlugin equinoxPlugin;
public AutoHotswapPathEventListener(OsgiEquinoxPlugin equinoxPlugin) {
this.equinoxPlugin = equinoxPlugin;
}
@Override
public void onEvent(WatchFileEvent event) {
ClassPool pool = ClassPool.getDefault();
if (!event.getURI().getPath().endsWith(".class")) {
return;
}
URI fileURI = event.getURI();
File classFile = new File(fileURI);
CtClass ctClass = null;
boolean doHotswap = false;
try {
ctClass = pool.makeClass(new FileInputStream(classFile));
doHotswap = equinoxPlugin.loadClassToTargetClassLoaders(ctClass, fileURI, true);
} catch (Exception e) {
LOGGER.warning("MakeClass exception : {}", e.getMessage());
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
if (doHotswap)
equinoxPlugin.scheduleHotswapCommand();
}
}
}