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