package org.hotswap.agent.plugin.elresolver;
import java.lang.reflect.Modifier;
import java.util.Collections;
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.Manifest;
import org.hotswap.agent.annotation.Maven;
import org.hotswap.agent.annotation.Name;
import org.hotswap.agent.annotation.OnClassLoadEvent;
import org.hotswap.agent.annotation.Plugin;
import org.hotswap.agent.annotation.Versions;
import org.hotswap.agent.command.Scheduler;
import org.hotswap.agent.javassist.CannotCompileException;
import org.hotswap.agent.javassist.CtClass;
import org.hotswap.agent.javassist.CtConstructor;
import org.hotswap.agent.javassist.CtField;
import org.hotswap.agent.javassist.CtMethod;
import org.hotswap.agent.javassist.CtNewMethod;
import org.hotswap.agent.javassist.NotFoundException;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.util.PluginManagerInvoker;
/**
* Clear javax.el.BeanELResolver cache after any class redefinition.
*
* @author Vladimir Dvorak
*/
@Plugin(name = "ELResolver",
group = "groupELResolver",
fallback = true,
description = "Purge BeanELResolver class cache on any class redefinition.",
testedVersions = {"2.2"},
expectedVersions = {"2.2"})
@Versions(
maven = {
//Jboss el 2
@Maven(value = "[1.0,)", artifactId = "jboss-el-api_2.2_spec", groupId = "org.jboss.spec.javax.el"),
//Juel
@Maven(value = "[2.0,)", artifactId = "juel", groupId = "de.odysseus.juel"),
//Jboss el 3.0
@Maven(value="[3.0,)", artifactId = "javax.el-api", groupId = "javax.el")
},
manifest = {
// Seam jboss
@Manifest(value="[1.0,)", versionName="JBoss-EL-Version", names ={@Name(key="JBoss-EL-Version", value=".*")}),
// Tomcat bundled EL (6-9)
@Manifest(value="[2.0,)",versionName = Name.SpecificationVersion, names={
@Name(key=Name.ImplementationTitle,value="javax.el"),
@Name(key=Name.ImplementationVendor, value="Apache.*Software.*Foundation")
}),
//Jetty 7,8
@Manifest(value="[2.0,)", versionName={Name.BundleVersion}, names={@Name(key=Name.BundleSymbolicName,value="javax.el")}),
//Jetty 9
@Manifest(value="[8.0,)", versionName={Name.BundleVersion}, names={
@Name(key=Name.BundleSymbolicName,value="org.mortbay.jasper.apache-el"),
@Name(key="Bundle-Vendor",value="Webtide")}),
// GlassFish
@Manifest(value="[3.0,)", versionName={Name.BundleVersion}, names={
@Name(key=Name.BundleSymbolicName,value="com.sun.el.javax.el"),
@Name(key="Bundle-Vendor",value="GlassFish Community")})
}
)
public class ELResolverPlugin {
private static AgentLogger LOGGER = AgentLogger.getLogger(ELResolverPlugin.class);
public static final String PURGE_CLASS_CACHE_METHOD_NAME = "__resetCache";
@Init
Scheduler scheduler;
Set<Object> registeredBeanELResolvers = Collections.newSetFromMap(new WeakHashMap<Object, Boolean>());
boolean jbossReflectionUtil = false;
public void registerJBossReflectionUtil() {
jbossReflectionUtil = true;
}
/**
* Hook on BeanELResolver class and for each instance:
* - ensure plugin is initialized
* - register instances using registerBeanELResolver() method
*/
@OnClassLoadEvent(classNameRegexp = "javax.el.BeanELResolver")
public static void beanELResolverRegisterVariable(CtClass ctClass) throws CannotCompileException {
String initPlugin = PluginManagerInvoker.buildInitializePlugin(ELResolverPlugin.class);
String registerThis = PluginManagerInvoker.buildCallPluginMethod(ELResolverPlugin.class, "registerBeanELResolver",
"this", "java.lang.Object");
for (CtConstructor constructor : ctClass.getDeclaredConstructors()) {
constructor.insertAfter(initPlugin);
constructor.insertAfter(registerThis);
}
boolean found = false;
if (checkJuelEL(ctClass)) {
found = true;
LOGGER.debug("JuelEL - javax.el.BeanELResolver - method added " + PURGE_CLASS_CACHE_METHOD_NAME + "(java.lang.ClassLoader classLoader). ");
} else if (checkApacheEL(ctClass)) {
found = true;
LOGGER.debug("ApacheEL - javax.el.BeanELResolver - method added " + PURGE_CLASS_CACHE_METHOD_NAME + "(java.lang.ClassLoader classLoader). ");
} else if (checkJBoss_3_0_EL(ctClass)) {
found = true;
LOGGER.debug("JBossEL 3.0 - javax.el.BeanELResolver - method added " + PURGE_CLASS_CACHE_METHOD_NAME + "(java.lang.ClassLoader classLoader). ");
}
if (!found) {
LOGGER.warning("Unable to add javax.el.BeanELResolver." + PURGE_CLASS_CACHE_METHOD_NAME + "() method. Purging will not be available.");
}
}
@OnClassLoadEvent(classNameRegexp = "org.jboss.el.util.ReflectionUtil")
public static void patchJBossReflectionUtil(CtClass ctClass) throws NotFoundException, CannotCompileException {
CtField ctField = new CtField(CtClass.booleanType, "__haInitialized", ctClass);
ctField.setModifiers(org.hotswap.agent.javassist.Modifier.PRIVATE | org.hotswap.agent.javassist.Modifier.STATIC);
ctClass.addField(ctField, CtField.Initializer.constant(false));
String buildInitializePlugin = PluginManagerInvoker.buildInitializePlugin(ELResolverPlugin.class, "base.getClass().getClassLoader()");
String registerJBossReflectionUtil = PluginManagerInvoker.buildCallPluginMethod("base.getClass().getClassLoader()", ELResolverPlugin.class, "registerJBossReflectionUtil");
StringBuilder src = new StringBuilder("{");
src.append(" if(!__haInitialized) {");
src.append(" __haInitialized=true;");
src.append(" " + buildInitializePlugin);
src.append(" " + registerJBossReflectionUtil);
src.append(" }");
src.append("}");
CtMethod mFindMethod = ctClass.getDeclaredMethod("findMethod");
mFindMethod.insertAfter(src.toString());
LOGGER.debug("org.jboss.el.util.ReflectionUtil enhanced with resource bundles registration.");
}
private static boolean checkJuelEL(CtClass ctClass) {
try {
// JUEL, (JSF BeanELResolver[s])
// check if we have purgeBeanClasses method
CtMethod purgeMeth = ctClass.getDeclaredMethod("purgeBeanClasses");
ctClass.addMethod(CtNewMethod.make("public void " + PURGE_CLASS_CACHE_METHOD_NAME + "(java.lang.ClassLoader classLoader) {" +
" purgeBeanClasses(classLoader); " +
"}", ctClass));
return true;
} catch (NotFoundException | CannotCompileException e) {
// purgeBeanClasses method not found -do nothing
}
return false;
}
private static boolean checkApacheEL(CtClass ctClass)
{
try {
CtField field = ctClass.getField("cache");
// Apache BeanELResolver (has cache property)
ctClass.addField(new CtField(CtClass.booleanType, "__purgeRequested", ctClass), CtField.Initializer.constant(false));
ctClass.addMethod(CtNewMethod.make("public void " + PURGE_CLASS_CACHE_METHOD_NAME + "(java.lang.ClassLoader classLoader) {" +
" __purgeRequested=true;" +
"}", ctClass));
CtMethod mGetBeanProperty = ctClass.getDeclaredMethod("property");
mGetBeanProperty.insertBefore(
" if(__purgeRequested) {" +
" __purgeRequested=false;" +
" this.cache = new javax.el.BeanELResolver.ConcurrentCache(CACHE_SIZE); " +
" }");
return true;
} catch(NotFoundException e1) {
} catch (CannotCompileException e2) {
}
return false;
}
private static boolean checkJBoss_3_0_EL(CtClass ctClass) {
// JBoss EL Resolver - is recognized by "javax.el.BeanELResolver.properties" property
try {
CtField field = ctClass.getField("properties");
if ((field.getModifiers() & Modifier.STATIC) != 0) {
field.setModifiers(Modifier.STATIC);
patchJBossEl(ctClass);
}
return true;
} catch (NotFoundException e1) {
// do nothing
}
return false;
}
/*
* JBossEL has weak reference cache. Values are stored in ThreadGroupContext cache, that must be flushed from appropriate thread.
* Therefore we must create request for cleanup cache in PURGE_CLASS_CACHE_METHOD and own cleanup is executed indirectly when
* application calls getBeanProperty(...).
*/
private static void patchJBossEl(CtClass ctClass) {
try {
ctClass.addField(new CtField(CtClass.booleanType, "__purgeRequested", ctClass), CtField.Initializer.constant(false));
ctClass.addMethod(CtNewMethod.make("public void " + PURGE_CLASS_CACHE_METHOD_NAME + "(java.lang.ClassLoader classLoader) {" +
" __purgeRequested=true;" +
"}", ctClass));
try {
CtMethod mGetBeanProperty = ctClass.getDeclaredMethod("getBeanProperty");
mGetBeanProperty.insertBefore(
" if(__purgeRequested) {" +
" __purgeRequested=false;" +
" java.lang.reflect.Method meth = javax.el.BeanELResolver.SoftConcurrentHashMap.class.getDeclaredMethod(\"__createNewInstance\", null);" +
" properties = (javax.el.BeanELResolver.SoftConcurrentHashMap) meth.invoke(properties, null);" +
" }");
} catch (NotFoundException e) {
LOGGER.debug("FIXME : checkJBoss_3_0_EL() 'getBeanProperty(...)' not found in javax.el.BeanELResolver.");
}
} catch (CannotCompileException e) {
LOGGER.error("patchJBossEl() exception {}", e.getMessage());
}
}
@OnClassLoadEvent(classNameRegexp = "javax.el.BeanELResolver\\$SoftConcurrentHashMap")
public static void patchJbossElSoftConcurrentHashMap(CtClass ctClass) throws CannotCompileException {
try {
ctClass.addMethod(CtNewMethod.make("public javax.el.BeanELResolver.SoftConcurrentHashMap __createNewInstance() {" +
" return new javax.el.BeanELResolver.SoftConcurrentHashMap();" +
"}", ctClass));
} catch (CannotCompileException e) {
LOGGER.error("patchJbossElSoftConcurrentHashMap() exception {}", e.getMessage());
}
}
public void registerBeanELResolver(Object beanELResolver) {
registeredBeanELResolvers.add(beanELResolver);
LOGGER.debug("ELResolverPlugin - BeanELResolver registered : " + beanELResolver.getClass().getName());
}
@OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE)
public void invalidateClassCache(ClassLoader appClassLoader, CtClass ctClass) throws Exception {
if (jbossReflectionUtil) {
PurgeJbossReflectionUtil jbossCleanCmd = new PurgeJbossReflectionUtil(appClassLoader);
scheduler.scheduleCommand(jbossCleanCmd);
}
PurgeBeanELResolverCacheCommand cmd = new PurgeBeanELResolverCacheCommand(appClassLoader, registeredBeanELResolvers);
scheduler.scheduleCommand(cmd);
}
}