View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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="http://www.eclipse.org/jetty/documentation/current/maven-and-jetty.html">reference guide</a> to the configuration parameters for this plugin.
59   *  </p>
60   * 
61   * 
62   * @goal run
63   * @requiresDependencyResolution test
64   * @execute phase="test-compile"
65   * @description Runs jetty directly from a maven project
66   */
67  public class JettyRunMojo extends AbstractJettyMojo
68  {
69      public static final String DEFAULT_WEBAPP_SRC = "src"+File.separator+"main"+File.separator+"webapp";
70      public static final String FAKE_WEBAPP = "webapp-tmp";
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 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                 getLog().info("webAppSourceDirectory"+(webAppSourceDirectory == null ? " not set." : (webAppSourceDirectory.getAbsolutePath()+" does not exist."))+" Trying "+DEFAULT_WEBAPP_SRC);
186                 webAppSourceDirectory = new File (project.getBasedir(), DEFAULT_WEBAPP_SRC);             
187                 if (!webAppSourceDirectory.exists())
188                 {
189                     getLog().info("webAppSourceDirectory "+webAppSourceDirectory.getAbsolutePath()+" does not exist. Trying "+project.getBuild().getDirectory()+File.separator+FAKE_WEBAPP);
190                     
191                     //try last resort of making a fake empty dir
192                     File target = new File(project.getBuild().getDirectory());
193                     webAppSourceDirectory = new File(target, FAKE_WEBAPP);
194                     if (!webAppSourceDirectory.exists())
195                         webAppSourceDirectory.mkdirs();              
196                 }
197             }
198             else
199                 getLog().info( "Webapp source directory = " + webAppSourceDirectory.getCanonicalPath());
200         }
201         catch (IOException e)
202         {
203             throw new MojoExecutionException("Webapp source directory does not exist", e);
204         }
205         
206         // check reload mechanic
207         if ( !"automatic".equalsIgnoreCase( reload ) && !"manual".equalsIgnoreCase( reload ) )
208         {
209             throw new MojoExecutionException( "invalid reload mechanic specified, must be 'automatic' or 'manual'" );
210         }
211         else
212         {
213             getLog().info("Reload Mechanic: " + reload );
214         }
215 
216 
217         // check the classes to form a classpath with
218         try
219         {
220             //allow a webapp with no classes in it (just jsps/html)
221             if (classesDirectory != null)
222             {
223                 if (!classesDirectory.exists())
224                     getLog().info( "Classes directory "+ classesDirectory.getCanonicalPath()+ " does not exist");
225                 else
226                     getLog().info("Classes = " + classesDirectory.getCanonicalPath());
227             }
228             else
229                 getLog().info("Classes directory not set");         
230         }
231         catch (IOException e)
232         {
233             throw new MojoExecutionException("Location of classesDirectory does not exist");
234         }
235         
236         extraScanTargets = new ArrayList<File>();
237         if (scanTargets != null)
238         {            
239             for (int i=0; i< scanTargets.length; i++)
240             {
241                 getLog().info("Added extra scan target:"+ scanTargets[i]);
242                 extraScanTargets.add(scanTargets[i]);
243             }            
244         }
245         
246         if (scanTargetPatterns!=null)
247         {
248             for (int i=0;i<scanTargetPatterns.length; i++)
249             {
250                 Iterator itor = scanTargetPatterns[i].getIncludes().iterator();
251                 StringBuffer strbuff = new StringBuffer();
252                 while (itor.hasNext())
253                 {
254                     strbuff.append((String)itor.next());
255                     if (itor.hasNext())
256                         strbuff.append(",");
257                 }
258                 String includes = strbuff.toString();
259                 
260                 itor = scanTargetPatterns[i].getExcludes().iterator();
261                 strbuff= new StringBuffer();
262                 while (itor.hasNext())
263                 {
264                     strbuff.append((String)itor.next());
265                     if (itor.hasNext())
266                         strbuff.append(",");
267                 }
268                 String excludes = strbuff.toString();
269 
270                 try
271                 {
272                     List<File> files = FileUtils.getFiles(scanTargetPatterns[i].getDirectory(), includes, excludes);
273                     itor = files.iterator();
274                     while (itor.hasNext())
275                         getLog().info("Adding extra scan target from pattern: "+itor.next());
276                     List<File> currentTargets = extraScanTargets;
277                     if(currentTargets!=null && !currentTargets.isEmpty())
278                         currentTargets.addAll(files);
279                     else
280                         extraScanTargets = files;
281                 }
282                 catch (IOException e)
283                 {
284                     throw new MojoExecutionException(e.getMessage());
285                 }
286             }
287         }
288     }
289 
290    
291 
292 
293     @Override
294     public void finishConfigurationBeforeStart() throws Exception
295     {
296         server.setStopAtShutdown(true); //as we will normally be stopped with a cntrl-c, ensure server stopped 
297         super.finishConfigurationBeforeStart();
298     }
299 
300 
301 
302 
303     /** 
304      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureWebApplication()
305      */
306     public void configureWebApplication() throws Exception
307     {
308        super.configureWebApplication();
309        
310        //Set up the location of the webapp.
311        //There are 2 parts to this: setWar() and setBaseResource(). On standalone jetty,
312        //the former could be the location of a packed war, while the latter is the location
313        //after any unpacking. With this mojo, you are running an unpacked, unassembled webapp,
314        //so the two locations should be equal.
315        Resource webAppSourceDirectoryResource = Resource.newResource(webAppSourceDirectory.getCanonicalPath());
316        if (webApp.getWar() == null)
317            webApp.setWar(webAppSourceDirectoryResource.toString());
318        
319        if (webApp.getBaseResource() == null)
320                webApp.setBaseResource(webAppSourceDirectoryResource);
321 
322        if (classesDirectory != null)
323            webApp.setClasses (classesDirectory);
324        if (useTestScope && (testClassesDirectory != null))
325            webApp.setTestClasses (testClassesDirectory);
326        
327        webApp.setWebInfLib (getDependencyFiles());
328 
329        //get copy of a list of war artifacts
330        Set<Artifact> matchedWarArtifacts = new HashSet<Artifact>();
331 
332        //make sure each of the war artifacts is added to the scanner
333        for (Artifact a:getWarArtifacts())
334            extraScanTargets.add(a.getFile());
335 
336        //process any overlays and the war type artifacts
337        List<Overlay> overlays = new ArrayList<Overlay>();
338        for (OverlayConfig config:warPluginInfo.getMavenWarOverlayConfigs())
339        {
340            //overlays can be individually skipped
341            if (config.isSkip())
342                continue;
343 
344            //an empty overlay refers to the current project - important for ordering
345            if (config.isCurrentProject())
346            {
347                Overlay overlay = new Overlay(config, null);
348                overlays.add(overlay);
349                continue;
350            }
351 
352            //if a war matches an overlay config
353            Artifact a = getArtifactForOverlay(config, getWarArtifacts());
354            if (a != null)
355            {
356                matchedWarArtifacts.add(a);
357                SelectiveJarResource r = new SelectiveJarResource(new URL("jar:"+Resource.toURL(a.getFile()).toString()+"!/"));
358                r.setIncludes(config.getIncludes());
359                r.setExcludes(config.getExcludes());
360                Overlay overlay = new Overlay(config, r);
361                overlays.add(overlay);
362            }
363        }
364 
365        //iterate over the left over war artifacts and unpack them (without include/exclude processing) as necessary
366        for (Artifact a: getWarArtifacts())
367        {
368            if (!matchedWarArtifacts.contains(a))
369            {
370                Overlay overlay = new Overlay(null, Resource.newResource(new URL("jar:"+Resource.toURL(a.getFile()).toString()+"!/")));
371                overlays.add(overlay);
372            }
373        }
374 
375        webApp.setOverlays(overlays);
376        
377         //if we have not already set web.xml location, need to set one up
378         if (webApp.getDescriptor() == null)
379         {
380             //Has an explicit web.xml file been configured to use?
381             if (webXml != null)
382             {
383                 Resource r = Resource.newResource(webXml);
384                 if (r.exists() && !r.isDirectory())
385                 {
386                     webApp.setDescriptor(r.toString());
387                 }
388             }
389             
390             //Still don't have a web.xml file: try the resourceBase of the webapp, if it is set
391             if (webApp.getDescriptor() == null && webApp.getBaseResource() != null)
392             {
393                 Resource r = webApp.getBaseResource().addPath("WEB-INF/web.xml");
394                 if (r.exists() && !r.isDirectory())
395                 {
396                     webApp.setDescriptor(r.toString());
397                 }
398             }
399             
400             //Still don't have a web.xml file: finally try the configured static resource directory if there is one
401             if (webApp.getDescriptor() == null && (webAppSourceDirectory != null))
402             {
403                 File f = new File (new File (webAppSourceDirectory, "WEB-INF"), "web.xml");
404                 if (f.exists() && f.isFile())
405                 {
406                    webApp.setDescriptor(f.getCanonicalPath());
407                 }
408             }
409         }
410         getLog().info( "web.xml file = "+webApp.getDescriptor());       
411         getLog().info("Webapp directory = " + webAppSourceDirectory.getCanonicalPath());
412     }
413     
414     
415 
416     
417     /** 
418      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureScanner()
419      */
420     public void configureScanner ()
421     throws MojoExecutionException
422     {
423         // start the scanner thread (if necessary) on the main webapp
424         scanList = new ArrayList<File>();
425         if (webApp.getDescriptor() != null)
426         {
427             try (Resource r = Resource.newResource(webApp.getDescriptor());)
428             {
429                 scanList.add(r.getFile());
430             }
431             catch (IOException e)
432             {
433                 throw new MojoExecutionException("Problem configuring scanner for web.xml", e);
434             }
435         }
436 
437         if (webApp.getJettyEnvXml() != null)
438         {
439             try (Resource r = Resource.newResource(webApp.getJettyEnvXml());)
440             {
441                 scanList.add(r.getFile());
442             }
443             catch (IOException e)
444             {
445                 throw new MojoExecutionException("Problem configuring scanner for jetty-env.xml", e);
446             }
447         }
448 
449         if (webApp.getDefaultsDescriptor() != null)
450         {
451             try (Resource r = Resource.newResource(webApp.getDefaultsDescriptor());)
452             {
453                 if (!WebAppContext.WEB_DEFAULTS_XML.equals(webApp.getDefaultsDescriptor()))
454                     scanList.add(r.getFile());
455             }
456             catch (IOException e)
457             {
458                 throw new MojoExecutionException("Problem configuring scanner for webdefaults.xml", e);
459             }
460         }
461         
462         if (webApp.getOverrideDescriptor() != null)
463         {
464             try (Resource r = Resource.newResource(webApp.getOverrideDescriptor());)
465             {
466                 scanList.add(r.getFile());
467             }
468             catch (IOException e)
469             {
470                 throw new MojoExecutionException("Problem configuring scanner for webdefaults.xml", e);
471             }
472         }
473         
474         
475         File jettyWebXmlFile = findJettyWebXmlFile(new File(webAppSourceDirectory,"WEB-INF"));
476         if (jettyWebXmlFile != null)
477             scanList.add(jettyWebXmlFile);
478         scanList.addAll(extraScanTargets);
479         scanList.add(project.getFile());
480         if (webApp.getTestClasses() != null)
481             scanList.add(webApp.getTestClasses());
482         if (webApp.getClasses() != null)
483         scanList.add(webApp.getClasses());
484         scanList.addAll(webApp.getWebInfLib());
485      
486         scannerListeners = new ArrayList<Scanner.BulkListener>();
487         scannerListeners.add(new Scanner.BulkListener()
488         {
489             public void filesChanged (List changes)
490             {
491                 try
492                 {
493                     boolean reconfigure = changes.contains(project.getFile().getCanonicalPath());
494                     restartWebApp(reconfigure);
495                 }
496                 catch (Exception e)
497                 {
498                     getLog().error("Error reconfiguring/restarting webapp after change in watched files",e);
499                 }
500             }
501         });
502     }
503 
504     
505     
506     
507     /** 
508      * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean)
509      */
510     public void restartWebApp(boolean reconfigureScanner) throws Exception 
511     {
512         getLog().info("restarting "+webApp);
513         getLog().debug("Stopping webapp ...");
514         webApp.stop();
515         getLog().debug("Reconfiguring webapp ...");
516  
517         checkPomConfiguration();
518         configureWebApplication();
519 
520         // check if we need to reconfigure the scanner,
521         // which is if the pom changes
522         if (reconfigureScanner)
523         {
524             getLog().info("Reconfiguring scanner after change to pom.xml ...");
525             scanList.clear();
526             if (webApp.getDescriptor() != null)
527                 scanList.add(new File(webApp.getDescriptor()));
528             if (webApp.getJettyEnvXml() != null)
529                 scanList.add(new File(webApp.getJettyEnvXml()));
530             scanList.addAll(extraScanTargets);
531             scanList.add(project.getFile());
532             if (webApp.getTestClasses() != null)
533                 scanList.add(webApp.getTestClasses());
534             if (webApp.getClasses() != null)
535             scanList.add(webApp.getClasses());
536             scanList.addAll(webApp.getWebInfLib());
537             scanner.setScanDirs(scanList);
538         }
539 
540         getLog().debug("Restarting webapp ...");
541         webApp.start();
542         getLog().info("Restart completed at "+new Date().toString());
543     }
544     
545     
546     
547     
548     /**
549      * @return
550      */
551     private List<File> getDependencyFiles ()
552     {
553         List<File> dependencyFiles = new ArrayList<File>();
554         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
555         {
556             Artifact artifact = (Artifact) iter.next();
557             
558             // Include runtime and compile time libraries, and possibly test libs too
559             if(artifact.getType().equals("war"))
560             {
561                 continue;
562             }
563 
564             if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
565                 continue; //never add dependencies of scope=provided to the webapp's classpath (see also <useProvidedScope> param)
566 
567             if (Artifact.SCOPE_TEST.equals(artifact.getScope()) && !useTestScope)
568                 continue; //only add dependencies of scope=test if explicitly required
569 
570             dependencyFiles.add(artifact.getFile());
571             getLog().debug( "Adding artifact " + artifact.getFile().getName() + " with scope "+artifact.getScope()+" for WEB-INF/lib " );   
572         }
573               
574         return dependencyFiles; 
575     }
576     
577     
578     
579     
580     /**
581      * @return
582      */
583     private List<Artifact> getWarArtifacts ()
584     {
585         if (warArtifacts != null)
586             return warArtifacts;       
587         
588         warArtifacts = new ArrayList<Artifact>();
589         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
590         {
591             Artifact artifact = (Artifact) iter.next();            
592             if (artifact.getType().equals("war"))
593             {
594                 try
595                 {                  
596                     warArtifacts.add(artifact);
597                     getLog().info("Dependent war artifact "+artifact.getId());
598                 }
599                 catch(Exception e)
600                 {
601                     throw new RuntimeException(e);
602                 }
603             }
604         }
605         return warArtifacts;
606     }
607 
608     
609     
610     /**
611      * @param o
612      * @param warArtifacts
613      * @return
614      */
615     protected Artifact getArtifactForOverlay (OverlayConfig o, List<Artifact> warArtifacts)
616     {
617         if (o == null || warArtifacts == null || warArtifacts.isEmpty())
618             return null;
619         
620         for (Artifact a:warArtifacts)
621         {
622             if (o.matchesArtifact (a.getGroupId(), a.getArtifactId(), a.getClassifier()))
623             {
624                return a;
625             }
626         }
627         
628         return null;
629     }
630 }