org.apache.openejb.maven.plugins.TomEEEmbeddedMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.openejb.maven.plugins.TomEEEmbeddedMojo.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package org.apache.openejb.maven.plugins;

import org.apache.catalina.LifecycleState;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.UndeployException;
import org.apache.openejb.assembler.classic.Assembler;
import org.apache.openejb.core.ParentClassLoaderFinder;
import org.apache.openejb.core.ProvidedClassLoaderFinder;
import org.apache.openejb.loader.Files;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.maven.util.MavenLogStreamFactory;
import org.apache.openejb.maven.util.XmlFormatter;
import org.apache.openejb.util.JuliLogStreamFactory;
import org.apache.tomee.catalina.TomEERuntimeException;
import org.apache.tomee.embedded.Configuration;
import org.apache.tomee.embedded.Container;
import org.apache.tomee.livereload.LiveReloadInstaller;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.util.FileUtils;

import javax.naming.NamingException;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.logging.SimpleFormatter;

/**
 * Run an Embedded TomEE.
 */
@Mojo(name = "run", requiresDependencyResolution = ResolutionScope.RUNTIME_PLUS_SYSTEM)
public class TomEEEmbeddedMojo extends AbstractMojo {
    @Parameter(defaultValue = "${project.packaging}")
    protected String packaging;

