All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hotswap.agent.plugin.owb.OwbPlugin Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
/*
 * Copyright 2013-2024 the HotswapAgent authors.
 *
 * This file is part of HotswapAgent.
 *
 * HotswapAgent is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 2 of the License, or (at your
 * option) any later version.
 *
 * HotswapAgent is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with HotswapAgent. If not, see http://www.gnu.org/licenses/.
 */
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.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
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.plugin.owb.transformer.AbstractProducerTransformer;
import org.hotswap.agent.plugin.owb.transformer.BeansDeployerTransformer;
import org.hotswap.agent.plugin.owb.transformer.CdiContextsTransformer;
import org.hotswap.agent.plugin.owb.transformer.ProxyFactoryTransformer;
import org.hotswap.agent.util.AnnotationHelper;
import org.hotswap.agent.util.IOUtils;
import org.hotswap.agent.util.classloader.ClassLoaderHelper;
import org.hotswap.agent.util.signature.ClassSignatureComparerHelper;
import org.hotswap.agent.util.signature.ClassSignatureElement;
import org.hotswap.agent.watch.WatchEventListener;
import org.hotswap.agent.watch.WatchFileEvent;
import org.hotswap.agent.watch.Watcher;

/**
 * OwbPlugin (OpenWebBeans)
 *
 * @author Vladimir Dvorak
 */
@Plugin(name = "Owb",
        description = "OpenWebBeans framework(http://openwebbeans.apache.org/). Reload, reinject bean, redefine proxy class after bean class definition/redefinition.",
        testedVersions = {"1.7.0-2.0.16"},
        expectedVersions = {"All between 1.7.0-2.0.16"},
        supportClass = { BeansDeployerTransformer.class, CdiContextsTransformer.class, ProxyFactoryTransformer.class, AbstractProducerTransformer.class })
public class OwbPlugin {

    private static AgentLogger LOGGER = AgentLogger.getLogger(OwbPlugin.class);

    private static final String VETOED_ANNOTATION = "javax.enterprise.inject.Vetoed";
    private static final String DS_EXCLUDED_ANNOTATION = "org.apache.deltaspike.core.api.exclude.Exclude";

    // 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;

    private boolean initialized = false;

    private BeanReloadStrategy beanReloadStrategy;

    private int waitOnCreate = WAIT_ON_CREATE;
    private int waitOnRedefine = WAIT_ON_REDEFINE;

    private Map registeredArchives = new HashMap<>();

    /**
     * Plugin initialization, called from archive registration,
     */
    public void init() {
        if (!initialized) {
            LOGGER.info("Owb plugin initialized.");
            initialized = true;
            beanReloadStrategy = setBeanReloadStrategy(pluginConfiguration.getProperty("owb.beanReloadStrategy"));
            waitOnCreate = Integer.valueOf(pluginConfiguration.getProperty("owb.waitOnCreate", String.valueOf(WAIT_ON_CREATE)));
            waitOnRedefine = Integer.valueOf(pluginConfiguration.getProperty("owb.waitOnRedefine", String.valueOf(WAIT_ON_REDEFINE)));
        }
    }

    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 (final URL beanArchiveUrl : (Set) 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;
            }

            LOGGER.info("OWB: Registerering '{}' for changes....", archivePath);
            OwbPlugin.archivePath = archivePath; // store path for unit tests (single archive expected)

            try {
                URL archivePathUrl = resourceNameToURL(archivePath);

                if (registeredArchives.containsKey(archivePathUrl)) {
                    continue;
                }

                registeredArchives.put(archivePathUrl, beanArchiveUrl);

                URI uri = archivePathUrl.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, beanArchiveUrl, event), waitOnCreate);
                            }
                        }
                    }
                });
                LOGGER.info("Registered  watch for path '{}' for changes.", archivePathUrl);
            } 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)) {
            if (original != null) {
                LOGGER.trace("Skipping synthetic or inner class {}.", original.getName());
            }
            return;
        }

        if (AnnotationHelper.hasAnnotation(ctClass, VETOED_ANNOTATION)) {
            LOGGER.trace("Skipping @Vetoed class {}.", ctClass.getName());
            return;
        }

        if (AnnotationHelper.hasAnnotation(ctClass, DS_EXCLUDED_ANNOTATION)) {
            LOGGER.trace("Skipping @Excluded class {}.", ctClass.getName());
            return;
        }

        try {
            String classUrl = ctClass.getURL().toExternalForm();
            Iterator> iterator = registeredArchives.entrySet().iterator();
            while (iterator.hasNext()) {
                Entry entry = iterator.next();
                if (classUrl.startsWith(entry.getKey().toExternalForm())) {
                    LOGGER.debug("Class '{}' redefined in classLoader {}.", original.getName(), classLoader);
                    String oldSignForProxyCheck = OwbClassSignatureHelper.getSignatureForProxyClass(original);
                    String oldSignByStrategy = OwbClassSignatureHelper.getSignatureByStrategy(beanReloadStrategy, original);
                    String oldFullSignature = ClassSignatureComparerHelper.getJavaClassSignature(original, ClassSignatureElement.values());
                    scheduler.scheduleCommand(
                            new BeanClassRefreshCommand(appClassLoader,
                                    original.getName(),
                                    oldFullSignature,
                                    oldSignForProxyCheck,
                                    oldSignByStrategy,
                                    entry.getValue(),
                                    beanReloadStrategy),
                            waitOnRedefine
                            );
                    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("$$") || className.contains("$HibernateProxy$");
    }

    // 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();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy