View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.maven.plugin;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.URL;
24  import java.util.ArrayList;
25  import java.util.Date;
26  import java.util.HashSet;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.eclipse.jetty.util.PathWatcher;
35  import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
36  import org.eclipse.jetty.util.resource.Resource;
37  import org.eclipse.jetty.webapp.WebAppContext;
38  
39  /**
40   *  This goal is used in-situ on a Maven project without first requiring that the project 
41   *  is assembled into a war, saving time during the development cycle.
42   *  <p>
43   *  The plugin forks a parallel lifecycle to ensure that the "compile" phase has been completed before invoking Jetty. This means
44   *  that you do not need to explicity execute a "mvn compile" first. It also means that a "mvn clean jetty:run" will ensure that
45   *  a full fresh compile is done before invoking Jetty.
46   *  <p>
47   *  Once invoked, the plugin can be configured to run continuously, scanning for changes in the project and automatically performing a 
48   *  hot redeploy when necessary. This allows the developer to concentrate on coding changes to the project using their IDE of choice and have those changes
49   *  immediately and transparently reflected in the running web container, eliminating development time that is wasted on rebuilding, reassembling and redeploying.
50   *  <p>
51   *  You may also specify the location of a jetty.xml file whose contents will be applied before any plugin configuration.
52   *  This can be used, for example, to deploy a static webapp that is not part of your maven build. 
53   *  <p>
54   *  There is a <a href="http://www.eclipse.org/jetty/documentation/current/maven-and-jetty.html">reference guide</a> to the configuration parameters for this plugin.
55   * 
56   * @goal run
57   * @requiresDependencyResolution test
58   * @execute phase="test-compile"
59   * @description Runs jetty directly from a maven project
60   */
61  public class JettyRunMojo extends AbstractJettyMojo
62  {
63      public static final String DEFAULT_WEBAPP_SRC = "src"+File.separator+"main"+File.separator+"webapp";
64      public static final String FAKE_WEBAPP = "webapp-tmp";
65      
66      
67  
68      /**
69       * If true, the &lt;testOutputDirectory&gt;
70       * and the dependencies of &lt;scope&gt;test&lt;scope&gt;
71       * will be put first on the runtime classpath.
72       * 
73       * @parameter alias="useTestClasspath" default-value="false"
74       */
75      protected boolean useTestScope;
76      
77    
78      /**
79       * The default location of the web.xml file. Will be used
80       * if &lt;webApp&gt;&lt;descriptor&gt; is not set.
81       * 
82       * @parameter default-value="${maven.war.webxml}"
83       * @readonly
84       */
85      protected String webXml;
86      
87      
88      /**
89       * The directory containing generated classes.
90       *
91       * @parameter default-value="${project.build.outputDirectory}"
92       * @required
93       * 
94       */
95      protected File classesDirectory;
96      
97      /**
98       * An optional pattern for includes/excludes of classes in the classesDirectory
99       * @parameter
100      */
101     protected ScanPattern scanClassesPattern;
102     
103     
104     
105     
106     /**
107      * The directory containing generated test classes.
108      * 
109      * @parameter default-value="${project.build.testOutputDirectory}"
110      * @required
111      */
112     protected File testClassesDirectory;
113     
114     /**
115      * An optional pattern for includes/excludes of classes in the testClassesDirectory
116      * @parameter
117      */
118     protected ScanPattern scanTestClassesPattern;
119     
120    
121     
122     /**
123      * Root directory for all html/jsp etc files
124      *
125      * @parameter default-value="${maven.war.src}"
126      * 
127      */
128     protected File webAppSourceDirectory;
129     
130  
131     /**
132      * List of files or directories to additionally periodically scan for changes. Optional.
133      * @parameter
134      */
135     protected File[] scanTargets;
136     
137     
138     /**
139      * List of directories with ant-style &lt;include&gt; and &lt;exclude&gt; patterns
140      * for extra targets to periodically scan for changes. Can be used instead of,
141      * or in conjunction with &lt;scanTargets&gt;.Optional.
142      * @parameter
143      */
144     protected ScanTargetPattern[] scanTargetPatterns;
145 
146     
147     /**
148      * maven-war-plugin reference
149      */
150     protected WarPluginInfo warPluginInfo;
151     
152     
153     /**
154      * List of deps that are wars
155      */
156     protected List<Artifact> warArtifacts;
157     
158     
159     
160     
161     
162     
163     /** 
164      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#execute()
165      */
166     @Override
167     public void execute() throws MojoExecutionException, MojoFailureException
168     {
169         warPluginInfo = new WarPluginInfo(project);
170         super.execute();
171     }
172     
173     
174     
175     
176     /**
177      * Verify the configuration given in the pom.
178      * 
179      * @see AbstractJettyMojo#checkPomConfiguration()
180      */
181     public void checkPomConfiguration () throws MojoExecutionException
182     {
183         // check the location of the static content/jsps etc
184         try
185         {
186             if ((webAppSourceDirectory == null) || !webAppSourceDirectory.exists())
187             {  
188                 getLog().info("webAppSourceDirectory"+(webAppSourceDirectory == null ? " not set." : (webAppSourceDirectory.getAbsolutePath()+" does not exist."))+" Trying "+DEFAULT_WEBAPP_SRC);
189                 webAppSourceDirectory = new File (project.getBasedir(), DEFAULT_WEBAPP_SRC);             
190                 if (!webAppSourceDirectory.exists())
191                 {
192                     getLog().info("webAppSourceDirectory "+webAppSourceDirectory.getAbsolutePath()+" does not exist. Trying "+project.getBuild().getDirectory()+File.separator+FAKE_WEBAPP);
193                     
194                     //try last resort of making a fake empty dir
195                     File target = new File(project.getBuild().getDirectory());
196                     webAppSourceDirectory = new File(target, FAKE_WEBAPP);
197                     if (!webAppSourceDirectory.exists())
198                         webAppSourceDirectory.mkdirs();              
199                 }
200             }
201             else
202                 getLog().info( "Webapp source directory = " + webAppSourceDirectory.getCanonicalPath());
203         }
204         catch (IOException e)
205         {
206             throw new MojoExecutionException("Webapp source directory does not exist", e);
207         }
208         
209         // check reload mechanic
210         if ( !"automatic".equalsIgnoreCase( reload ) && !"manual".equalsIgnoreCase( reload ) )
211         {
212             throw new MojoExecutionException( "invalid reload mechanic specified, must be 'automatic' or 'manual'" );
213         }
214         else
215         {
216             getLog().info("Reload Mechanic: " + reload );
217         }
218 
219 
220         // check the classes to form a classpath with
221         try
222         {
223             //allow a webapp with no classes in it (just jsps/html)
224             if (classesDirectory != null)
225             {
226                 if (!classesDirectory.exists())
227                     getLog().info( "Classes directory "+ classesDirectory.getCanonicalPath()+ " does not exist");
228                 else
229                     getLog().info("Classes = " + classesDirectory.getCanonicalPath());
230             }
231             else
232                 getLog().info("Classes directory not set");         
233         }
234         catch (IOException e)
235         {
236             throw new MojoExecutionException("Location of classesDirectory does not exist");
237         }
238     }
239 
240    
241 
242 
243     @Override
244     public void finishConfigurationBeforeStart() throws Exception
245     {
246         server.setStopAtShutdown(true); //as we will normally be stopped with a cntrl-c, ensure server stopped 
247         super.finishConfigurationBeforeStart();
248     }
249 
250 
251 
252 
253     /** 
254      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureWebApplication()
255      */
256     public void configureWebApplication() throws Exception
257     {
258        super.configureWebApplication();
259        
260        //Set up the location of the webapp.
261        //There are 2 parts to this: setWar() and setBaseResource(). On standalone jetty,
262        //the former could be the location of a packed war, while the latter is the location
263        //after any unpacking. With this mojo, you are running an unpacked, unassembled webapp,
264        //so the two locations should be equal.
265        Resource webAppSourceDirectoryResource = Resource.newResource(webAppSourceDirectory.getCanonicalPath());
266        if (webApp.getWar() == null)
267            webApp.setWar(webAppSourceDirectoryResource.toString());
268        
269        if (webApp.getBaseResource() == null)
270                webApp.setBaseResource(webAppSourceDirectoryResource);
271 
272        if (classesDirectory != null)
273            webApp.setClasses (classesDirectory);
274        if (useTestScope && (testClassesDirectory != null))
275            webApp.setTestClasses (testClassesDirectory);
276        
277        webApp.setWebInfLib (getDependencyFiles());
278 
279        //get copy of a list of war artifacts
280        Set<Artifact> matchedWarArtifacts = new HashSet<Artifact>();
281 
282 
283 
284        //process any overlays and the war type artifacts
285        List<Overlay> overlays = new ArrayList<Overlay>();
286        for (OverlayConfig config:warPluginInfo.getMavenWarOverlayConfigs())
287        {
288            //overlays can be individually skipped
289            if (config.isSkip())
290                continue;
291 
292            //an empty overlay refers to the current project - important for ordering
293            if (config.isCurrentProject())
294            {
295                Overlay overlay = new Overlay(config, null);
296                overlays.add(overlay);
297                continue;
298            }
299 
300            //if a war matches an overlay config
301            Artifact a = getArtifactForOverlay(config, getWarArtifacts());
302            if (a != null)
303            {
304                matchedWarArtifacts.add(a);
305                SelectiveJarResource r = new SelectiveJarResource(new URL("jar:"+Resource.toURL(a.getFile()).toString()+"!/"));
306                r.setIncludes(config.getIncludes());
307                r.setExcludes(config.getExcludes());
308                Overlay overlay = new Overlay(config, r);
309                overlays.add(overlay);
310            }
311        }
312 
313        //iterate over the left over war artifacts and unpack them (without include/exclude processing) as necessary
314        for (Artifact a: getWarArtifacts())
315        {
316            if (!matchedWarArtifacts.contains(a))
317            {
318                Overlay overlay = new Overlay(null, Resource.newResource(new URL("jar:"+Resource.toURL(a.getFile()).toString()+"!/")));
319                overlays.add(overlay);
320            }
321        }
322 
323        webApp.setOverlays(overlays);
324        
325         //if we have not already set web.xml location, need to set one up
326         if (webApp.getDescriptor() == null)
327         {
328             //Has an explicit web.xml file been configured to use?
329             if (webXml != null)
330             {
331                 Resource r = Resource.newResource(webXml);
332                 if (r.exists() && !r.isDirectory())
333                 {
334                     webApp.setDescriptor(r.toString());
335                 }
336             }
337             
338             //Still don't have a web.xml file: try the resourceBase of the webapp, if it is set
339             if (webApp.getDescriptor() == null && webApp.getBaseResource() != null)
340             {
341                 Resource r = webApp.getBaseResource().addPath("WEB-INF/web.xml");
342                 if (r.exists() && !r.isDirectory())
343                 {
344                     webApp.setDescriptor(r.toString());
345                 }
346             }
347             
348             //Still don't have a web.xml file: finally try the configured static resource directory if there is one
349             if (webApp.getDescriptor() == null && (webAppSourceDirectory != null))
350             {
351                 File f = new File (new File (webAppSourceDirectory, "WEB-INF"), "web.xml");
352                 if (f.exists() && f.isFile())
353                 {
354                    webApp.setDescriptor(f.getCanonicalPath());
355                 }
356             }
357         }
358         getLog().info( "web.xml file = "+webApp.getDescriptor());       
359         getLog().info("Webapp directory = " + webAppSourceDirectory.getCanonicalPath());
360     }
361     
362     
363 
364     
365     /** 
366      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureScanner()
367      */
368     public void configureScanner ()
369     throws MojoExecutionException
370     {
371         try
372         {
373             gatherScannables();
374         }
375         catch (Exception e)
376         {
377             throw new MojoExecutionException("Error forming scan list", e);
378         }
379 
380         scanner.addListener(new PathWatcher.EventListListener()
381         {
382 
383             @Override
384             public void onPathWatchEvents(List<PathWatchEvent> events)
385             {
386                 try
387                 {
388                     boolean reconfigure = false;
389                     if (events != null)
390                     {
391                         for (PathWatchEvent e:events)
392                         {
393                             if (e.getPath().equals(project.getFile().toPath()))
394                             {
395                                 reconfigure = true;
396                                 break;
397                             }
398                         }
399                     }
400 
401                     restartWebApp(reconfigure);
402                 }
403                 catch (Exception e)
404                 {
405                     getLog().error("Error reconfiguring/restarting webapp after change in watched files",e);
406                 }
407             }
408         });
409     }
410 
411     
412     public void gatherScannables() throws Exception
413     {
414         if (webApp.getDescriptor() != null)
415         {
416             Resource r = Resource.newResource(webApp.getDescriptor());
417             scanner.watch(r.getFile().toPath());
418         }
419         
420         if (webApp.getJettyEnvXml() != null)
421             scanner.watch(new File(webApp.getJettyEnvXml()).toPath());
422 
423         if (webApp.getDefaultsDescriptor() != null)
424         {
425             if (!WebAppContext.WEB_DEFAULTS_XML.equals(webApp.getDefaultsDescriptor()))
426                 scanner.watch(new File(webApp.getDefaultsDescriptor()).toPath());
427         }
428 
429         if (webApp.getOverrideDescriptor() != null)
430         {
431             scanner.watch(new File(webApp.getOverrideDescriptor()).toPath());
432         }
433         
434         File jettyWebXmlFile = findJettyWebXmlFile(new File(webAppSourceDirectory,"WEB-INF"));
435         if (jettyWebXmlFile != null)
436         {
437             scanner.watch(jettyWebXmlFile.toPath());
438         }
439         
440         //make sure each of the war artifacts is added to the scanner
441         for (Artifact a:getWarArtifacts())
442         {
443             scanner.watch(a.getFile().toPath());
444         }
445         
446         //handle the explicit extra scan targets
447         if (scanTargets != null)
448         {
449             for (File f:scanTargets)
450             {
451                 if (f.isDirectory())
452                 {
453                     PathWatcher.Config config = new PathWatcher.Config(f.toPath());
454                     config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
455                     scanner.watch(config);
456                 }
457                 else
458                     scanner.watch(f.toPath());
459             }
460         }
461         
462         //handle the extra scan patterns
463         if (scanTargetPatterns != null)
464         {
465             for (ScanTargetPattern p:scanTargetPatterns)
466             {
467                 PathWatcher.Config config = new PathWatcher.Config(p.getDirectory().toPath());
468                 config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
469                 for (String pattern:p.getExcludes())
470                     config.addExcludeGlobRelative(pattern);
471                 for (String pattern:p.getIncludes())
472                     config.addIncludeGlobRelative(pattern);
473                 scanner.watch(config);
474             }
475         }
476       
477 
478         scanner.watch(project.getFile().toPath());
479 
480         if (webApp.getTestClasses() != null)
481         {
482             PathWatcher.Config config = new PathWatcher.Config(webApp.getTestClasses().toPath());
483             config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);           
484             if (scanTestClassesPattern != null)
485             {
486                 for (String p:scanTestClassesPattern.getExcludes())
487                     config.addExcludeGlobRelative(p);
488                 for (String p:scanTestClassesPattern.getIncludes())
489                     config.addIncludeGlobRelative(p);
490             }
491             scanner.watch(config);
492         }
493         
494         if (webApp.getClasses() != null)
495         {
496             PathWatcher.Config config = new PathWatcher.Config(webApp.getClasses().toPath());
497             config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
498             if (scanClassesPattern != null)
499             {
500                 for (String p:scanClassesPattern.getExcludes())
501                     config.addExcludeGlobRelative(p);
502 
503                 for (String p:scanClassesPattern.getIncludes())
504                     config.addIncludeGlobRelative(p);
505 
506             }
507             scanner.watch(config);
508         }
509 
510         if (webApp.getWebInfLib() != null)
511         {
512             for (File f:webApp.getWebInfLib())
513             {
514                 PathWatcher.Config config = new PathWatcher.Config(f.toPath());
515                 config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
516                 scanner.watch(config);
517             }
518         }
519     }
520 
521     
522     /** 
523      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean)
524      */
525     public void restartWebApp(boolean reconfigureScanner) throws Exception 
526     {
527         getLog().info("restarting "+webApp);
528         getLog().debug("Stopping webapp ...");
529         stopScanner();
530         webApp.stop();
531 
532         getLog().debug("Reconfiguring webapp ...");
533  
534         checkPomConfiguration();
535         configureWebApplication();
536 
537         // check if we need to reconfigure the scanner,
538         // which is if the pom changes
539         if (reconfigureScanner)
540         {
541             getLog().info("Reconfiguring scanner after change to pom.xml ...");
542             scanner.reset();
543             warArtifacts = null;
544             configureScanner();
545         }
546 
547         getLog().debug("Restarting webapp ...");
548         webApp.start();
549         startScanner();
550         getLog().info("Restart completed at "+new Date().toString());
551     }
552     
553     
554     
555     
556     /**
557      * @return
558      */
559     private List<File> getDependencyFiles ()
560     {
561         List<File> dependencyFiles = new ArrayList<File>();
562         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
563         {
564             Artifact artifact = (Artifact) iter.next();
565             
566             // Include runtime and compile time libraries, and possibly test libs too
567             if(artifact.getType().equals("war"))
568             {
569                 continue;
570             }
571 
572             if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
573                 continue; //never add dependencies of scope=provided to the webapp's classpath (see also <useProvidedScope> param)
574 
575             if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope)
576                 continue; //only add dependencies of scope=test if explicitly required
577 
578             dependencyFiles.add(artifact.getFile());
579             getLog().debug( "Adding artifact " + artifact.getFile().getName() + " with scope "+artifact.getScope()+" for WEB-INF/lib " );   
580         }
581               
582         return dependencyFiles; 
583     }
584     
585     
586     
587     
588     /**
589      * @return
590      */
591     private List<Artifact> getWarArtifacts ()
592     {
593         if (warArtifacts != null)
594             return warArtifacts;       
595         
596         warArtifacts = new ArrayList<Artifact>();
597         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
598         {
599             Artifact artifact = (Artifact) iter.next(); 
600             if (artifact.getType().equals("war") || artifact.getType().equals("zip"))
601             {
602                 try
603                 {                  
604                     warArtifacts.add(artifact);
605                     getLog().info("Dependent war artifact "+artifact.getId());
606                 }
607                 catch(Exception e)
608                 {
609                     throw new RuntimeException(e);
610                 }
611             }
612         }
613         return warArtifacts;
614     }
615 
616     protected Artifact getArtifactForOverlay (OverlayConfig o, List<Artifact> warArtifacts)
617     {
618         if (o == null || warArtifacts == null || warArtifacts.isEmpty())
619             return null;
620         
621         for (Artifact a:warArtifacts)
622         {
623             if (o.matchesArtifact (a.getGroupId(), a.getArtifactId(), a.getClassifier()))
624             {
625                return a;
626             }
627         }
628         
629         return null;
630     }
631 }