    /**
     * When not in classpath mode which war to deploy.
     */
    @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}")
    protected File warFile;

    /**
     * HTTP port.
     */
    @Parameter(property = "tomee-embedded-plugin.http", defaultValue = "8080")
    protected int httpPort;

    /**
     * HTTPS port if relevant.
     */
    @Parameter(property = "tomee-embedded-plugin.httpsPort", defaultValue = "8443")
    protected int httpsPort;

    /**
     * Shutdown port.
     */
    @Parameter(property = "tomee-embedded-plugin.stop", defaultValue = "8005")
    protected int stopPort;

    /**
     * Server host.
     */
    @Parameter(property = "tomee-embedded-plugin.host", defaultValue = "localhost")
    protected String host;

    /**
     * Temporary working directory.
     */
    @Parameter(property = "tomee-embedded-plugin.dir", defaultValue = "${project.build.directory}/apache-tomee-embedded")
    protected String dir;

    /**
     * For https connector the keystore location.
     */
    @Parameter(property = "tomee-embedded-plugin.keystoreFile")
    protected String keystoreFile;

    /**
     * For https connector the keystore password.
     */
    @Parameter(property = "tomee-embedded-plugin.keystorePass")
    protected String keystorePass;

    /**
     * For https connector the keystore type.
     */
    @Parameter(property = "tomee-embedded-plugin.keystoreType", defaultValue = "JKS")
    protected String keystoreType;

    /**
     * For https connector if client auth is activated.
     */
    @Parameter(property = "tomee-embedded-plugin.clientAuth")
    protected String clientAuth;

    /**
     * For https connector the keystore alias to use.
     */
    @Parameter(property = "tomee-embedded-plugin.keyAlias")
    protected String keyAlias;

    /**
     * For https connector the SSL protocol.
     */
    @Parameter(property = "tomee-embedded-plugin.sslProtocol")
    protected String sslProtocol;

    /**
     * Where is the server.xml to use if provided.
     */
    @Parameter
    protected File serverXml;

    /**
     * Is https activated.
     */
    @Parameter(property = "tomee-embedded-plugin.ssl", defaultValue = "false")
    protected boolean ssl;

    /**
     * Is EJBd activated.
     */
    @Parameter(property = "tomee-embedded-plugin.withEjbRemote", defaultValue = "false")
    protected boolean withEjbRemote;

    /**
     * Should we use a fast but unsecured session id generation implementation.
     */
    @Parameter(property = "tomee-embedded-plugin.quickSession", defaultValue = "true")
    protected boolean quickSession;

    /**
     * Should we skip http connector (and rely only on other connectors if setup).
     */
    @Parameter(property = "tomee-embedded-plugin.skipHttp", defaultValue = "false")
    protected boolean skipHttp;

    /**
     * Deploy the classpath as a webapp (instead of deploying a war).
     */
    @Parameter(property = "tomee-embedded-plugin.classpathAsWar", defaultValue = "false")
    protected boolean classpathAsWar;

    /**
     * Use pom dependencies when classpathAsWar=true.
     */
    @Parameter(property = "tomee-embedded-plugin.useProjectClasspath", defaultValue = "true")
    protected boolean useProjectClasspath;

    /**
     * Used to deactivate tomcat web resources caching (useful to get F5 working).
     */
    @Parameter(property = "tomee-embedded-plugin.webResourceCached", defaultValue = "true")
    protected boolean webResourceCached;

    /**
     * Avoid to create multiple classloaders and use root one for the application.
     */
    @Parameter(property = "tomee-embedded-plugin.singleClassLoader", defaultValue = "false" /* for compat */)
    protected boolean singleClassLoader;

    /**
     * Support for reload command (ie redeploy the webapp by undeploying/deploying).
     */
    @Parameter(property = "tomee-embedded-plugin.forceReloadable", defaultValue = "true")
    protected boolean forceReloadable;

    /**
     * Additional modules.
     */
    @Parameter(property = "tomee-embedded-plugin.modules", defaultValue = "${project.build.outputDirectory}")
    protected List<File> modules;

    /**
     * Additional web resources (directories).
     */
    @Parameter(property = "tomee-embedded-plugin.web-resources")
    protected List<File> webResources;

    /**
     * Where is docBase/web resources.
     */
    @Parameter(property = "tomee-embedded-plugin.docBase", defaultValue = "${project.basedir}/src/main/webapp")
    protected File docBase;

    /**
     * Context name.
     */
    @Parameter(property = "tomee-embedded-plugin.context")
    protected String context;

    /**
     * Conf classpath folder.
     */
    @Parameter(property = "tomee-embedded-plugin.conf")
    protected String conf;

    /**
     * TomEE properties.
     */
    @Parameter // don't call it properties to avoid to break getConfig()
    protected Map<String, String> containerProperties;

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    private MavenProject project;

    /**
     * Should TomEE use maven logging system instead of default one.
     */
    @Parameter(property = "tomee-embedded-plugin.mavenLog", defaultValue = "true")
    private boolean mavenLog;

    /**
     * Don't try to update port/host in server.xml.
     */
    @Parameter(property = "tomee-embedded-plugin.keepServerXmlAsThis", defaultValue = "false")
    private boolean keepServerXmlAsThis;

    /**
     * User/Password map.
     */
    @Parameter
    private Map<String, String> users;

    /**
     * Role/users map.
     */
    @Parameter
    private Map<String, String> roles;

    /**
     * force webapp to be support JSP reloading.
     */
    @Parameter(property = "tomee-plugin.jsp-development", defaultValue = "true")
    private boolean forceJspDevelopment;

    @Component
    private ArtifactFactory factory;

    @Component
    private ArtifactResolver resolver;

    @Parameter(defaultValue = "${localRepository}", readonly = true)
    private ArtifactRepository local;

    @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true)
    private List<ArtifactRepository> remoteRepos;

    /**
     * Additional applications to deploy.
     */
    @Parameter
    private List<String> applications;

    /**
     * Scopes to take into account when deploying the project classpath.
     */
    @Parameter
    private List<String> applicationScopes;

    @Parameter(property = "tomee-plugin.skip-current-project", defaultValue = "false")
    private boolean skipCurrentProject;

    @Parameter(property = "tomee-plugin.application-copy", defaultValue = "${project.build.directory}/tomee-embedded/applications")
    private File applicationCopyFolder;

    @Parameter(property = "tomee-plugin.work", defaultValue = "${project.build.directory}/tomee-embedded-work")
    private File workDir;

    /**
     * serverl.xml content directly in the pom.xml.
     */
    @Parameter
    protected PlexusConfiguration inlinedServerXml;

    /**
     * tomee.xml directly in the pom.xml.
     */
    @Parameter
    protected PlexusConfiguration inlinedTomEEXml;

    /**
     * Advanced configuration for live reload (to change port, context...).
     */
    @Parameter //advanced config but a simple boolean will be used for defaults (withLiveReload)
    private LiveReload liveReload;

    /**
     * Use livereload.
     */
    @Parameter(property = "tomee-plugin.liveReload", defaultValue = "false")
    private boolean withLiveReload;

    /**
     * A list of js scripts executed before the container starts.
     */
    @Parameter
    protected List<String> jsCustomizers;

    /**
     * A list of groovy scripts executed before the container starts. Needs to add groovy as dependency.
     */
    @Parameter
    protected List<String> groovyCustomizers;

    private Map<String, Command> commands;
    private String deployedName;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (!classpathAsWar && "pom".equals(packaging)) {
            getLog().warn("this project is a pom, it is not deployable");
            return;
        }

        final Properties originalSystProp = new Properties();
        originalSystProp.putAll(System.getProperties());

        // we use MavenLogStreamFactory but if user set some JUL config in properties we want to respect them
        configureJULIfNeeded();

        final Thread thread = Thread.currentThread();
        final ClassLoader loader = thread.getContextClassLoader();

        final String logFactory = System.getProperty("openejb.log.factory");
        MavenLogStreamFactory.setLogger(getLog());
        if (mavenLog) {
            System.setProperty("openejb.log.factory", MavenLogStreamFactory.class.getName()); // this line also preload the class (<cinit>)
            System.setProperty("openejb.jul.forceReload", "true");
        }

        if (inlinedServerXml != null && inlinedServerXml.getChildCount() > 0) {
            if (serverXml != null && serverXml.exists()) {
                throw new MojoFailureException("you can't define a server.xml and an inlinedServerXml");
            }
            try {
                FileUtils.forceMkdir(workDir);
                serverXml = new File(workDir, "server.xml_dump");
                FileUtils.fileWrite(serverXml, XmlFormatter.format(inlinedServerXml.getChild(0).toString()));
            } catch (final Exception e) {
                throw new MojoExecutionException(e.getMessage(), e);
            }
        }

        final Container container = new Container() {
            @Override
            public void setup(final Configuration configuration) {
                super.setup(configuration);
                if (inlinedTomEEXml != null && inlinedTomEEXml.getChildCount() > 0) {
                    try {
                        final File conf = new File(dir, "conf");
                        FileUtils.forceMkdir(conf);
                        FileUtils.fileWrite(new File(conf, "tomee.xml"),
                                XmlFormatter.format(inlinedTomEEXml.getChild(0).toString()));
                    } catch (final Exception e) {
                        throw new TomEERuntimeException(e);
                    }
                }

                final String base = getBase().getAbsolutePath();
                scriptCustomization(jsCustomizers, "js", base);
                scriptCustomization(groovyCustomizers, "groovy", base);
            }
        };
        final Configuration config = getConfig();
        container.setup(config);

        final Thread hook = new Thread() {
            @Override
            public void run() {
                if (container.getTomcat() != null
                        && container.getTomcat().getServer().getState() != LifecycleState.DESTROYED) {
                    final Thread thread = Thread.currentThread();
                    final ClassLoader old = thread.getContextClassLoader();
                    thread.setContextClassLoader(ParentClassLoaderFinder.Helper.get());
                    try {
                        if (!classpathAsWar) {
                            container.undeploy(warFile.getAbsolutePath());
                        }
                        container.stop();
                    } catch (final NoClassDefFoundError noClassDefFoundError) {
                        // debug cause it is too late to shutdown properly so don't pollute logs
                        getLog().debug("can't stop TomEE", noClassDefFoundError);
                    } catch (final Exception e) {
                        getLog().error("can't stop TomEE", e);
                    } finally {
                        thread.setContextClassLoader(old);
                    }
                }
            }
        };
        hook.setName("TomEE-Embedded-ShutdownHook");

        try {
            container.start();
            SystemInstance.get().setComponent(ParentClassLoaderFinder.class, new ProvidedClassLoaderFinder(loader));

            Runtime.getRuntime().addShutdownHook(hook);

            deployedName = doDeploy(thread, loader, container, useProjectClasspath);

            if (applications != null && !applications.isEmpty()) {
                Files.mkdirs(applicationCopyFolder);

                for (final String app : applications) {
                    final String renameStr = "?name=";
                    final int nameIndex = app.lastIndexOf(renameStr);
                    final String coordinates = nameIndex > 0 ? app.substring(0, nameIndex) : app;
                    File file = mvnToFile(coordinates);
                    final String name = nameIndex > 0 ? app.substring(nameIndex + renameStr.length() + 1)
                            : file.getName();
                    if (applicationCopyFolder != null) {
                        final File copy = new File(applicationCopyFolder, name);
                        IO.copy(file, copy);
                        file = copy;
                    }
                    container.deploy(name, file);
                }
            }

            getLog().info("TomEE embedded started on " + config.getHost() + ":" + config.getHttpPort());
        } catch (final Exception e) {
            getLog().error("can't start TomEE", e);
        }

        installLiveReloadEndpointIfNeeded();

        try {
            String line;
            final Scanner scanner = newScanner();
            while ((line = scanner.nextLine()) != null) {
                switch (line.trim()) {
                case "exit":
                case "quit":
                    Runtime.getRuntime().removeShutdownHook(hook);
                    container.close();
                    return;
                case "reload":
                    reload(thread, loader, container);
                    break;
                default:
                    onMissingCommand(line);
                }
            }
        } catch (final Exception e) {
            Thread.interrupted();
        } finally {
            if (logFactory == null) {
                System.clearProperty("openejb.log.factory");
            } else {
                System.setProperty("openejb.log.factory", logFactory);
            }
            thread.setContextClassLoader(loader);
            System.setProperties(originalSystProp);
        }
    }

    private void scriptCustomization(final List<String> customizers, final String ext, final String base) {
        if (customizers == null || customizers.isEmpty()) {
            return;
        }
        final ScriptEngine engine = new ScriptEngineManager().getEngineByExtension(ext);
        if (engine == null) {
            throw new IllegalStateException(
                    "No engine for " + ext + ". Maybe add the JSR223 implementation as plugin dependency.");
        }
        for (final String js : customizers) {
            try {
                final SimpleBindings bindings = new SimpleBindings();
                bindings.put("catalinaBase", base);
                engine.eval(new StringReader(js), bindings);
            } catch (final ScriptException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
    }

    protected Scanner newScanner() {
        return new Scanner(System.in);
    }

    private String doDeploy(final Thread thread, final ClassLoader loader, final Container container,
            final boolean useProjectClasspath) throws OpenEJBException, IOException, NamingException {
        if (!skipCurrentProject) {
            if (!classpathAsWar) {
                final String name = '/' + (context == null ? warFile.getName() : context);
                container.deploy(name, warFile, true);
                return name;
            } else {
                if (useProjectClasspath) {
                    thread.setContextClassLoader(createClassLoader(loader));
                }
                container.deployClasspathAsWebApp(context, docBase, singleClassLoader);
            }
        }
        return context;
    }

    protected void onMissingCommand(final String line) {
        if (line == null) {
            return;
        }
        if (commands == null) { // lazy loading
            commands = new HashMap<>();
            for (final Command c : ServiceLoader.load(Command.class)) {
                commands.put(c.name(), c);
            }
        }
        { // direct command
            final Command c = commands.get(line.trim());
            if (c != null) {
                c.invoke(line);
                return;
            }
        }
        // else match by "startsWith" all possible commands
        for (final Map.Entry<String, Command> c : commands.entrySet()) {
            if (line.startsWith(c.getKey())) {
                c.getValue().invoke(line);
            }
        }
    }

    protected synchronized void reload(final Thread thread, final ClassLoader loader, final Container container)
            throws OpenEJBException, NamingException, IOException {
        getLog().info("Redeploying " + (deployedName == null ? '/' : deployedName));
        try {
            final Assembler assembler = SystemInstance.get().getComponent(Assembler.class);
            if (classpathAsWar) { // this doesn't track module names so no need to go through container.undeploy()
                assembler.destroyApplication(assembler.getDeployedApplications().iterator().next().path);
            } else {
                container.undeploy(deployedName);
            }
        } catch (final UndeployException e) {
            throw new IllegalStateException(e);
        }
        doDeploy(thread, loader, container, false/*already done*/);
        getLog().info("Redeployed " + (deployedName == null ? '/' : deployedName));
    }

    private void installLiveReloadEndpointIfNeeded() {
        if (withLiveReload && liveReload == null) {
            liveReload = new LiveReload();
        }
        if (liveReload != null) {
            LiveReloadInstaller.install(liveReload.getPath(), liveReload.getPort(),
                    liveReload.getWatchedFolder() == null ? docBase.getAbsolutePath()
                            : liveReload.getWatchedFolder());
        }
    }

    private File mvnToFile(final String lib) throws Exception {
        final String[] infos = lib.split(":");
        final String classifier;
        final String type;
        if (infos.length < 3) {
            throw new MojoExecutionException(
                    "format for librairies should be <groupId>:<artifactId>:<version>[:<type>[:<classifier>]]");
        }
        if (infos.length >= 4) {
            type = infos[3];
        } else {
            type = "war";
        }
        if (infos.length == 5) {
            classifier = infos[4];
        } else {
            classifier = null;
        }

        final Artifact artifact = factory.createDependencyArtifact(infos[0], infos[1],
                VersionRange.createFromVersion(infos[2]), type, classifier, "compile");
        resolver.resolve(artifact, remoteRepos, local);
        return artifact.getFile();
    }

    private void configureJULIfNeeded() {
        if (containerProperties != null
                && "true".equalsIgnoreCase(containerProperties.get("openejb.jul.forceReload"))) {
            System.getProperties().putAll(containerProperties);
            new JuliLogStreamFactory(); // easiest way to support forceReload, note this doesn't do that much ATM
            final String simpleFormat = containerProperties.get("java.util.logging.SimpleFormatter.format");
            if (simpleFormat != null) {
                try {
                    final Field field = SimpleFormatter.class.getDeclaredField("format");
                    field.setAccessible(true);
                    final int modifiers = field.getModifiers();
                    if (Modifier.isFinal(modifiers)) {
                        final Field modifiersField = Field.class.getDeclaredField("modifiers");
                        modifiersField.setAccessible(true);
                        modifiersField.setInt(field, modifiers & ~Modifier.FINAL);
                    }
                    field.set(null, simpleFormat);
                } catch (final Throwable ignored) {
                    // no-op: don't block for it
                }
            }
        }
    }

    private ClassLoader createClassLoader(final ClassLoader parent) {
        final List<URL> urls = new ArrayList<>();
        for (final Artifact artifact : (Set<Artifact>) project.getArtifacts()) {
            final String scope = artifact.getScope();
            if ((applicationScopes == null
                    && !(Artifact.SCOPE_COMPILE.equals(scope) || Artifact.SCOPE_RUNTIME.equals(scope)))
                    || (applicationScopes != null && !applicationScopes.contains(scope))) {
                continue;
            }
            try {
                urls.add(artifact.getFile().toURI().toURL());
            } catch (final MalformedURLException e) {
                getLog().warn("can't use artifact " + artifact.toString());
            }
        }
        if (modules != null) {
            for (final File file : modules) {
                if (file.exists()) {
                    try {
                        urls.add(file.toURI().toURL());
                    } catch (final MalformedURLException e) {
                        getLog().warn("can't use path " + file.getAbsolutePath());
                    }
                } else {
                    getLog().warn("can't find " + file.getAbsolutePath());
                }
            }
        }
        return urls.isEmpty() ? parent : new URLClassLoader(urls.toArray(new URL[urls.size()]), parent) {
            @Override
            public boolean equals(final Object obj) {
                return super.equals(obj) || parent.equals(obj); // fake container loader since we deploy the classpath normally (see tomee webapp loader)
            }
        };
    }

    private Configuration getConfig() { // lazy way but it works fine
        final Configuration config = new Configuration();
        for (final Field field : TomEEEmbeddedMojo.class.getDeclaredFields()) {
            try {
                final Field configField = Configuration.class.getDeclaredField(field.getName());
                field.setAccessible(true);
                configField.setAccessible(true);

                final Object value = field.get(this);
                if (value != null) {
                    configField.set(config, value);
                    getLog().debug("using " + field.getName() + " = " + value);
                }
            } catch (final NoSuchFieldException nsfe) {
                // ignored
            } catch (final Exception e) {
                getLog().warn("can't initialize attribute " + field.getName());
            }
        }
        if (containerProperties != null) {
            final Properties props = new Properties();
            props.putAll(containerProperties);
            config.setProperties(props);
        }
        if (forceJspDevelopment) {
            if (config.getProperties() == null) {
                config.setProperties(new Properties());
            }
            config.getProperties().put("tomee.jsp-development", "true");
        }
        if (forceReloadable) {
            if (config.getProperties() == null) {
                config.setProperties(new Properties());
            }
            config.getProperties().setProperty("tomee.force-reloadable", "true");
        }
        if (webResources != null && !webResources.isEmpty()) {
            for (final File f : webResources) {
                config.addCustomWebResources(f.getAbsolutePath());
            }
        }
        return config;
    }

    /**
     * A potential command identified by a name.
     *
     * Note that reload and quit/exit are built in commands.
     *
     * It is recommanded to prefix the command by something specific to your set of commands.
     */
    public interface Command {
        /**
         * @return the string to invoke this comamnd.
         */
        String name();

        /**
         * Executes this command.
         *
         * @param line the raw line entered by the user.
         */
        void invoke(String line);
    }
}