View Javadoc

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