View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.maven.plugin;
20  
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.InputStream;
24  import java.net.MalformedURLException;
25  import java.net.URL;
26  import java.net.URLClassLoader;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Enumeration;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Properties;
33  import java.util.Set;
34  
35  import org.apache.maven.artifact.Artifact;
36  import org.apache.maven.plugin.AbstractMojo;
37  import org.apache.maven.plugin.MojoExecutionException;
38  import org.apache.maven.plugin.MojoFailureException;
39  import org.apache.maven.project.MavenProject;
40  import org.codehaus.plexus.util.FileUtils;
41  import org.eclipse.jetty.security.LoginService;
42  import org.eclipse.jetty.server.RequestLog;
43  import org.eclipse.jetty.server.Server;
44  import org.eclipse.jetty.server.ShutdownMonitor;
45  import org.eclipse.jetty.server.handler.ContextHandler;
46  import org.eclipse.jetty.server.handler.ContextHandlerCollection;
47  import org.eclipse.jetty.server.handler.HandlerCollection;
48  import org.eclipse.jetty.util.PathWatcher;
49  import org.eclipse.jetty.util.StringUtil;
50  import org.eclipse.jetty.util.resource.Resource;
51  import org.eclipse.jetty.xml.XmlConfiguration;
52  
53  /**
54   * Common base class for most jetty mojos.
55   */
56  public abstract class AbstractJettyMojo extends AbstractMojo
57  {
58      /**
59       * Whether or not to include dependencies on the plugin's classpath with <scope>provided</scope>
60       * Use WITH CAUTION as you may wind up with duplicate jars/classes.
61       * 
62       * @since jetty-7.5.2
63       * @parameter  default-value="false"
64       */
65      protected boolean useProvidedScope;
66      
67      /**
68       * List of goals that are NOT to be used
69       * 
70       * @since jetty-7.5.2
71       * @parameter
72       */
73      protected String[] excludedGoals;
74      
75      /**
76       * List of other contexts to set up. Consider using instead
77       * the <jettyXml> element to specify external jetty xml config file. 
78       * Optional.
79       * 
80       * 
81       * @parameter
82       */
83      protected ContextHandler[] contextHandlers;
84      
85      /**
86       * List of security realms to set up. Consider using instead
87       * the <jettyXml> element to specify external jetty xml config file. 
88       * Optional.
89       * 
90       * 
91       * @parameter
92       */
93      protected LoginService[] loginServices;
94  
95      /**
96       * A RequestLog implementation to use for the webapp at runtime.
97       * Consider using instead the <jettyXml> element to specify external jetty xml config file. 
98       * Optional.
99       * 
100      *
101      * @parameter
102      */
103     protected RequestLog requestLog;
104     
105     /**
106      * An instance of org.eclipse.jetty.webapp.WebAppContext that represents the webapp.
107      * Use any of its setters to configure the webapp. This is the preferred and most
108      * flexible method of configuration, rather than using the (deprecated) individual
109      * parameters like "tmpDirectory", "contextPath" etc.
110      * 
111      * @parameter alias="webAppConfig"
112      */
113     protected JettyWebAppContext webApp;
114 
115     /**
116      * The interval in seconds to scan the webapp for changes 
117      * and restart the context if necessary. Ignored if reload
118      * is enabled. Disabled by default.
119      * 
120      * @parameter property="jetty.scanIntervalSeconds" default-value="0"
121      * @required
122      */
123     protected int scanIntervalSeconds;
124     
125     /**
126      * reload can be set to either 'automatic' or 'manual'
127      *
128      * if 'manual' then the context can be reloaded by a linefeed in the console
129      * if 'automatic' then traditional reloading on changed files is enabled.
130      * 
131      * @parameter property="jetty.reload" default-value="automatic"
132      */
133     protected String reload;
134 
135     
136     /**
137      * File containing system properties to be set before execution
138      * 
139      * Note that these properties will NOT override System properties
140      * that have been set on the command line, by the JVM, or directly 
141      * in the POM via systemProperties. Optional.
142      * 
143      * @parameter property="jetty.systemPropertiesFile"
144      */
145     protected File systemPropertiesFile;
146 
147     
148     /**
149      * System properties to set before execution. 
150      * Note that these properties will NOT override System properties 
151      * that have been set on the command line or by the JVM. They WILL 
152      * override System properties that have been set via systemPropertiesFile.
153      * Optional.
154      * @parameter
155      */
156     protected SystemProperties systemProperties;
157     
158     
159     /**
160      * Comma separated list of a jetty xml configuration files whose contents 
161      * will be applied before any plugin configuration. Optional.
162      * 
163      * 
164      * @parameter alias="jettyConfig"
165      */
166     protected String jettyXml;
167     
168     
169     /**
170      * Port to listen to stop jetty on executing -DSTOP.PORT=<stopPort> 
171      * -DSTOP.KEY=<stopKey> -jar start.jar --stop
172      * 
173      * @parameter
174      */
175     protected int stopPort;
176     
177     
178     /**
179      * Key to provide when stopping jetty on executing java -DSTOP.KEY=<stopKey> 
180      * -DSTOP.PORT=<stopPort> -jar start.jar --stop
181      * 
182      * @parameter
183      */
184     protected String stopKey;
185 
186     /**
187      * Use the dump() facility of jetty to print out the server configuration to logging
188      * 
189      * @parameter property="dumponStart" default-value="false"
190      */
191     protected boolean dumpOnStart;
192     
193     
194     /**  
195      * Skip this mojo execution.
196      * 
197      * @parameter property="jetty.skip" default-value="false"
198      */
199     protected boolean skip;
200 
201     
202     /**
203      * Location of a context xml configuration file whose contents
204      * will be applied to the webapp AFTER anything in <webApp>.Optional.
205      * 
206      * 
207      * @parameter alias="webAppXml"
208      */
209     protected String contextXml;
210 
211 
212     /**
213      * The maven project.
214      *
215      * @parameter default-value="${project}"
216      * @readonly
217      */
218     protected MavenProject project;
219 
220     
221     /**
222      * The artifacts for the project.
223      * 
224      * @parameter default-value="${project.artifacts}"
225      * @readonly
226      */
227     protected Set projectArtifacts;
228     
229     
230     /** 
231      * @parameter default-value="${mojoExecution}" 
232      * @readonly
233      */
234     protected org.apache.maven.plugin.MojoExecution execution;
235     
236 
237     /**
238      * The artifacts for the plugin itself.
239      * 
240      * @parameter default-value="${plugin.artifacts}"
241      * @readonly
242      */
243     protected List pluginArtifacts;
244     
245     
246 
247     /**
248      * A ServerConnector to use.
249      * 
250      * @parameter
251      */
252     protected MavenServerConnector httpConnector;
253     
254     
255     /**
256      * A wrapper for the Server object
257      * @parameter
258      */
259     protected Server server;
260     
261     
262     /**
263      * A scanner to check for changes to the webapp
264      */
265     protected PathWatcher scanner;
266     
267     
268     
269     /**
270      * A scanner to check ENTER hits on the console
271      */
272     protected Thread consoleScanner;
273     
274     protected ServerSupport serverSupport;
275     
276     
277     
278     
279     /**
280      * <p>
281      * Determines whether or not the server blocks when started. The default
282      * behavior (false) will cause the server to pause other processes
283      * while it continues to handle web requests. This is useful when starting the
284      * server with the intent to work with it interactively. This is the 
285      * behaviour of the jetty:run, jetty:run-war, jetty:run-war-exploded goals. 
286      * </p><p>
287      * If true, the server will not block the execution of subsequent code. This
288      * is the behaviour of the jetty:start and default behaviour of the jetty:deploy goals.
289      * </p>
290      */
291     protected boolean nonblocking = false;
292       
293     
294     public abstract void restartWebApp(boolean reconfigureScanner) throws Exception;
295 
296     
297     public abstract void checkPomConfiguration() throws MojoExecutionException;    
298     
299     
300     public abstract void configureScanner () throws MojoExecutionException;
301     
302 
303     
304 
305 
306     /** 
307      * @see org.apache.maven.plugin.Mojo#execute()
308      */
309     public void execute() throws MojoExecutionException, MojoFailureException
310     {
311         getLog().info("Configuring Jetty for project: " + this.project.getName());
312         if (skip)
313         {
314             getLog().info("Skipping Jetty start: jetty.skip==true");
315             return;
316         }
317 
318         if (isExcluded(execution.getMojoDescriptor().getGoal()))
319         {
320             getLog().info("The goal \""+execution.getMojoDescriptor().getFullGoalName()+
321                           "\" has been made unavailable for this web application by an <excludedGoal> configuration.");
322             return;
323         }
324         
325         configurePluginClasspath();
326         PluginLog.setLog(getLog());
327         checkPomConfiguration();
328         startJetty();
329     }
330     
331     
332     
333     
334     public void configurePluginClasspath() throws MojoExecutionException
335     {  
336         //if we are configured to include the provided dependencies on the plugin's classpath
337         //(which mimics being on jetty's classpath vs being on the webapp's classpath), we first
338         //try and filter out ones that will clash with jars that are plugin dependencies, then
339         //create a new classloader that we setup in the parent chain.
340         if (useProvidedScope)
341         {
342             try
343             {
344                 List<URL> provided = new ArrayList<URL>();
345                 URL[] urls = null;
346                
347                 for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
348                 {                   
349                     Artifact artifact = iter.next();
350                     if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()) && !isPluginArtifact(artifact))
351                     {
352                         provided.add(artifact.getFile().toURI().toURL());
353                         if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
354                     }
355                 }
356 
357                 if (!provided.isEmpty())
358                 {
359                     urls = new URL[provided.size()];
360                     provided.toArray(urls);
361                     URLClassLoader loader  = new URLClassLoader(urls, getClass().getClassLoader());
362                     Thread.currentThread().setContextClassLoader(loader);
363                     getLog().info("Plugin classpath augmented with <scope>provided</scope> dependencies: "+Arrays.toString(urls));
364                 }
365             }
366             catch (MalformedURLException e)
367             {
368                 throw new MojoExecutionException("Invalid url", e);
369             }
370         }
371     }
372     
373     public boolean isPluginArtifact(Artifact artifact)
374     {
375         if (pluginArtifacts == null || pluginArtifacts.isEmpty())
376             return false;
377         
378         boolean isPluginArtifact = false;
379         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext() && !isPluginArtifact; )
380         {
381             Artifact pluginArtifact = iter.next();
382             if (getLog().isDebugEnabled()) { getLog().debug("Checking "+pluginArtifact);}
383             if (pluginArtifact.getGroupId().equals(artifact.getGroupId()) && pluginArtifact.getArtifactId().equals(artifact.getArtifactId()))
384                 isPluginArtifact = true;
385         }
386         
387         return isPluginArtifact;
388     }
389 
390     public void finishConfigurationBeforeStart() throws Exception
391     {
392         HandlerCollection contexts = (HandlerCollection)server.getChildHandlerByClass(ContextHandlerCollection.class);
393         if (contexts==null)
394             contexts = (HandlerCollection)server.getChildHandlerByClass(HandlerCollection.class);
395         
396         for (int i=0; (this.contextHandlers != null) && (i < this.contextHandlers.length); i++)
397         {
398             contexts.addHandler(this.contextHandlers[i]);
399         }
400     }
401 
402     public void applyJettyXml() throws Exception
403     {        
404         Server tmp = ServerSupport.applyXmlConfigurations(server, getJettyXmlFiles());
405         if (server == null)
406             server = tmp;
407         
408         if (server == null)
409             server = new Server();
410     }
411 
412     public void startJetty () throws MojoExecutionException
413     {
414         try
415         {
416             getLog().debug("Starting Jetty Server ...");
417             
418             //make sure Jetty does not use URLConnection caches with the plugin
419             Resource.setDefaultUseCaches(false);
420          
421             configureMonitor();
422             
423             printSystemProperties();
424             
425             //apply any config from a jetty.xml file first which is able to
426             //be overwritten by config in the pom.xml
427             applyJettyXml ();      
428             
429             // if a <httpConnector> was specified in the pom, use it
430             if (httpConnector != null)
431             {
432                 // check that its port was set
433                 if (httpConnector.getPort() <= 0)
434                 {
435                     //use any jetty.http.port settings provided
436                     String tmp = System.getProperty(MavenServerConnector.PORT_SYSPROPERTY, System.getProperty("jetty.port", MavenServerConnector.DEFAULT_PORT_STR));
437                     httpConnector.setPort(Integer.parseInt(tmp.trim()));
438                 }  
439                 httpConnector.setServer(server);
440             }
441 
442             ServerSupport.configureConnectors(server, httpConnector);
443 
444             //set up a RequestLog if one is provided and the handle structure
445             ServerSupport.configureHandlers(server, this.requestLog);
446             
447             //Set up list of default Configurations to apply to a webapp
448             ServerSupport.configureDefaultConfigurationClasses(server);
449             configureWebApplication();
450             ServerSupport.addWebApplication(server, webApp);
451 
452             // set up security realms
453             ServerSupport.configureLoginServices(server, loginServices);
454 
455             //do any other configuration required by the
456             //particular Jetty version
457             finishConfigurationBeforeStart();
458 
459             // start Jetty
460             this.server.start();
461 
462             getLog().info("Started Jetty Server");
463 
464             if ( dumpOnStart )
465             {
466                 getLog().info(this.server.dump());
467             }
468 
469             // start the scanner thread (if necessary) on the main webapp
470             if (isScanningEnabled())
471             {
472                 scanner = new PathWatcher();
473                 configureScanner ();
474                 startScanner();
475             }
476 
477             // start the new line scanner thread if necessary
478             startConsoleScanner();
479 
480             // keep the thread going if not in daemon mode
481             if (!nonblocking )
482             {
483                 server.join();
484             }
485         }
486         catch (Exception e)
487         {
488             throw new MojoExecutionException("Failure", e);
489         }
490         finally
491         {
492             if (!nonblocking )
493             {
494                 getLog().info("Jetty server exiting.");
495             }            
496         }        
497     }
498     
499     
500     public void configureMonitor()
501     { 
502         if(stopPort>0 && stopKey!=null)
503         {
504             ShutdownMonitor monitor = ShutdownMonitor.getInstance();
505             monitor.setPort(stopPort);
506             monitor.setKey(stopKey);
507             monitor.setExitVm(!nonblocking);
508         }
509     }
510 
511     
512     
513     
514     
515     
516     /**
517      * Subclasses should invoke this to setup basic info
518      * on the webapp
519      * 
520      * @throws Exception if unable to configure web application 
521      */
522     public void configureWebApplication () throws Exception
523     {
524         //As of jetty-7, you must use a <webApp> element
525         if (webApp == null)
526             webApp = new JettyWebAppContext();
527         
528         //Apply any context xml file to set up the webapp
529         //CAUTION: if you've defined a <webApp> element then the
530         //context xml file can OVERRIDE those settings
531         if (contextXml != null)
532         {
533             File file = FileUtils.getFile(contextXml);
534             XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(file));
535             getLog().info("Applying context xml file "+contextXml);
536             xmlConfiguration.configure(webApp);   
537         }
538         
539         //If no contextPath was specified, go with default of project artifactid
540         String cp = webApp.getContextPath();
541         if (cp == null || "".equals(cp))
542         {
543             cp = "/"+project.getArtifactId();
544             webApp.setContextPath(cp);
545         }        
546 
547         //If no tmp directory was specified, and we have one, use it
548         if (webApp.getTempDirectory() == null)
549         {
550             File target = new File(project.getBuild().getDirectory());
551             File tmp = new File(target,"tmp");
552             if (!tmp.exists())
553                 tmp.mkdirs();            
554             webApp.setTempDirectory(tmp);
555         }
556       
557         getLog().info("Context path = " + webApp.getContextPath());
558         getLog().info("Tmp directory = "+ (webApp.getTempDirectory()== null? " determined at runtime": webApp.getTempDirectory()));
559         getLog().info("Web defaults = "+(webApp.getDefaultsDescriptor()==null?" jetty default":webApp.getDefaultsDescriptor()));
560         getLog().info("Web overrides = "+(webApp.getOverrideDescriptor()==null?" none":webApp.getOverrideDescriptor()));
561     }
562 
563 
564 
565     
566     /**
567      * Run a scanner thread on the given list of files and directories, calling
568      * stop/start on the given list of LifeCycle objects if any of the watched
569      * files change.
570      * @throws Exception if unable to start scanner 
571      */
572     public void startScanner() throws Exception
573     {
574         if (!isScanningEnabled())
575             return;
576 
577         scanner.setNotifyExistingOnStart(false);
578        
579        
580         scanner.start();
581     }
582     
583     
584     public boolean isScanningEnabled ()
585     {
586         if (scanIntervalSeconds <=0 || "manual".equalsIgnoreCase( reload ))
587             return false;
588         return true;
589     }
590     
591     public void stopScanner() throws Exception
592     {
593         if (!isScanningEnabled())
594             return;
595         
596         if (scanner != null)
597             scanner.stop();
598     }
599     
600     
601     /**
602      * Run a thread that monitors the console input to detect ENTER hits.
603      * @throws Exception if unable to start the console
604      */
605     protected void startConsoleScanner() throws Exception
606     {
607         if ( "manual".equalsIgnoreCase( reload ) )
608         {
609             getLog().info("Console reloading is ENABLED. Hit ENTER on the console to restart the context.");
610             consoleScanner = new ConsoleScanner(this);
611             consoleScanner.start();
612         }       
613     }
614 
615     protected void printSystemProperties ()
616     {
617         // print out which system properties were set up
618         if (getLog().isDebugEnabled())
619         {
620             if (systemProperties != null)
621             {
622                 Iterator itor = systemProperties.getSystemProperties().iterator();
623                 while (itor.hasNext())
624                 {
625                     SystemProperty prop = (SystemProperty)itor.next();
626                     getLog().debug("Property "+prop.getName()+"="+prop.getValue()+" was "+ (prop.isSet() ? "set" : "skipped"));
627                 }
628             }
629         }
630     }
631 
632     /**
633      * Try and find a jetty-web.xml file, using some
634      * historical naming conventions if necessary.
635      * @param webInfDir the web inf directory
636      * @return the jetty web xml file
637      */
638     public File findJettyWebXmlFile (File webInfDir)
639     {
640         if (webInfDir == null)
641             return null;
642         if (!webInfDir.exists())
643             return null;
644 
645         File f = new File (webInfDir, "jetty-web.xml");
646         if (f.exists())
647             return f;
648 
649         //try some historical alternatives
650         f = new File (webInfDir, "web-jetty.xml");
651         if (f.exists())
652             return f;
653         
654         return null;
655     }
656 
657     public void setSystemPropertiesFile(File file) throws Exception
658     {
659         this.systemPropertiesFile = file;
660         Properties properties = new Properties();
661         try (InputStream propFile = new FileInputStream(systemPropertiesFile))
662         {
663             properties.load(propFile);
664         }
665         if (this.systemProperties == null )
666             this.systemProperties = new SystemProperties();
667         
668         for (Enumeration<?> keys = properties.keys(); keys.hasMoreElements();  )
669         {
670             String key = (String)keys.nextElement();
671             if ( ! systemProperties.containsSystemProperty(key) )
672             {
673                 SystemProperty prop = new SystemProperty();
674                 prop.setKey(key);
675                 prop.setValue(properties.getProperty(key));
676                 
677                 this.systemProperties.setSystemProperty(prop);
678             }
679         } 
680     }
681     
682     public void setSystemProperties(SystemProperties systemProperties)
683     {
684         if (this.systemProperties == null)
685             this.systemProperties = systemProperties;
686         else
687         {
688             for (SystemProperty prop: systemProperties.getSystemProperties())
689             {
690                 this.systemProperties.setSystemProperty(prop);
691             }   
692         }
693     }
694     
695     public List<File> getJettyXmlFiles()
696     {
697         if ( this.jettyXml == null )
698         {
699             return null;
700         }
701         
702         List<File> jettyXmlFiles = new ArrayList<File>();
703         
704         if ( this.jettyXml.indexOf(',') == -1 )
705         {
706             jettyXmlFiles.add( new File( this.jettyXml ) );
707         }
708         else
709         {
710             String[] files = StringUtil.csvSplit(this.jettyXml);
711             
712             for ( String file : files )
713             {
714                 jettyXmlFiles.add( new File(file) );
715             }
716         }
717         
718         return jettyXmlFiles;
719     }
720 
721     public boolean isExcluded (String goal)
722     {
723         if (excludedGoals == null || goal == null)
724             return false;
725         
726         goal = goal.trim();
727         if ("".equals(goal))
728             return false;
729         
730         boolean excluded = false;
731         for (int i=0; i<excludedGoals.length && !excluded; i++)
732         {
733             if (excludedGoals[i].equalsIgnoreCase(goal))
734                 excluded = true;
735         }
736         
737         return excluded;
738     }
739 }