View Javadoc

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