org.eclipse.jetty.maven.plugin.AbstractWebAppMojo Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.maven.plugin;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.Resource;
/**
* AbstractWebAppMojo
*
* Base class for common behaviour of jetty mojos.
*/
public abstract class AbstractWebAppMojo extends AbstractMojo
{
public static final String JETTY_HOME_GROUPID = "org.eclipse.jetty";
public static final String JETTY_HOME_ARTIFACTID = "jetty-home";
public static final String FAKE_WEBAPP = "webapp-synth";
public enum DeploymentMode
{
EMBED,
FORK,
HOME, //alias for EXTERNAL
DISTRO, //alias for EXTERNAL
EXTERNAL
}
/**
* Max number of times to check to see if jetty has started correctly
* when running in FORK or EXTERNAL mode.
*/
@Parameter (defaultValue = "10")
protected int maxChildStartChecks;
/**
* How long to wait in msec between checks to see if jetty has started
* correctly when running in FORK or EXTERNAL mode.
*/
@Parameter (defaultValue = "200")
protected long maxChildStartCheckMs;
/**
* Whether or not to include dependencies on the plugin's classpath with <scope>provided</scope>
* Use WITH CAUTION as you may wind up with duplicate jars/classes.
*
* @since jetty-7.5.2
*/
@Parameter (defaultValue = "false")
protected boolean useProvidedScope;
/**
* List of goals that are NOT to be used
*
* @since jetty-7.5.2
*/
@Parameter
protected String[] excludedGoals;
/**
* An instance of org.eclipse.jetty.webapp.WebAppContext that represents the webapp.
* Use any of its setters to configure the webapp. This is the preferred and most
* flexible method of configuration, rather than using the (deprecated) individual
* parameters like "tmpDirectory", "contextPath" etc.
*
*/
@Parameter
protected MavenWebAppContext webApp;
/**
* Skip this mojo execution.
*/
@Parameter (property = "jetty.skip", defaultValue = "false")
protected boolean skip;
/**
* Location of a context xml configuration file whose contents
* will be applied to the webapp AFTER anything in <webApp>.Optional.
*/
@Parameter
protected String contextXml;
/**
* The maven project.
*/
@Parameter(defaultValue = "${project}", readonly = true)
protected MavenProject project;
/**
* The artifacts for the project.
*/
@Parameter (defaultValue = "${project.artifacts}", readonly = true)
protected Set projectArtifacts;
/**
* The maven build executing.
*/
@Parameter (defaultValue = "${mojoExecution}", readonly = true)
protected org.apache.maven.plugin.MojoExecution execution;
/**
* The artifacts for the plugin itself.
*/
@Parameter (defaultValue = "${plugin.artifacts}", readonly = true)
protected List pluginArtifacts;
/**
* If true, the <testOutputDirectory>
* and the dependencies of <scope>test<scope>
* will be put first on the runtime classpath.
*/
@Parameter (defaultValue = "false")
protected boolean useTestScope;
/**
* List of directories with ant-style <include> and <exclude> patterns
* for extra targets to periodically scan for changes.Optional.
*/
@Parameter
protected List scanTargetPatterns;
@Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
protected List reactorProjects;
/**
* The target directory
*/
@Parameter (defaultValue = "${project.build.directory}", required = true, readonly = true)
protected File target;
/**
* List of jetty xml configuration files whose contents
* will be applied (in order declared) before any plugin configuration. Optional.
*/
@Parameter
protected List jettyXmls;
/**
* Optional jetty properties to put on the command line
*/
@Parameter
protected Map jettyProperties;
/**
* File containing system properties to be set before execution
*
* Note that these properties will NOT override System properties
* that have been set on the command line, by the JVM, or directly
* in the POM via systemProperties. Optional.
*
*
*/
@Parameter (property = "jetty.systemPropertiesFile")
protected File systemPropertiesFile;
/**
* System properties to set before execution.
* Note that these properties will NOT override System properties
* that have been set on the command line or by the JVM. They WILL
* override System properties that have been set via systemPropertiesFile.
* Optional.
*/
@Parameter
protected Map systemProperties;
/**
* Controls how to run jetty. Valid values are EMBED,FORK,EXTERNAL.
*/
@Parameter (property = "jetty.deployMode", defaultValue = "EMBED")
protected DeploymentMode deployMode;
/**
* List of other contexts to set up. Consider using instead
* the <jettyXml> element to specify external jetty xml config file.
* Optional.
*/
@Parameter
protected List contextHandlers;
/**
* List of security realms to set up. Consider using instead
* the <jettyXml> element to specify external jetty xml config file.
* Optional.
*/
@Parameter
protected List loginServices;
/**
* A RequestLog implementation to use for the webapp at runtime.
* Consider using instead the <jettyXml> element to specify external jetty xml config file.
* Optional.
*/
@Parameter
protected RequestLog requestLog;
/**
* A ServerConnector to use.
*/
@Parameter
protected MavenServerConnector httpConnector;
/**
* A wrapper for the Server object
*/
@Parameter
protected Server server;
//End of EMBED only
//Start of parameters only valid for FORK/EXTERNAL
/**
* Extra environment variables to be passed to the forked process
*/
@Parameter
protected Map env = new HashMap<>();
/**
* Arbitrary jvm args to pass to the forked process
*/
@Parameter (property = "jetty.jvmArgs")
protected String jvmArgs;
/**
* Port to listen to stop jetty on executing -DSTOP.PORT=<stopPort>
* -DSTOP.KEY=<stopKey> -jar start.jar --stop
*
*/
@Parameter
protected int stopPort;
/**
* Key to provide when stopping jetty on executing java -DSTOP.KEY=<stopKey>
* -DSTOP.PORT=<stopPort> -jar start.jar --stop
*
*/
@Parameter
protected String stopKey;
//End of FORK or EXTERNAL parameters
//Start of parameters only valid for EXTERNAL
/**
* Location of jetty home directory
*/
@Parameter
protected File jettyHome;
/**
* Location of jetty base directory
*/
@Parameter
protected File jettyBase;
/**
* Optional list of other modules to
* activate.
*/
@Parameter
protected String[] modules;
/**
* Extra options that can be passed to the jetty command line
*/
@Parameter (property = "jetty.options")
protected String jettyOptions;
//End of EXTERNAL only parameters
//Start of parameters only valid for FORK
/**
* The file into which to generate the quickstart web xml for the forked process to use
*
*/
@Parameter (defaultValue = "${project.build.directory}/fork-web.xml")
protected File forkWebXml;
//End of FORK only parameters
/**
* Helper for interacting with the maven project space
*/
protected MavenProjectHelper mavenProjectHelper;
/**
* This plugin
*/
@Parameter (defaultValue = "${plugin}", readonly = true, required = true)
protected PluginDescriptor plugin;
/**
* The project's remote repositories to use for the resolution.
*/
@Parameter (defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true)
private List remoteRepositories;
/**
*
*/
@Component
private RepositorySystem repositorySystem;
/**
* The current maven session
*/
@Parameter (defaultValue = "${session}", required = true, readonly = true)
private MavenSession session;
/**
* Default supported project type is war
packaging.
*/
@Parameter
protected List supportedPackagings = Collections.singletonList("war");
/**
* List of deps that are wars
*/
protected List warArtifacts;
/**
* Webapp base before applying overlays etc
*/
protected Resource originalBaseResource;
/**
* List of jars with scope=provided
*/
protected List providedJars;
/**
* System properties from both systemPropertyFile and systemProperties.
*/
protected Map mergedSystemProperties;
@Override
public void execute() throws MojoExecutionException, MojoFailureException
{
if (isPackagingSupported())
{
if (skip)
{
getLog().info("Skipping Jetty start: jetty.skip==true");
return;
}
if (isExcludedGoal(execution.getMojoDescriptor().getGoal()))
{
getLog().info("The goal \"" + execution.getMojoDescriptor().getFullGoalName() +
"\" is unavailable for this web app because of an configuration.");
return;
}
getLog().info("Configuring Jetty for project: " + getProjectName());
mavenProjectHelper = new MavenProjectHelper(project, repositorySystem, remoteRepositories, session);
mergedSystemProperties = mergeSystemProperties();
configureSystemProperties();
augmentPluginClasspath();
PluginLog.setLog(getLog());
verifyPomConfiguration();
startJetty();
}
else
getLog().info("Packaging type [" + project.getPackaging() + "] is unsupported");
}
protected void startJetty()
throws MojoExecutionException, MojoFailureException
{
try
{
configureWebApp();
}
catch (Exception e)
{
throw new MojoExecutionException("Webapp config failure", e);
}
switch (deployMode)
{
case EMBED:
{
startJettyEmbedded();
break;
}
case FORK:
{
startJettyForked();
break;
}
case DISTRO:
case HOME:
case EXTERNAL:
{
if (deployMode != DeploymentMode.EXTERNAL)
getLog().warn(deployMode + " mode is deprecated, use mode EXTERNAL");
startJettyHome();
break;
}
default:
throw new MojoExecutionException("Unrecognized runType=" + deployMode);
}
}
protected abstract void startJettyEmbedded() throws MojoExecutionException;
protected abstract void startJettyForked() throws MojoExecutionException;
protected abstract void startJettyHome() throws MojoExecutionException;
protected JettyEmbedder newJettyEmbedder()
throws Exception
{
JettyEmbedder jetty = new JettyEmbedder();
jetty.setStopKey(stopKey);
jetty.setStopPort(stopPort);
jetty.setServer(server);
jetty.setContextHandlers(contextHandlers);
jetty.setRequestLog(requestLog);
jetty.setJettyXmlFiles(jettyXmls);
jetty.setHttpConnector(httpConnector);
jetty.setJettyProperties(jettyProperties);
jetty.setLoginServices(loginServices);
jetty.setContextXml(contextXml);
jetty.setWebApp(webApp);
return jetty;
}
protected JettyForker newJettyForker()
throws Exception
{
JettyForker jetty = new JettyForker();
jetty.setServer(server);
jetty.setWorkDir(project.getBasedir());
jetty.setStopKey(stopKey);
jetty.setStopPort(stopPort);
jetty.setEnv(env);
jetty.setJvmArgs(jvmArgs);
jetty.setSystemProperties(mergedSystemProperties);
jetty.setContainerClassPath(getContainerClassPath());
jetty.setJettyXmlFiles(jettyXmls);
jetty.setJettyProperties(jettyProperties);
jetty.setForkWebXml(forkWebXml);
jetty.setContextXml(contextXml);
jetty.setWebAppPropsFile(new File(target, "webApp.props"));
Random random = new Random();
String token = Long.toString(random.nextLong() ^ System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
jetty.setTokenFile(target.toPath().resolve(token + ".txt").toFile());
jetty.setWebApp(webApp);
return jetty;
}
protected JettyHomeForker newJettyHomeForker()
throws Exception
{
JettyHomeForker jetty = new JettyHomeForker();
jetty.setStopKey(stopKey);
jetty.setStopPort(stopPort);
jetty.setEnv(env);
jetty.setJvmArgs(jvmArgs);
jetty.setJettyOptions(jettyOptions);
jetty.setJettyXmlFiles(jettyXmls);
jetty.setJettyProperties(jettyProperties);
jetty.setModules(modules);
jetty.setSystemProperties(mergedSystemProperties);
Random random = new Random();
String token = Long.toString(random.nextLong() ^ System.currentTimeMillis(), 36).toUpperCase(Locale.ENGLISH);
jetty.setTokenFile(target.toPath().resolve(token + ".txt").toFile());
List libExtJars = new ArrayList<>();
List pdeps = plugin.getPlugin().getDependencies();
if (pdeps != null && !pdeps.isEmpty())
{
boolean warned = false;
for (Dependency d:pdeps)
{
if (d.getGroupId().equalsIgnoreCase("org.eclipse.jetty"))
{
if (!warned)
{
getLog().warn("Jetty jars detected in : use in parameter instead to select appropriate jetty modules.");
warned = true;
}
}
else
{
libExtJars.add(mavenProjectHelper.resolveArtifact(d.getGroupId(), d.getArtifactId(), d.getVersion(), d.getType()));
}
}
jetty.setLibExtJarFiles(libExtJars);
}
jetty.setWebApp(webApp);
jetty.setContextXml(contextXml);
if (jettyHome == null)
jetty.setJettyHomeZip(mavenProjectHelper.resolveArtifact(JETTY_HOME_GROUPID, JETTY_HOME_ARTIFACTID, plugin.getVersion(), "zip"));
jetty.version = plugin.getVersion();
jetty.setJettyHome(jettyHome);
jetty.setJettyBase(jettyBase);
jetty.setBaseDir(target);
return jetty;
}
/**
* Used by subclasses.
* @throws MojoExecutionException
*/
protected void verifyPomConfiguration() throws MojoExecutionException
{
}
/**
* Unite system properties set via systemPropertiesFile element and the systemProperties element.
* Properties from the pom override properties from the file.
*
* @return united properties map
* @throws MojoExecutionException
*/
protected Map mergeSystemProperties()
throws MojoExecutionException
{
Map properties = new HashMap<>();
//Get the properties from any file first
if (systemPropertiesFile != null)
{
Properties tmp = new Properties();
try (InputStream propFile = new FileInputStream(systemPropertiesFile))
{
tmp.load(propFile);
for (Object k:tmp.keySet())
properties.put(k.toString(), tmp.get(k).toString());
}
catch (Exception e)
{
throw new MojoExecutionException("Problem applying system properties from file " + systemPropertiesFile.getName(), e);
}
}
//Allow systemProperties defined in the pom to override the file
if (systemProperties != null)
{
properties.putAll(systemProperties);
}
return properties;
}
protected void configureSystemProperties()
throws MojoExecutionException
{
if (mergedSystemProperties != null)
{
for (Map.Entry e : mergedSystemProperties.entrySet())
{
if (!StringUtil.isEmpty(e.getKey()) && !StringUtil.isEmpty(e.getValue()))
{
System.setProperty(e.getKey(), e.getValue());
if (getLog().isDebugEnabled())
getLog().debug("Set system property " + e.getKey() + "=" + e.getValue());
}
}
}
}
/**
* Augment jetty's classpath with dependencies marked as scope=provided
* if useProvidedScope==true.
*
* @throws MojoExecutionException
*/
protected void augmentPluginClasspath() throws MojoExecutionException
{
//Filter out ones that will clash with jars that are plugin dependencies, then
//create a new classloader that we setup in the parent chain.
providedJars = getProvidedJars();
if (!providedJars.isEmpty())
{
try
{
URL[] urls = new URL[providedJars.size()];
int i = 0;
for (File providedJar:providedJars)
urls[i++] = providedJar.toURI().toURL();
URLClassLoader loader = new URLClassLoader(urls, getClass().getClassLoader());
Thread.currentThread().setContextClassLoader(loader);
getLog().info("Plugin classpath augmented with provided dependencies: " + Arrays.toString(urls));
}
catch (MalformedURLException e)
{
throw new MojoExecutionException("Invalid url", e);
}
}
}
/**
* Get any dependencies that are scope "provided" if useProvidedScope == true. Ensure
* that only those dependencies that are not already present via the plugin are
* included.
*
* @return provided scope dependencies that are not also plugin dependencies.
* @throws MojoExecutionException
*/
protected List getProvidedJars() throws MojoExecutionException
{
if (useProvidedScope)
{
return project.getArtifacts()
.stream()
.filter(a -> Artifact.SCOPE_PROVIDED.equals(a.getScope()) && !isPluginArtifact(a))
.map(a -> a.getFile()).collect(Collectors.toList());
}
else
return Collections.emptyList();
}
/**
* Synthesize a classpath appropriate for a forked jetty based off
* the artifacts associated with the jetty plugin, plus any dependencies
* that are marked as provided and useProvidedScope is true.
*
* @return jetty classpath
* @throws Exception
*/
protected String getContainerClassPath() throws Exception
{
//Add in all the plugin artifacts
StringBuilder classPath = new StringBuilder();
for (Object obj : pluginArtifacts)
{
Artifact artifact = (Artifact)obj;
if ("jar".equals(artifact.getType()))
{
if (classPath.length() > 0)
classPath.append(File.pathSeparator);
classPath.append(artifact.getFile().getAbsolutePath());
}
else
{
if (artifact.getArtifactId().equals(plugin.getArtifactId())) //get the jetty-maven-plugin jar
classPath.append(artifact.getFile().getAbsolutePath());
}
}
//Any jars that we need from the project's dependencies because we're useProvided
if (providedJars != null && !providedJars.isEmpty())
{
for (File jar:providedJars)
{
classPath.append(File.pathSeparator);
classPath.append(jar.getAbsolutePath());
if (getLog().isDebugEnabled()) getLog().debug("Adding provided jar: " + jar);
}
}
return classPath.toString();
}
/**
* Check to see if the given artifact is one of the dependency artifacts for this plugin.
*
* @param artifact to check
* @return true if it is a plugin dependency, false otherwise
*/
protected boolean isPluginArtifact(Artifact artifact)
{
if (pluginArtifacts == null)
return false;
return pluginArtifacts.stream().anyMatch(pa -> pa.getGroupId().equals(artifact.getGroupId()) && pa.getArtifactId().equals(artifact.getArtifactId()));
}
/**
* Check if the goal that we're executing as is excluded or not.
*
* @param goal the goal to check
* @return true if the goal is excluded, false otherwise
*/
protected boolean isExcludedGoal(String goal)
{
if (excludedGoals == null || goal == null)
return false;
goal = goal.trim();
if ("".equals(goal))
return false;
boolean excluded = false;
for (int i = 0; i < excludedGoals.length && !excluded; i++)
{
if (excludedGoals[i].equalsIgnoreCase(goal))
excluded = true;
}
return excluded;
}
protected boolean isPackagingSupported()
{
if (!supportedPackagings.contains(project.getPackaging()))
return false;
return true;
}
protected String getProjectName()
{
String projectName = project.getName();
if (StringUtils.isBlank(projectName))
{
projectName = project.getGroupId() + ":" + project.getArtifactId();
}
return projectName;
}
/**
* Ensure there is a webapp, and that some basic defaults are applied
* if the user has not supplied them.
*
* @throws Exception
*/
protected void configureWebApp()
throws Exception
{
if (webApp == null)
webApp = new MavenWebAppContext();
//If no contextPath was specified, go with default of project artifactid
String cp = webApp.getContextPath();
if (cp == null || "".equals(cp))
{
cp = "/" + project.getArtifactId();
webApp.setContextPath(cp);
}
//If no tmp directory was specified, and we have one, use it
if (webApp.getTempDirectory() == null)
{
File target = new File(project.getBuild().getDirectory());
File tmp = new File(target, "tmp");
if (!tmp.exists())
{
if (!tmp.mkdirs())
{
throw new MojoFailureException("Unable to create temp directory: " + tmp);
}
}
webApp.setTempDirectory(tmp);
}
getLog().info("Context path = " + webApp.getContextPath());
getLog().info("Tmp directory = " + (webApp.getTempDirectory() == null ? " determined at runtime" : webApp.getTempDirectory()));
}
/**
* Try and find a jetty-web.xml file, using some
* historical naming conventions if necessary.
*
* @param webInfDir the web inf directory
* @return the jetty web xml file
*/
protected File findJettyWebXmlFile(File webInfDir)
{
if (webInfDir == null)
return null;
if (!webInfDir.exists())
return null;
File f = new File(webInfDir, "jetty-web.xml");
if (f.exists())
return f;
//try some historical alternatives
f = new File(webInfDir, "web-jetty.xml");
if (f.exists())
return f;
return null;
}
/**
* Get a file into which to write output from jetty.
*
* @param name the name of the file
* @return the created file
* @throws Exception
*/
protected File getJettyOutputFile(String name) throws Exception
{
File outputFile = new File(target, name);
if (outputFile.exists())
outputFile.delete();
outputFile.createNewFile();
return outputFile;
}
/**
* Configure any extra files, directories or patterns thereof for the
* scanner to watch for changes.
*
* @param scanner Scanner that notices changes in files and dirs.
* @throws IOException
*/
protected void configureScanTargetPatterns(Scanner scanner) throws IOException
{
//handle the extra scan patterns
if (scanTargetPatterns != null)
{
for (ScanTargetPattern p : scanTargetPatterns)
{
IncludeExcludeSet includesExcludes = scanner.addDirectory(p.getDirectory().toPath());
p.configureIncludesExcludeSet(includesExcludes);
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy