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