package org.hotswap.agent.plugin.owb; 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.HashSet; import java.util.Set; 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.owb.command.BeanClassRefreshCommand; import org.hotswap.agent.util.IOUtils; 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; /** * OwbPlugin * * @author Vladimir Dvorak */ @Plugin(name = "Owb", description = "Open Web Beans framework(http://openwebbeans.apache.org/). Reload, reinject bean, redefine proxy class after bean class definition/redefinition.", testedVersions = {"1.7.0"}, expectedVersions = {"All between 1.7.0-1.7.0"}, supportClass = { BeansDeployerTransformer.class, CdiContextsTransformer.class, WebBeansContextsServiceTransformer.class, ProxyFactoryTransformer.class }) public class OwbPlugin { private static AgentLogger LOGGER = AgentLogger.getLogger(OwbPlugin.class); /** True for UnitTests */ static boolean isTestEnvironment = false; /** Store archive path for unit tests */ static String archivePath = null; /** * If a class is modified in IDE, sequence of multiple events is generated - * class file DELETE, CREATE, MODIFY, than Hotswap transformer is invoked. * ClassPath_ BeanRefreshCommand 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; BeanReloadStrategy beanReloadStrategy; Set<URL> registeredArchives = new HashSet<>(); /** * Plugin initialization, called from archive registration, */ public void init() { if (!initialized) { LOGGER.info("CDI/Owb plugin initialized."); initialized = true; beanReloadStrategy = setBeanReloadStrategy(pluginConfiguration.getProperty("owb.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 'owb.beanReloadStrategy' value: {} ", property); } } return ret; } /** * Register BeanArchive's paths 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 bdaLocations the Set of URLs of archive locations */ public void registerBeansXmls(Set bdaLocations) { // for all application resources watch for changes for (URL beanArchiveUrl : (Set<URL>) bdaLocations) { String beansXmlPath = beanArchiveUrl.getPath(); if (!beansXmlPath.endsWith("beans.xml")) { LOGGER.debug("Skipping bda location: {} ", beanArchiveUrl); continue; } final String archivePath; if (beansXmlPath.endsWith("META-INF/beans.xml")) { archivePath = beansXmlPath.substring(0, beansXmlPath.length() - "META-INF/beans.xml".length()); } else if (beansXmlPath.endsWith("WEB-INF/beans.xml")) { archivePath = beansXmlPath.substring(0, beansXmlPath.length() - "beans.xml".length()) + "classes"; } else { LOGGER.warning("Unexpected beans.xml location {}", beansXmlPath); continue; } if (archivePath.endsWith(".jar!/")) { LOGGER.debug("Skipping unsupported jar beans.xml location {}", beansXmlPath); continue; } OwbPlugin.archivePath = archivePath; // store path for unit tests (single archive expected) try { URL archiveUrl = resourceNameToURL(archivePath); if (registeredArchives.contains(archiveUrl)) { continue; } registeredArchives.add(archiveUrl); URI uri = archiveUrl.toURI(); 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); scheduler.scheduleCommand(new BeanClassRefreshCommand(appClassLoader, archivePath, event), WAIT_ON_CREATE); } } } }); LOGGER.info("Registered watch for path '{}' for changes.", archiveUrl); } catch (URISyntaxException e) { LOGGER.error("Unable to watch path '{}' for changes.", e, archivePath); } catch (Exception e) { LOGGER.warning("registerBeanDeplArchivePath() exception : {}", e.getMessage()); } } } /** * Called on class redefinition. Class may be bean class * * @param classLoader the class loader in which class is redefined (Archive class loader) * @param ctClass the ct class * @param original the original */ @OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE) public void classReload(ClassLoader classLoader, CtClass ctClass, Class<?> original) { if (classLoader != appClassLoader) { LOGGER.debug("Attempt to redefine class {} in unsupported classLoader{}.", original.getName(), classLoader); return; } if (original != null && !isSyntheticCdiClass(ctClass.getName()) && !isInnerNonPublicStaticClass(ctClass)) { try { String classUrl = ctClass.getURL().toExternalForm(); for (URL archiveUrl : registeredArchives) { if (classUrl.startsWith(archiveUrl.toExternalForm())) { LOGGER.debug("Class {} redefined in classLoader {}.", original.getName(), classLoader); String oldSignForProxyCheck = OwbClassSignatureHelper.getSignatureForProxyClass(original); String oldSignByStrategy = OwbClassSignatureHelper.getSignatureByStrategy(beanReloadStrategy, original); scheduler.scheduleCommand(new BeanClassRefreshCommand(appClassLoader, original.getName(), oldSignForProxyCheck, oldSignByStrategy, beanReloadStrategy), WAIT_ON_REDEFINE); break; } } } catch (Exception e) { LOGGER.error("classReload() exception {}.", e, e.getMessage()); } } } // Return true if class is OWB synthetic class. // Owb proxies contains $$ // DeltaSpike's proxies contains "$$" private boolean isSyntheticCdiClass(String className) { return 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; } 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(); } } }