package org.hotswap.agent.plugin.weld;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
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.Scheduler;
import org.hotswap.agent.config.PluginConfiguration;
import org.hotswap.agent.javassist.CtClass;
import org.hotswap.agent.javassist.NotFoundException;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.plugin.weld.command.BdaAgentRegistry;
import org.hotswap.agent.plugin.weld.command.BeanClassRefreshCommand;
import org.hotswap.agent.util.IOUtils;
import org.hotswap.agent.util.ReflectionHelper;
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;
/**
* WeldPlugin
*
* @author Vladimir Dvorak
*/
@Plugin(name = "Weld",
description = "Weld framework(http://weld.cdi-spec.org/). Reload, reinject bean, redefine proxy class after bean class definition/redefinition.",
testedVersions = {"2.2.5-2.2.16, 2.3.x, 2.4.0"},
expectedVersions = {"All between 2.2.5 - 2.4.x"},
supportClass = {BeanDeploymentArchiveTransformer.class, ProxyFactoryTransformer.class, AbstractClassBeanTransformer.class, CdiContextsTransformer.class})
public class WeldPlugin {
private static AgentLogger LOGGER = AgentLogger.getLogger(WeldPlugin.class);
/** True for UnitTests */
static boolean isTestEnvironment = false;
/**
* If a class is modified in IDE, sequence of multiple events is generated -
* class file DELETE, CREATE, MODIFY, than Hotswap transformer is invoked.
* ClassPathBeanRefreshCommand tries to merge these events into single command.
* Wait for this this timeout(milliseconds) after class file event before ClassPathBeanRefreshCommand
*/
private static final int WAIT_ON_CREATE = 500;
private static final int WAIT_ON_REDEFINE = 200;
@Init
Watcher watcher;
@Init
Scheduler scheduler;
@Init
ClassLoader appClassLoader;
@Init
PluginConfiguration pluginConfiguration;
boolean initialized = false;
private Map<Object, Object> registeredProxiedBeans = new WeakHashMap<Object, Object>();
private BeanReloadStrategy beanReloadStrategy;
public void init() {
if (!initialized) {
LOGGER.info("CDI/Weld plugin initialized.");
doInit();
}
}
public void initInJBossAS() {
if (!initialized) {
LOGGER.info("CDI/Weld plugin initialized in JBossAS.");
doInit();
}
}
public void initInGlassFish() {
if (!initialized) {
LOGGER.info("CDI/Weld plugin initialized in GlassFish.");
doInit();
}
}
private void doInit() {
initialized = true;
beanReloadStrategy = setBeanReloadStrategy(pluginConfiguration.getProperty("weld.beanReloadStrategy"));
}
private BeanReloadStrategy setBeanReloadStrategy(String property) {
BeanReloadStrategy ret = BeanReloadStrategy.NEVER;
if (property != null && !property.isEmpty()) {
try {
ret = BeanReloadStrategy.valueOf(property);
} catch (Exception e) {
LOGGER.error("Unknown property 'weld.beanReloadStrategy' value: {} ", property);
}
}
return ret;
}
/**
* Register BeanDeploymentArchive's normalizedArchivePath to watcher. In case of new class, the class file is not known
* to JVM hence no hotswap is called and therefore it must be handled by watcher.
*
* @param archivePath the archive path
*/
public synchronized void registerBeanDeplArchivePath(final String archivePath) {
URL resource = null;
try {
resource = resourceNameToURL(archivePath);
URI uri = resource.toURI();
if (!IOUtils.isDirectoryURL(uri.toURL())) {
LOGGER.trace("Weld - unable to watch files on URL '{}' for changes (JAR file?)", archivePath);
return;
} else {
LOGGER.info("Registering archive path {}", archivePath);
watcher.addEventListener(appClassLoader, uri, new WatchEventListener() {
@Override
public void onEvent(WatchFileEvent event) {
if (event.isFile() && event.getURI().toString().endsWith(".class")) {
// check that the class is not loaded by the classloader yet (avoid duplicate reload)
String className;
try {
className = IOUtils.urlToClassName(event.getURI());
} catch (IOException e) {
LOGGER.trace("Watch event on resource '{}' skipped, probably Ok because of delete/create event sequence (compilation not finished yet).",
e, event.getURI());
return;
}
if (!ClassLoaderHelper.isClassLoaded(appClassLoader, className) || isTestEnvironment) {
// refresh weld only for new classes
LOGGER.trace("register reload command: {} ", className);
if (isBdaRegistered(appClassLoader, archivePath)) {
// TODO : Create proxy factory
scheduler.scheduleCommand(new BeanClassRefreshCommand(appClassLoader, archivePath, event), WAIT_ON_CREATE);
}
}
}
}
});
}
LOGGER.info("Registered watch for path '{}' for changes.", resource);
} catch (URISyntaxException e) {
LOGGER.error("Unable to watch path '{}' for changes.", e, archivePath);
} catch (Exception e) {
LOGGER.warning("registerBeanDeplArchivePath() exception : {}", e.getMessage());
}
}
private static boolean isBdaRegistered(ClassLoader classLoader, String archivePath) {
if (archivePath != null) {
try {
return (boolean) ReflectionHelper.invoke(null, Class.forName(BdaAgentRegistry.class.getName(), true, classLoader), "contains", new Class[] {String.class}, archivePath);
} catch (ClassNotFoundException e) {
LOGGER.error("isBdaRegistered() exception {}.", e.getMessage());
}
}
return false;
}
public void registerProxyFactory(Object proxyFactory, Object bean, ClassLoader classLoader, Class<?> proxiedBeanType) {
synchronized(registeredProxiedBeans) {
if (!registeredProxiedBeans.containsKey(bean)) {
LOGGER.debug("ProxyFactory for {} registered.", proxiedBeanType.getName());
}
registeredProxiedBeans.put(bean, proxyFactory);
}
}
/**
* If bda archive is defined for given class than new BeanClassRefreshCommand is created
*
* @param classLoader
* @param ctClass
* @param original
*/
@OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE)
public void classReload(ClassLoader classLoader, CtClass ctClass, Class<?> original) {
if (original != null && !isSyntheticCdiClass(ctClass.getName()) && !isInnerNonPublicStaticClass(ctClass)) {
try {
String archivePath = getArchivePath(classLoader, ctClass, original.getName());
LOGGER.debug("Class {} redefined for archive {} ", original.getName(), archivePath);
if (isBdaRegistered(classLoader, archivePath)) {
String oldSignatureForProxyCheck = WeldClassSignatureHelper.getSignatureForProxyClass(original);
String oldSignatureByStrategy = WeldClassSignatureHelper.getSignatureByStrategy(beanReloadStrategy, original);
scheduler.scheduleCommand(new BeanClassRefreshCommand(classLoader, archivePath, registeredProxiedBeans,
original.getName(), oldSignatureForProxyCheck, oldSignatureByStrategy, beanReloadStrategy), WAIT_ON_REDEFINE);
}
} catch (Exception e) {
LOGGER.error("classReload() exception {}.", e, e.getMessage());
}
}
}
private String getArchivePath(ClassLoader classLoader, CtClass ctClass, String knownClassName) throws NotFoundException {
try {
return (String) ReflectionHelper.invoke(null, Class.forName(BdaAgentRegistry.class.getName(), true, classLoader),
"getArchiveByClassName", new Class[] {String.class}, knownClassName);
} catch (ClassNotFoundException e) {
LOGGER.error("getArchivePath() exception {}.", e.getMessage());
}
String classFilePath = ctClass.getURL().getPath();
String className = ctClass.getName().replace(".", "/");
// archive path ends with '/', therefore we set end position before the '/' (-1)
String archivePath = classFilePath.substring(0, classFilePath.indexOf(className) - 1);
return (new File(archivePath)).toPath().toString();
}
public 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();
}
}
// Return true if class is CDI synthetic class.
// Weld proxies contains $Proxy$ and $$$
// DeltaSpike's proxies contains "$$"
private boolean isSyntheticCdiClass(String className) {
return className.contains("$Proxy$") || className.contains("$$");
}
// Non static inner class is not allowed to be bean class
private boolean isInnerNonPublicStaticClass(CtClass ctClass) {
try {
CtClass declaringClass = ctClass.getDeclaringClass();
if (declaringClass != null && (
(ctClass.getModifiers() & Modifier.STATIC) == 0 ||
(ctClass.getModifiers() & Modifier.PUBLIC) == 0)) {
return true;
}
} catch (NotFoundException e) {
// swallow exception
}
return false;
}
}