| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.maven.plugin; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Properties; |
| import java.util.Set; |
| |
| import org.apache.maven.artifact.Artifact; |
| import org.apache.maven.plugin.AbstractMojo; |
| import org.apache.maven.plugin.MojoExecutionException; |
| import org.apache.maven.plugin.MojoFailureException; |
| import org.apache.maven.project.MavenProject; |
| import org.codehaus.plexus.util.FileUtils; |
| import org.eclipse.jetty.security.LoginService; |
| import org.eclipse.jetty.server.RequestLog; |
| import org.eclipse.jetty.server.Server; |
| import org.eclipse.jetty.server.ShutdownMonitor; |
| import org.eclipse.jetty.server.handler.ContextHandler; |
| import org.eclipse.jetty.server.handler.ContextHandlerCollection; |
| import org.eclipse.jetty.server.handler.HandlerCollection; |
| import org.eclipse.jetty.util.PathWatcher; |
| import org.eclipse.jetty.util.StringUtil; |
| import org.eclipse.jetty.util.resource.Resource; |
| import org.eclipse.jetty.xml.XmlConfiguration; |
| |
| /** |
| * Common base class for most jetty mojos. |
| */ |
| public abstract class AbstractJettyMojo extends AbstractMojo |
| { |
| /** |
| * 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 default-value="false" |
| */ |
| protected boolean useProvidedScope; |
| |
| /** |
| * List of goals that are NOT to be used |
| * |
| * @since jetty-7.5.2 |
| * @parameter |
| */ |
| protected String[] excludedGoals; |
| |
| /** |
| * List of other contexts to set up. Consider using instead |
| * the <jettyXml> element to specify external jetty xml config file. |
| * Optional. |
| * |
| * |
| * @parameter |
| */ |
| protected ContextHandler[] contextHandlers; |
| |
| /** |
| * List of security realms to set up. Consider using instead |
| * the <jettyXml> element to specify external jetty xml config file. |
| * Optional. |
| * |
| * |
| * @parameter |
| */ |
| protected LoginService[] 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; |
| |
| /** |
| * 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 alias="webAppConfig" |
| */ |
| protected JettyWebAppContext webApp; |
| |
| /** |
| * The interval in seconds to scan the webapp for changes |
| * and restart the context if necessary. Ignored if reload |
| * is enabled. Disabled by default. |
| * |
| * @parameter property="jetty.scanIntervalSeconds" default-value="0" |
| * @required |
| */ |
| protected int scanIntervalSeconds; |
| |
| /** |
| * reload can be set to either 'automatic' or 'manual' |
| * |
| * if 'manual' then the context can be reloaded by a linefeed in the console |
| * if 'automatic' then traditional reloading on changed files is enabled. |
| * |
| * @parameter property="jetty.reload" default-value="automatic" |
| */ |
| protected String reload; |
| |
| |
| /** |
| * 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 SystemProperties systemProperties; |
| |
| |
| /** |
| * Comma separated list of a jetty xml configuration files whose contents |
| * will be applied before any plugin configuration. Optional. |
| * |
| * |
| * @parameter alias="jettyConfig" |
| */ |
| protected String jettyXml; |
| |
| |
| /** |
| * 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; |
| |
| /** |
| * Use the dump() facility of jetty to print out the server configuration to logging |
| * |
| * @parameter property="dumponStart" default-value="false" |
| */ |
| protected boolean dumpOnStart; |
| |
| |
| /** |
| * Skip this mojo execution. |
| * |
| * @parameter property="jetty.skip" default-value="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 alias="webAppXml" |
| */ |
| protected String contextXml; |
| |
| |
| /** |
| * The maven project. |
| * |
| * @parameter default-value="${project}" |
| * @readonly |
| */ |
| protected MavenProject project; |
| |
| |
| /** |
| * The artifacts for the project. |
| * |
| * @parameter default-value="${project.artifacts}" |
| * @readonly |
| */ |
| protected Set projectArtifacts; |
| |
| |
| /** |
| * @parameter default-value="${mojoExecution}" |
| * @readonly |
| */ |
| protected org.apache.maven.plugin.MojoExecution execution; |
| |
| |
| /** |
| * The artifacts for the plugin itself. |
| * |
| * @parameter default-value="${plugin.artifacts}" |
| * @readonly |
| */ |
| protected List pluginArtifacts; |
| |
| |
| |
| /** |
| * A ServerConnector to use. |
| * |
| * @parameter |
| */ |
| protected MavenServerConnector httpConnector; |
| |
| |
| /** |
| * A wrapper for the Server object |
| * @parameter |
| */ |
| protected Server server; |
| |
| |
| /** |
| * A scanner to check for changes to the webapp |
| */ |
| protected PathWatcher scanner; |
| |
| |
| |
| /** |
| * A scanner to check ENTER hits on the console |
| */ |
| protected Thread consoleScanner; |
| |
| protected ServerSupport serverSupport; |
| |
| |
| |
| |
| /** |
| * <p> |
| * Determines whether or not the server blocks when started. The default |
| * behavior (false) will cause the server to pause other processes |
| * while it continues to handle web requests. This is useful when starting the |
| * server with the intent to work with it interactively. This is the |
| * behaviour of the jetty:run, jetty:run-war, jetty:run-war-exploded goals. |
| * </p><p> |
| * If true, the server will not block the execution of subsequent code. This |
| * is the behaviour of the jetty:start and default behaviour of the jetty:deploy goals. |
| * </p> |
| */ |
| protected boolean nonblocking = false; |
| |
| |
| public abstract void restartWebApp(boolean reconfigureScanner) throws Exception; |
| |
| |
| public abstract void checkPomConfiguration() throws MojoExecutionException; |
| |
| |
| public abstract void configureScanner () throws MojoExecutionException; |
| |
| |
| |
| |
| |
| /** |
| * @see org.apache.maven.plugin.Mojo#execute() |
| */ |
| public void execute() throws MojoExecutionException, MojoFailureException |
| { |
| getLog().info("Configuring Jetty for project: " + this.project.getName()); |
| if (skip) |
| { |
| getLog().info("Skipping Jetty start: jetty.skip==true"); |
| return; |
| } |
| |
| if (isExcluded(execution.getMojoDescriptor().getGoal())) |
| { |
| getLog().info("The goal \""+execution.getMojoDescriptor().getFullGoalName()+ |
| "\" has been made unavailable for this web application by an <excludedGoal> configuration."); |
| return; |
| } |
| |
| configurePluginClasspath(); |
| PluginLog.setLog(getLog()); |
| checkPomConfiguration(); |
| startJetty(); |
| } |
| |
| |
| |
| |
| public void configurePluginClasspath() throws MojoExecutionException |
| { |
| //if we are configured to include the provided dependencies on the plugin's classpath |
| //(which mimics being on jetty's classpath vs being on the webapp's classpath), we first |
| //try and filter out ones that will clash with jars that are plugin dependencies, then |
| //create a new classloader that we setup in the parent chain. |
| if (useProvidedScope) |
| { |
| try |
| { |
| List<URL> provided = new ArrayList<URL>(); |
| URL[] urls = null; |
| |
| for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); ) |
| { |
| Artifact artifact = iter.next(); |
| if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !isPluginArtifact(artifact)) |
| { |
| provided.add(artifact.getFile().toURI().toURL()); |
| if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);} |
| } |
| } |
| |
| if (!provided.isEmpty()) |
| { |
| urls = new URL[provided.size()]; |
| provided.toArray(urls); |
| URLClassLoader loader = new URLClassLoader(urls, getClass().getClassLoader()); |
| Thread.currentThread().setContextClassLoader(loader); |
| getLog().info("Plugin classpath augmented with <scope>provided</scope> dependencies: "+Arrays.toString(urls)); |
| } |
| } |
| catch (MalformedURLException e) |
| { |
| throw new MojoExecutionException("Invalid url", e); |
| } |
| } |
| } |
| |
| public boolean isPluginArtifact(Artifact artifact) |
| { |
| if (pluginArtifacts == null || pluginArtifacts.isEmpty()) |
| return false; |
| |
| boolean isPluginArtifact = false; |
| for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext() && !isPluginArtifact; ) |
| { |
| Artifact pluginArtifact = iter.next(); |
| if (getLog().isDebugEnabled()) { getLog().debug("Checking "+pluginArtifact);} |
| if (pluginArtifact.getGroupId().equals(artifact.getGroupId()) && pluginArtifact.getArtifactId().equals(artifact.getArtifactId())) |
| isPluginArtifact = true; |
| } |
| |
| return isPluginArtifact; |
| } |
| |
| public void finishConfigurationBeforeStart() throws Exception |
| { |
| HandlerCollection contexts = (HandlerCollection)server.getChildHandlerByClass(ContextHandlerCollection.class); |
| if (contexts==null) |
| contexts = (HandlerCollection)server.getChildHandlerByClass(HandlerCollection.class); |
| |
| for (int i=0; (this.contextHandlers != null) && (i < this.contextHandlers.length); i++) |
| { |
| contexts.addHandler(this.contextHandlers[i]); |
| } |
| } |
| |
| public void applyJettyXml() throws Exception |
| { |
| Server tmp = ServerSupport.applyXmlConfigurations(server, getJettyXmlFiles()); |
| if (server == null) |
| server = tmp; |
| |
| if (server == null) |
| server = new Server(); |
| } |
| |
| public void startJetty () throws MojoExecutionException |
| { |
| try |
| { |
| getLog().debug("Starting Jetty Server ..."); |
| |
| //make sure Jetty does not use URLConnection caches with the plugin |
| Resource.setDefaultUseCaches(false); |
| |
| configureMonitor(); |
| |
| printSystemProperties(); |
| |
| //apply any config from a jetty.xml file first which is able to |
| //be overwritten by config in the pom.xml |
| applyJettyXml (); |
| |
| // if a <httpConnector> was specified in the pom, use it |
| if (httpConnector != null) |
| { |
| // check that its port was set |
| if (httpConnector.getPort() <= 0) |
| { |
| //use any jetty.http.port settings provided |
| String tmp = System.getProperty(MavenServerConnector.PORT_SYSPROPERTY, System.getProperty("jetty.port", MavenServerConnector.DEFAULT_PORT_STR)); |
| httpConnector.setPort(Integer.parseInt(tmp.trim())); |
| } |
| httpConnector.setServer(server); |
| } |
| |
| ServerSupport.configureConnectors(server, httpConnector); |
| |
| //set up a RequestLog if one is provided and the handle structure |
| ServerSupport.configureHandlers(server, this.requestLog); |
| |
| //Set up list of default Configurations to apply to a webapp |
| ServerSupport.configureDefaultConfigurationClasses(server); |
| configureWebApplication(); |
| ServerSupport.addWebApplication(server, webApp); |
| |
| // set up security realms |
| ServerSupport.configureLoginServices(server, loginServices); |
| |
| //do any other configuration required by the |
| //particular Jetty version |
| finishConfigurationBeforeStart(); |
| |
| // start Jetty |
| this.server.start(); |
| |
| getLog().info("Started Jetty Server"); |
| |
| if ( dumpOnStart ) |
| { |
| getLog().info(this.server.dump()); |
| } |
| |
| // start the scanner thread (if necessary) on the main webapp |
| if (isScanningEnabled()) |
| { |
| scanner = new PathWatcher(); |
| configureScanner (); |
| startScanner(); |
| } |
| |
| // start the new line scanner thread if necessary |
| startConsoleScanner(); |
| |
| // keep the thread going if not in daemon mode |
| if (!nonblocking ) |
| { |
| server.join(); |
| } |
| } |
| catch (Exception e) |
| { |
| throw new MojoExecutionException("Failure", e); |
| } |
| finally |
| { |
| if (!nonblocking ) |
| { |
| getLog().info("Jetty server exiting."); |
| } |
| } |
| } |
| |
| |
| public void configureMonitor() |
| { |
| if(stopPort>0 && stopKey!=null) |
| { |
| ShutdownMonitor monitor = ShutdownMonitor.getInstance(); |
| monitor.setPort(stopPort); |
| monitor.setKey(stopKey); |
| monitor.setExitVm(!nonblocking); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| /** |
| * Subclasses should invoke this to setup basic info |
| * on the webapp |
| * |
| * @throws Exception if unable to configure web application |
| */ |
| public void configureWebApplication () throws Exception |
| { |
| //As of jetty-7, you must use a <webApp> element |
| if (webApp == null) |
| webApp = new JettyWebAppContext(); |
| |
| //Apply any context xml file to set up the webapp |
| //CAUTION: if you've defined a <webApp> element then the |
| //context xml file can OVERRIDE those settings |
| if (contextXml != null) |
| { |
| File file = FileUtils.getFile(contextXml); |
| XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(file)); |
| getLog().info("Applying context xml file "+contextXml); |
| xmlConfiguration.configure(webApp); |
| } |
| |
| //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()) |
| tmp.mkdirs(); |
| webApp.setTempDirectory(tmp); |
| } |
| |
| getLog().info("Context path = " + webApp.getContextPath()); |
| getLog().info("Tmp directory = "+ (webApp.getTempDirectory()== null? " determined at runtime": webApp.getTempDirectory())); |
| getLog().info("Web defaults = "+(webApp.getDefaultsDescriptor()==null?" jetty default":webApp.getDefaultsDescriptor())); |
| getLog().info("Web overrides = "+(webApp.getOverrideDescriptor()==null?" none":webApp.getOverrideDescriptor())); |
| } |
| |
| |
| |
| |
| /** |
| * Run a scanner thread on the given list of files and directories, calling |
| * stop/start on the given list of LifeCycle objects if any of the watched |
| * files change. |
| * @throws Exception if unable to start scanner |
| */ |
| public void startScanner() throws Exception |
| { |
| if (!isScanningEnabled()) |
| return; |
| |
| scanner.setNotifyExistingOnStart(false); |
| |
| |
| scanner.start(); |
| } |
| |
| |
| public boolean isScanningEnabled () |
| { |
| if (scanIntervalSeconds <=0 || "manual".equalsIgnoreCase( reload )) |
| return false; |
| return true; |
| } |
| |
| public void stopScanner() throws Exception |
| { |
| if (!isScanningEnabled()) |
| return; |
| |
| if (scanner != null) |
| scanner.stop(); |
| } |
| |
| |
| /** |
| * Run a thread that monitors the console input to detect ENTER hits. |
| * @throws Exception if unable to start the console |
| */ |
| protected void startConsoleScanner() throws Exception |
| { |
| if ( "manual".equalsIgnoreCase( reload ) ) |
| { |
| getLog().info("Console reloading is ENABLED. Hit ENTER on the console to restart the context."); |
| consoleScanner = new ConsoleScanner(this); |
| consoleScanner.start(); |
| } |
| } |
| |
| protected void printSystemProperties () |
| { |
| // print out which system properties were set up |
| if (getLog().isDebugEnabled()) |
| { |
| if (systemProperties != null) |
| { |
| Iterator itor = systemProperties.getSystemProperties().iterator(); |
| while (itor.hasNext()) |
| { |
| SystemProperty prop = (SystemProperty)itor.next(); |
| getLog().debug("Property "+prop.getName()+"="+prop.getValue()+" was "+ (prop.isSet() ? "set" : "skipped")); |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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 |
| */ |
| public 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; |
| } |
| |
| public void setSystemPropertiesFile(File file) throws Exception |
| { |
| this.systemPropertiesFile = file; |
| Properties properties = new Properties(); |
| try (InputStream propFile = new FileInputStream(systemPropertiesFile)) |
| { |
| properties.load(propFile); |
| } |
| if (this.systemProperties == null ) |
| this.systemProperties = new SystemProperties(); |
| |
| for (Enumeration<?> keys = properties.keys(); keys.hasMoreElements(); ) |
| { |
| String key = (String)keys.nextElement(); |
| if ( ! systemProperties.containsSystemProperty(key) ) |
| { |
| SystemProperty prop = new SystemProperty(); |
| prop.setKey(key); |
| prop.setValue(properties.getProperty(key)); |
| |
| this.systemProperties.setSystemProperty(prop); |
| } |
| } |
| } |
| |
| public void setSystemProperties(SystemProperties systemProperties) |
| { |
| if (this.systemProperties == null) |
| this.systemProperties = systemProperties; |
| else |
| { |
| for (SystemProperty prop: systemProperties.getSystemProperties()) |
| { |
| this.systemProperties.setSystemProperty(prop); |
| } |
| } |
| } |
| |
| public List<File> getJettyXmlFiles() |
| { |
| if ( this.jettyXml == null ) |
| { |
| return null; |
| } |
| |
| List<File> jettyXmlFiles = new ArrayList<File>(); |
| |
| if ( this.jettyXml.indexOf(',') == -1 ) |
| { |
| jettyXmlFiles.add( new File( this.jettyXml ) ); |
| } |
| else |
| { |
| String[] files = StringUtil.csvSplit(this.jettyXml); |
| |
| for ( String file : files ) |
| { |
| jettyXmlFiles.add( new File(file) ); |
| } |
| } |
| |
| return jettyXmlFiles; |
| } |
| |
| public boolean isExcluded (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; |
| } |
| } |