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