View Javadoc

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