View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.jspc.plugin;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.FileFilter;
24  import java.io.FileReader;
25  import java.io.FileWriter;
26  import java.io.IOException;
27  import java.io.PrintWriter;
28  import java.net.MalformedURLException;
29  import java.net.URL;
30  import java.net.URLClassLoader;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Set;
37  
38  import org.apache.jasper.JspC;
39  import org.apache.jasper.servlet.JspCServletContext;
40  import org.apache.jasper.servlet.TldScanner;
41  import org.apache.maven.artifact.Artifact;
42  import org.apache.maven.plugin.AbstractMojo;
43  import org.apache.maven.plugin.MojoExecutionException;
44  import org.apache.maven.plugin.MojoFailureException;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.tomcat.JarScanner;
47  import org.apache.tomcat.util.scan.StandardJarScanner;
48  import org.codehaus.plexus.util.FileUtils;
49  import org.codehaus.plexus.util.StringUtils;
50  import org.eclipse.jetty.util.IO;
51  import org.eclipse.jetty.util.resource.Resource;
52  
53  /**
54   * This goal will compile jsps for a webapp so that they can be included in a
55   * war.
56   * <p>
57   * At runtime, the plugin will use the jspc compiler to precompile jsps and tags.
58   * </p>
59   * <p>
60   * Note that the same java compiler will be used as for on-the-fly compiled
61   * jsps, which will be the Eclipse java compiler.
62   * <p>
63   * See <a
64   * href="https://www.eclipse.org/jetty/documentation/current/jetty-jspc-maven-plugin.html">Usage
65   * Guide</a> for instructions on using this plugin.
66   * </p>
67   * @goal jspc
68   * @phase process-classes
69   * @requiresDependencyResolution compile+runtime
70   * @description Runs jspc compiler to produce .java and .class files
71   */
72  public class JspcMojo extends AbstractMojo
73  {
74      public static final String END_OF_WEBAPP = "</web-app>";
75      public static final String PRECOMPILED_FLAG = "org.eclipse.jetty.jsp.precompiled";
76  
77  
78      /**
79       * JettyJspC
80       *
81       * Add some extra setters to standard JspC class to help configure it
82       * for running in maven.
83       * 
84       * TODO move all setters on the plugin onto this jspc class instead.
85       */
86      public static class JettyJspC extends JspC
87      {
88     
89          private boolean scanAll;
90          
91          public void setClassLoader (ClassLoader loader)
92          {
93              this.loader = loader;
94          }
95          
96         public void setScanAllDirectories (boolean scanAll)
97         {
98             this.scanAll = scanAll;
99         }
100        
101        public boolean getScanAllDirectories ()
102        {
103            return this.scanAll;
104        }
105        
106 
107         @Override
108         protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal)
109         {            
110             if (context != null && context.getAttribute(JarScanner.class.getName()) == null) 
111             {
112                 StandardJarScanner jarScanner = new StandardJarScanner();             
113                 jarScanner.setScanAllDirectories(getScanAllDirectories());
114                 context.setAttribute(JarScanner.class.getName(), jarScanner);
115             }
116                 
117             return super.newTldScanner(context, namespaceAware, validate, blockExternal);
118         }      
119     }
120     
121     
122     /**
123      * Whether or not to include dependencies on the plugin's classpath with &lt;scope&gt;provided&lt;/scope&gt;
124      * Use WITH CAUTION as you may wind up with duplicate jars/classes.
125      * 
126      * @since jetty-7.6.3
127      * @parameter  default-value="false"
128      */
129     private boolean useProvidedScope;
130     
131     /**
132      * The artifacts for the project.
133      * 
134      * @since jetty-7.6.3
135      * @parameter default-value="${project.artifacts}"
136      * @readonly
137      */
138     private Set projectArtifacts;
139     
140     
141     /**
142      * The maven project.
143      * 
144      * @parameter default-value="${project}"
145      * @required
146      * @readonly
147      */
148     private MavenProject project;
149 
150     
151 
152     /**
153      * The artifacts for the plugin itself.
154      * 
155      * @parameter default-value="${plugin.artifacts}"
156      * @readonly
157      */
158     private List pluginArtifacts;
159     
160     
161     /**
162      * File into which to generate the &lt;servlet&gt; and
163      * &lt;servlet-mapping&gt; tags for the compiled jsps
164      * 
165      * @parameter default-value="${basedir}/target/webfrag.xml"
166      */
167     private String webXmlFragment;
168 
169     /**
170      * Optional. A marker string in the src web.xml file which indicates where
171      * to merge in the generated web.xml fragment. Note that the marker string
172      * will NOT be preserved during the insertion. Can be left blank, in which
173      * case the generated fragment is inserted just before the &lt;/web-app&gt;
174      * line
175      * 
176      * @parameter
177      */
178     private String insertionMarker;
179 
180     /**
181      * Merge the generated fragment file with the web.xml from
182      * webAppSourceDirectory. The merged file will go into the same directory as
183      * the webXmlFragment.
184      * 
185      * @parameter default-value="true"
186      */
187     private boolean mergeFragment;
188 
189     /**
190      * The destination directory into which to put the compiled jsps.
191      * 
192      * @parameter default-value="${project.build.outputDirectory}"
193      */
194     private String generatedClasses;
195 
196     /**
197      * Controls whether or not .java files generated during compilation will be
198      * preserved.
199      * 
200      * @parameter default-value="false"
201      */
202     private boolean keepSources;
203 
204 
205     /**
206      * Root directory for all html/jsp etc files
207      * 
208      * @parameter default-value="${basedir}/src/main/webapp"
209      * 
210      */
211     private String webAppSourceDirectory;
212     
213    
214     
215     /**
216      * Location of web.xml. Defaults to src/main/webapp/web.xml.
217      * @parameter default-value="${basedir}/src/main/webapp/WEB-INF/web.xml"
218      */
219     private String webXml;
220 
221 
222     /**
223      * The comma separated list of patterns for file extensions to be processed. By default
224      * will include all .jsp and .jspx files.
225      * 
226      * @parameter default-value="**\/*.jsp, **\/*.jspx"
227      */
228     private String includes;
229 
230     /**
231      * The comma separated list of file name patters to exclude from compilation.
232      * 
233      * @parameter default_value="**\/.svn\/**";
234      */
235     private String excludes;
236 
237     /**
238      * The location of the compiled classes for the webapp
239      * 
240      * @parameter default-value="${project.build.outputDirectory}"
241      */
242     private File classesDirectory;
243 
244     
245     /**
246      * Patterns of jars on the system path that contain tlds. Use | to separate each pattern.
247      * 
248      * @parameter default-value=".*taglibs[^/]*\.jar|.*jstl[^/]*\.jar$
249      */
250     private String tldJarNamePatterns;
251     
252     
253     /**
254      * Source version - if not set defaults to jsp default (currently 1.7)
255      * @parameter 
256      */
257     private String sourceVersion;
258     
259     
260     /**
261      * Target version - if not set defaults to jsp default (currently 1.7)
262      * @parameter 
263      */
264     private String targetVersion;
265     
266     /**
267      * 
268      * The JspC instance being used to compile the jsps.
269      * 
270      * @parameter
271      */
272     private JettyJspC jspc;
273 
274 
275     /**
276      * Whether dirs on the classpath should be scanned as well as jars.
277      * True by default. This allows for scanning for tlds of dependent projects that
278      * are in the reactor as unassembled jars.
279      * 
280      * @parameter default-value=true
281      */
282     private boolean scanAllDirectories;
283     
284 
285     public void execute() throws MojoExecutionException, MojoFailureException
286     {
287         if (getLog().isDebugEnabled())
288         {
289 
290             getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
291             getLog().info("generatedClasses=" + generatedClasses);
292             getLog().info("webXmlFragment=" + webXmlFragment);
293             getLog().info("webXml="+webXml);
294             getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker));
295             getLog().info("keepSources=" + keepSources);
296             getLog().info("mergeFragment=" + mergeFragment);  
297             if (sourceVersion != null)
298                 getLog().info("sourceVersion="+sourceVersion);
299             if (targetVersion != null)
300                 getLog().info("targetVersion="+targetVersion);
301         }
302         try
303         {
304             prepare();
305             compile();
306             cleanupSrcs();
307             mergeWebXml();
308         }
309         catch (Exception e)
310         {
311             throw new MojoExecutionException("Failure processing jsps", e);
312         }
313     }
314 
315     public void compile() throws Exception
316     {
317         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
318 
319         //set up the classpath of the webapp
320         List<URL> webAppUrls = setUpWebAppClassPath();
321         
322         //set up the classpath of the container (ie jetty and jsp jars)
323         Set<URL> pluginJars = getPluginJars();
324         Set<URL> providedJars = getProvidedScopeJars(pluginJars);
325  
326 
327         //Make a classloader so provided jars will be on the classpath
328         List<URL> sysUrls = new ArrayList<URL>();      
329         sysUrls.addAll(providedJars);     
330         URLClassLoader sysClassLoader = new URLClassLoader((URL[])sysUrls.toArray(new URL[0]), currentClassLoader);
331       
332         //make a classloader with the webapp classpath
333         URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), sysClassLoader);
334         StringBuffer webAppClassPath = new StringBuffer();
335 
336         for (int i = 0; i < webAppUrls.size(); i++)
337         {
338             if (getLog().isDebugEnabled())
339                 getLog().debug("webappclassloader contains: " + webAppUrls.get(i));                
340             webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
341             if (getLog().isDebugEnabled())
342                 getLog().debug("added to classpath: " + ((URL) webAppUrls.get(i)).getFile());
343             if (i+1<webAppUrls.size())
344                 webAppClassPath.append(System.getProperty("path.separator"));
345         }
346         
347         //Interpose a fake classloader as the webapp class loader. This is because the Apache JspC class
348         //uses a TldScanner which ignores jars outside of the WEB-INF/lib path on the webapp classloader.
349         //It will, however, look at all jars on the parents of the webapp classloader.
350         URLClassLoader fakeWebAppClassLoader = new URLClassLoader(new URL[0], webAppClassLoader);
351         Thread.currentThread().setContextClassLoader(fakeWebAppClassLoader);
352   
353         if (jspc == null)
354             jspc = new JettyJspC();
355         
356 
357         jspc.setWebXmlFragment(webXmlFragment);
358         jspc.setUriroot(webAppSourceDirectory);     
359         jspc.setOutputDir(generatedClasses);
360         jspc.setClassLoader(fakeWebAppClassLoader);
361         jspc.setScanAllDirectories(scanAllDirectories);
362         jspc.setCompile(true);
363         if (sourceVersion != null)
364             jspc.setCompilerSourceVM(sourceVersion);
365         if (targetVersion != null)
366             jspc.setCompilerTargetVM(targetVersion);
367 
368         // JspC#setExtensions() does not exist, so 
369         // always set concrete list of files that will be processed.
370         String jspFiles = getJspFiles(webAppSourceDirectory);
371        
372         try
373         {
374             if (jspFiles == null | jspFiles.equals(""))
375             {
376                 getLog().info("No files selected to precompile");
377             }
378             else
379             {
380                 getLog().info("Compiling "+jspFiles+" from includes="+includes+" excludes="+excludes);
381                 jspc.setJspFiles(jspFiles);
382                 jspc.execute();
383             }
384         }
385         finally
386         {
387 
388             Thread.currentThread().setContextClassLoader(currentClassLoader);
389         }
390     }
391 
392     private String getJspFiles(String webAppSourceDirectory)
393     throws Exception
394     {
395         List fileNames =  FileUtils.getFileNames(new File(webAppSourceDirectory),includes, excludes, false);
396         return StringUtils.join(fileNames.toArray(new String[0]), ",");
397 
398     }
399 
400     /**
401      * Until Jasper supports the option to generate the srcs in a different dir
402      * than the classes, this is the best we can do.
403      * 
404      * @throws Exception if unable to clean srcs
405      */
406     public void cleanupSrcs() throws Exception
407     {
408         // delete the .java files - depending on keepGenerated setting
409         if (!keepSources)
410         {
411             File generatedClassesDir = new File(generatedClasses);
412 
413             if(generatedClassesDir.exists() && generatedClassesDir.isDirectory())
414             {
415                 delete(generatedClassesDir, new FileFilter()
416                 {
417                     public boolean accept(File f)
418                     {
419                         return f.isDirectory() || f.getName().endsWith(".java");
420                     }                
421                 });
422             }
423         }
424     }
425     
426     static void delete(File dir, FileFilter filter)
427     {
428         File[] files = dir.listFiles(filter);
429         if (files != null)
430         {
431             for(File f: files)
432             {
433                 if(f.isDirectory())
434                     delete(f, filter);
435                 else
436                     f.delete();
437             }
438         }
439     }
440 
441     /**
442      * Take the web fragment and put it inside a copy of the web.xml.
443      * 
444      * You can specify the insertion point by specifying the string in the
445      * insertionMarker configuration entry.
446      * 
447      * If you dont specify the insertionMarker, then the fragment will be
448      * inserted at the end of the file just before the &lt;/webapp&gt;
449      * 
450      * @throws Exception if unable to merge the web xml
451      */
452     public void mergeWebXml() throws Exception
453     {
454         if (mergeFragment)
455         {
456             // open the src web.xml
457             File webXml = getWebXmlFile();
458            
459             if (!webXml.exists())
460             {
461                 getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
462                 return;
463             }
464 
465             File fragmentWebXml = new File(webXmlFragment);         
466             File mergedWebXml = new File(fragmentWebXml.getParentFile(), "web.xml");
467 
468             try (BufferedReader webXmlReader = new BufferedReader(new FileReader(webXml));
469                  PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(mergedWebXml))) 
470             {
471 
472                 if (!fragmentWebXml.exists())
473                 {
474                     getLog().info("No fragment web.xml file generated");
475                     //just copy existing web.xml to expected position
476                     IO.copy(webXmlReader, mergedWebXmlWriter);
477                 }
478                 else
479                 {
480                     // read up to the insertion marker or the </webapp> if there is no
481                     // marker
482                     boolean atInsertPoint = false;
483                     boolean atEOF = false;
484                     String marker = (insertionMarker == null
485                             || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker);
486                     while (!atInsertPoint && !atEOF)
487                     {
488                         String line = webXmlReader.readLine();
489                         if (line == null)
490                             atEOF = true;
491                         else if (line.indexOf(marker) >= 0)
492                         {
493                             atInsertPoint = true;
494                         }
495                         else
496                         {
497                             mergedWebXmlWriter.println(line);
498                         }
499                     }
500 
501                     if (atEOF && !atInsertPoint)
502                         throw new IllegalStateException("web.xml does not contain insertionMarker "+insertionMarker);
503 
504                     //put in a context init-param to flag that the contents have been precompiled
505                     mergedWebXmlWriter.println("<context-param><param-name>"+PRECOMPILED_FLAG+"</param-name><param-value>true</param-value></context-param>");
506 
507 
508                     // put in the generated fragment
509                     try (BufferedReader fragmentWebXmlReader = 
510                             new BufferedReader(new FileReader(fragmentWebXml))) 
511                     {
512                         IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);
513 
514                         // if we inserted just before the </web-app>, put it back in
515                         if (marker.equals(END_OF_WEBAPP))
516                             mergedWebXmlWriter.println(END_OF_WEBAPP);
517 
518                         // copy in the rest of the original web.xml file
519                         IO.copy(webXmlReader, mergedWebXmlWriter);
520                     }
521                 }
522             }
523         }
524     }
525 
526     private void prepare() throws Exception
527     {
528         // For some reason JspC doesn't like it if the dir doesn't
529         // already exist and refuses to create the web.xml fragment
530         File generatedSourceDirectoryFile = new File(generatedClasses);
531         if (!generatedSourceDirectoryFile.exists())
532             generatedSourceDirectoryFile.mkdirs();
533     }
534 
535     /**
536      * Set up the execution classpath for Jasper.
537      * 
538      * Put everything in the classesDirectory and all of the dependencies on the
539      * classpath.
540      * 
541      * @returns a list of the urls of the dependencies
542      * @throws Exception
543      */
544     private List<URL> setUpWebAppClassPath() throws Exception
545     {
546         //add any classes from the webapp
547         List<URL> urls = new ArrayList<URL>();
548         String classesDir = classesDirectory.getCanonicalPath();
549         classesDir = classesDir + (classesDir.endsWith(File.pathSeparator) ? "" : File.separator);
550         urls.add(Resource.toURL(new File(classesDir)));
551 
552         if (getLog().isDebugEnabled())
553             getLog().debug("Adding to classpath classes dir: " + classesDir);
554 
555         //add the dependencies of the webapp (which will form WEB-INF/lib)
556         for (Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext();)
557         {
558             Artifact artifact = (Artifact)iter.next();
559 
560             // Include runtime and compile time libraries
561             if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
562             {
563                 String filePath = artifact.getFile().getCanonicalPath();
564                 if (getLog().isDebugEnabled())
565                     getLog().debug("Adding to classpath dependency file: " + filePath);
566 
567                 urls.add(Resource.toURL(artifact.getFile()));
568             }
569         }
570         return urls;
571     }
572     
573     
574     
575     /**
576      * @return
577      * @throws MalformedURLException
578      */
579     private Set<URL> getPluginJars () throws MalformedURLException
580     {
581         HashSet<URL> pluginJars = new HashSet<>();
582         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
583         {
584             Artifact pluginArtifact = iter.next();
585             if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
586             {
587                 if (getLog().isDebugEnabled()) { getLog().debug("Adding plugin artifact "+pluginArtifact);}
588                 pluginJars.add(pluginArtifact.getFile().toURI().toURL());
589             }
590         }
591         
592         return pluginJars;
593     }
594     
595     
596     
597     /**
598      * @param pluginJars
599      * @return
600      * @throws MalformedURLException
601      */
602     private Set<URL>  getProvidedScopeJars (Set<URL> pluginJars) throws MalformedURLException
603     {
604         if (!useProvidedScope)
605             return Collections.emptySet();
606         
607         HashSet<URL> providedJars = new HashSet<>();
608         
609         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
610         {                   
611             Artifact artifact = iter.next();
612             if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
613             {
614                 //test to see if the provided artifact was amongst the plugin artifacts
615                 URL jar = artifact.getFile().toURI().toURL();
616                 if (!pluginJars.contains(jar))
617                 {
618                     providedJars.add(jar);
619                     if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
620                 }  
621                 else
622                 {
623                     if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);}
624                 }
625             }
626         }
627         return providedJars;
628     }
629 
630     
631     
632     private File getWebXmlFile ()
633     throws IOException
634     {
635         File file = null;
636         File baseDir = project.getBasedir().getCanonicalFile();
637         File defaultWebAppSrcDir = new File (baseDir, "src/main/webapp").getCanonicalFile();
638         File webAppSrcDir = new File (webAppSourceDirectory).getCanonicalFile();
639         File defaultWebXml = new File (defaultWebAppSrcDir, "web.xml").getCanonicalFile();
640         
641         //If the web.xml has been changed from the default, try that
642         File webXmlFile = new File (webXml).getCanonicalFile();
643         if (webXmlFile.compareTo(defaultWebXml) != 0)
644         {
645             file = new File (webXml);
646             return file;
647         }
648         
649         //If the web app src directory has not been changed from the default, use whatever
650         //is set for the web.xml location
651         file = new File (webAppSrcDir, "web.xml");
652         return file;
653     }
654 }