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 (Resource r = Resource.newResource(webApp.getDescriptor());)
409             {
410                 scanList.add(r.getFile());
411             }
412             catch (IOException e)
413             {
414                 throw new MojoExecutionException("Problem configuring scanner for web.xml", e);
415             }
416         }
417 
418         if (webApp.getJettyEnvXml() != null)
419         {
420             try (Resource r = Resource.newResource(webApp.getJettyEnvXml());)
421             {
422                 scanList.add(r.getFile());
423             }
424             catch (IOException e)
425             {
426                 throw new MojoExecutionException("Problem configuring scanner for jetty-env.xml", e);
427             }
428         }
429 
430         if (webApp.getDefaultsDescriptor() != null)
431         {
432             try (Resource r = Resource.newResource(webApp.getDefaultsDescriptor());)
433             {
434                 if (!WebAppContext.WEB_DEFAULTS_XML.equals(webApp.getDefaultsDescriptor()))
435                     scanList.add(r.getFile());
436             }
437             catch (IOException e)
438             {
439                 throw new MojoExecutionException("Problem configuring scanner for webdefaults.xml", e);
440             }
441         }
442         
443         if (webApp.getOverrideDescriptor() != null)
444         {
445             try (Resource r = Resource.newResource(webApp.getOverrideDescriptor());)
446             {
447                 scanList.add(r.getFile());
448             }
449             catch (IOException e)
450             {
451                 throw new MojoExecutionException("Problem configuring scanner for webdefaults.xml", e);
452             }
453         }
454         
455         
456         File jettyWebXmlFile = findJettyWebXmlFile(new File(webAppSourceDirectory,"WEB-INF"));
457         if (jettyWebXmlFile != null)
458             scanList.add(jettyWebXmlFile);
459         scanList.addAll(extraScanTargets);
460         scanList.add(project.getFile());
461         if (webApp.getTestClasses() != null)
462             scanList.add(webApp.getTestClasses());
463         if (webApp.getClasses() != null)
464         scanList.add(webApp.getClasses());
465         scanList.addAll(webApp.getWebInfLib());
466      
467         scannerListeners = new ArrayList<Scanner.BulkListener>();
468         scannerListeners.add(new Scanner.BulkListener()
469         {
470             public void filesChanged (List changes)
471             {
472                 try
473                 {
474                     boolean reconfigure = changes.contains(project.getFile().getCanonicalPath());
475                     restartWebApp(reconfigure);
476                 }
477                 catch (Exception e)
478                 {
479                     getLog().error("Error reconfiguring/restarting webapp after change in watched files",e);
480                 }
481             }
482         });
483     }
484 
485     
486     
487     
488     /** 
489      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean)
490      */
491     public void restartWebApp(boolean reconfigureScanner) throws Exception 
492     {
493         getLog().info("restarting "+webApp);
494         getLog().debug("Stopping webapp ...");
495         webApp.stop();
496         getLog().debug("Reconfiguring webapp ...");
497  
498         checkPomConfiguration();
499         configureWebApplication();
500 
501         // check if we need to reconfigure the scanner,
502         // which is if the pom changes
503         if (reconfigureScanner)
504         {
505             getLog().info("Reconfiguring scanner after change to pom.xml ...");
506             scanList.clear();
507             scanList.add(new File(webApp.getDescriptor()));
508             if (webApp.getJettyEnvXml() != null)
509                 scanList.add(new File(webApp.getJettyEnvXml()));
510             scanList.addAll(extraScanTargets);
511             scanList.add(project.getFile());
512             if (webApp.getTestClasses() != null)
513                 scanList.add(webApp.getTestClasses());
514             if (webApp.getClasses() != null)
515             scanList.add(webApp.getClasses());
516             scanList.addAll(webApp.getWebInfLib());
517             scanner.setScanDirs(scanList);
518         }
519 
520         getLog().debug("Restarting webapp ...");
521         webApp.start();
522         getLog().info("Restart completed at "+new Date().toString());
523     }
524     
525     
526     
527     
528     /**
529      * @return
530      */
531     private List<File> getDependencyFiles ()
532     {
533         List<File> dependencyFiles = new ArrayList<File>();
534         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
535         {
536             Artifact artifact = (Artifact) iter.next();
537             
538             // Include runtime and compile time libraries, and possibly test libs too
539             if(artifact.getType().equals("war"))
540             {
541                 continue;
542             }
543 
544             if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
545                 continue; //never add dependencies of scope=provided to the webapp's classpath (see also <useProvidedScope> param)
546 
547             if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope)
548                 continue; //only add dependencies of scope=test if explicitly required
549 
550             dependencyFiles.add(artifact.getFile());
551             getLog().debug( "Adding artifact " + artifact.getFile().getName() + " with scope "+artifact.getScope()+" for WEB-INF/lib " );   
552         }
553               
554         return dependencyFiles; 
555     }
556     
557     
558     
559     
560     /**
561      * @return
562      */
563     private List<Artifact> getWarArtifacts ()
564     {
565         if (warArtifacts != null)
566             return warArtifacts;       
567         
568         warArtifacts = new ArrayList<Artifact>();
569         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
570         {
571             Artifact artifact = (Artifact) iter.next();            
572             if (artifact.getType().equals("war"))
573             {
574                 try
575                 {                  
576                     warArtifacts.add(artifact);
577                     getLog().info("Dependent war artifact "+artifact.getId());
578                 }
579                 catch(Exception e)
580                 {
581                     throw new RuntimeException(e);
582                 }
583             }
584         }
585         return warArtifacts;
586     }
587 
588     
589     
590     /**
591      * @param o
592      * @param warArtifacts
593      * @return
594      */
595     protected Artifact getArtifactForOverlay (OverlayConfig o, List<Artifact> warArtifacts)
596     {
597         if (o == null || warArtifacts == null || warArtifacts.isEmpty())
598             return null;
599         
600         for (Artifact a:warArtifacts)
601         {
602             if (overlayMatchesArtifact (o, a))
603             {
604                return a;
605             }
606         }
607         
608         return null;
609     }
610 
611 
612 
613     
614     /**
615      * @param o
616      * @param a
617      * @return
618      */
619     protected boolean overlayMatchesArtifact(OverlayConfig o, Artifact a)
620     {
621         if (((o.getGroupId() == null && a.getGroupId() == null) || (o.getGroupId() != null && o.getGroupId().equals(a.getGroupId())))
622         &&  ((o.getArtifactId() == null && a.getArtifactId() == null) || (o.getArtifactId() != null && o.getArtifactId().equals(a.getArtifactId())))
623         &&  ((o.getClassifier() == null) || (o.getClassifier().equals(a.getClassifier()))))
624             return true;
625 
626         return false;
627     }
628 }