View Javadoc

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