View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 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      * 
255      * The JspC instance being used to compile the jsps.
256      * 
257      * @parameter
258      */
259     private JettyJspC jspc;
260 
261 
262     /**
263      * Whether dirs on the classpath should be scanned as well as jars.
264      * True by default. This allows for scanning for tlds of dependent projects that
265      * are in the reactor as unassembled jars.
266      * 
267      * @parameter default-value=true
268      */
269     private boolean scanAllDirectories;
270     
271 
272     public void execute() throws MojoExecutionException, MojoFailureException
273     {
274         if (getLog().isDebugEnabled())
275         {
276 
277             getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
278             getLog().info("generatedClasses=" + generatedClasses);
279             getLog().info("webXmlFragment=" + webXmlFragment);
280             getLog().info("webXml="+webXml);
281             getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker));
282             getLog().info("keepSources=" + keepSources);
283             getLog().info("mergeFragment=" + mergeFragment);            
284         }
285         try
286         {
287             prepare();
288             compile();
289             cleanupSrcs();
290             mergeWebXml();
291         }
292         catch (Exception e)
293         {
294             throw new MojoExecutionException("Failure processing jsps", e);
295         }
296     }
297 
298     public void compile() throws Exception
299     {
300         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
301 
302         //set up the classpath of the webapp
303         List<URL> webAppUrls = setUpWebAppClassPath();
304         
305         //set up the classpath of the container (ie jetty and jsp jars)
306         Set<URL> pluginJars = getPluginJars();
307         Set<URL> providedJars = getProvidedScopeJars(pluginJars);
308  
309 
310         //Make a classloader so provided jars will be on the classpath
311         List<URL> sysUrls = new ArrayList<URL>();      
312         sysUrls.addAll(providedJars);     
313         URLClassLoader sysClassLoader = new URLClassLoader((URL[])sysUrls.toArray(new URL[0]), currentClassLoader);
314       
315         //make a classloader with the webapp classpath
316         URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), sysClassLoader);
317         StringBuffer webAppClassPath = new StringBuffer();
318 
319         for (int i = 0; i < webAppUrls.size(); i++)
320         {
321             if (getLog().isDebugEnabled())
322                 getLog().debug("webappclassloader contains: " + webAppUrls.get(i));                
323             webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
324             if (getLog().isDebugEnabled())
325                 getLog().debug("added to classpath: " + ((URL) webAppUrls.get(i)).getFile());
326             if (i+1<webAppUrls.size())
327                 webAppClassPath.append(System.getProperty("path.separator"));
328         }
329         
330         //Interpose a fake classloader as the webapp class loader. This is because the Apache JspC class
331         //uses a TldScanner which ignores jars outside of the WEB-INF/lib path on the webapp classloader.
332         //It will, however, look at all jars on the parents of the webapp classloader.
333         URLClassLoader fakeWebAppClassLoader = new URLClassLoader(new URL[0], webAppClassLoader);
334         Thread.currentThread().setContextClassLoader(fakeWebAppClassLoader);
335   
336         if (jspc == null)
337             jspc = new JettyJspC();
338         
339 
340         jspc.setWebXmlFragment(webXmlFragment);
341         jspc.setUriroot(webAppSourceDirectory);     
342         jspc.setOutputDir(generatedClasses);
343         jspc.setClassLoader(fakeWebAppClassLoader);
344         jspc.setScanAllDirectories(scanAllDirectories);
345         jspc.setCompile(true);
346 
347         // JspC#setExtensions() does not exist, so 
348         // always set concrete list of files that will be processed.
349         String jspFiles = getJspFiles(webAppSourceDirectory);
350        
351         try
352         {
353             if (jspFiles == null | jspFiles.equals(""))
354             {
355                 getLog().info("No files selected to precompile");
356             }
357             else
358             {
359                 getLog().info("Compiling "+jspFiles+" from includes="+includes+" excludes="+excludes);
360                 jspc.setJspFiles(jspFiles);
361                 jspc.execute();
362             }
363         }
364         finally
365         {
366 
367             Thread.currentThread().setContextClassLoader(currentClassLoader);
368         }
369     }
370 
371     private String getJspFiles(String webAppSourceDirectory)
372     throws Exception
373     {
374         List fileNames =  FileUtils.getFileNames(new File(webAppSourceDirectory),includes, excludes, false);
375         return StringUtils.join(fileNames.toArray(new String[0]), ",");
376 
377     }
378 
379     /**
380      * Until Jasper supports the option to generate the srcs in a different dir
381      * than the classes, this is the best we can do.
382      * 
383      * @throws Exception if unable to clean srcs
384      */
385     public void cleanupSrcs() throws Exception
386     {
387         // delete the .java files - depending on keepGenerated setting
388         if (!keepSources)
389         {
390             File generatedClassesDir = new File(generatedClasses);
391 
392             if(generatedClassesDir.exists() && generatedClassesDir.isDirectory())
393             {
394                 delete(generatedClassesDir, new FileFilter()
395                 {
396                     public boolean accept(File f)
397                     {
398                         return f.isDirectory() || f.getName().endsWith(".java");
399                     }                
400                 });
401             }
402         }
403     }
404     
405     static void delete(File dir, FileFilter filter)
406     {
407         File[] files = dir.listFiles(filter);
408         if (files != null)
409         {
410             for(File f: files)
411             {
412                 if(f.isDirectory())
413                     delete(f, filter);
414                 else
415                     f.delete();
416             }
417         }
418     }
419 
420     /**
421      * Take the web fragment and put it inside a copy of the web.xml.
422      * 
423      * You can specify the insertion point by specifying the string in the
424      * insertionMarker configuration entry.
425      * 
426      * If you dont specify the insertionMarker, then the fragment will be
427      * inserted at the end of the file just before the &lt;/webapp&gt;
428      * 
429      * @throws Exception if unable to merge the web xml
430      */
431     public void mergeWebXml() throws Exception
432     {
433         if (mergeFragment)
434         {
435             // open the src web.xml
436             File webXml = getWebXmlFile();
437            
438             if (!webXml.exists())
439             {
440                 getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
441                 return;
442             }
443 
444             File fragmentWebXml = new File(webXmlFragment);         
445             File mergedWebXml = new File(fragmentWebXml.getParentFile(), "web.xml");
446 
447             try (BufferedReader webXmlReader = new BufferedReader(new FileReader(webXml));
448                  PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(mergedWebXml))) 
449             {
450 
451                 if (!fragmentWebXml.exists())
452                 {
453                     getLog().info("No fragment web.xml file generated");
454                     //just copy existing web.xml to expected position
455                     IO.copy(webXmlReader, mergedWebXmlWriter);
456                 }
457                 else
458                 {
459                     // read up to the insertion marker or the </webapp> if there is no
460                     // marker
461                     boolean atInsertPoint = false;
462                     boolean atEOF = false;
463                     String marker = (insertionMarker == null
464                             || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker);
465                     while (!atInsertPoint && !atEOF)
466                     {
467                         String line = webXmlReader.readLine();
468                         if (line == null)
469                             atEOF = true;
470                         else if (line.indexOf(marker) >= 0)
471                         {
472                             atInsertPoint = true;
473                         }
474                         else
475                         {
476                             mergedWebXmlWriter.println(line);
477                         }
478                     }
479 
480                     if (atEOF && !atInsertPoint)
481                         throw new IllegalStateException("web.xml does not contain insertionMarker "+insertionMarker);
482 
483                     //put in a context init-param to flag that the contents have been precompiled
484                     mergedWebXmlWriter.println("<context-param><param-name>"+PRECOMPILED_FLAG+"</param-name><param-value>true</param-value></context-param>");
485 
486 
487                     // put in the generated fragment
488                     try (BufferedReader fragmentWebXmlReader = 
489                             new BufferedReader(new FileReader(fragmentWebXml))) 
490                     {
491                         IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);
492 
493                         // if we inserted just before the </web-app>, put it back in
494                         if (marker.equals(END_OF_WEBAPP))
495                             mergedWebXmlWriter.println(END_OF_WEBAPP);
496 
497                         // copy in the rest of the original web.xml file
498                         IO.copy(webXmlReader, mergedWebXmlWriter);
499                     }
500                 }
501             }
502         }
503     }
504 
505     private void prepare() throws Exception
506     {
507         // For some reason JspC doesn't like it if the dir doesn't
508         // already exist and refuses to create the web.xml fragment
509         File generatedSourceDirectoryFile = new File(generatedClasses);
510         if (!generatedSourceDirectoryFile.exists())
511             generatedSourceDirectoryFile.mkdirs();
512     }
513 
514     /**
515      * Set up the execution classpath for Jasper.
516      * 
517      * Put everything in the classesDirectory and all of the dependencies on the
518      * classpath.
519      * 
520      * @returns a list of the urls of the dependencies
521      * @throws Exception
522      */
523     private List<URL> setUpWebAppClassPath() throws Exception
524     {
525         //add any classes from the webapp
526         List<URL> urls = new ArrayList<URL>();
527         String classesDir = classesDirectory.getCanonicalPath();
528         classesDir = classesDir + (classesDir.endsWith(File.pathSeparator) ? "" : File.separator);
529         urls.add(Resource.toURL(new File(classesDir)));
530 
531         if (getLog().isDebugEnabled())
532             getLog().debug("Adding to classpath classes dir: " + classesDir);
533 
534         //add the dependencies of the webapp (which will form WEB-INF/lib)
535         for (Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext();)
536         {
537             Artifact artifact = (Artifact)iter.next();
538 
539             // Include runtime and compile time libraries
540             if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
541             {
542                 String filePath = artifact.getFile().getCanonicalPath();
543                 if (getLog().isDebugEnabled())
544                     getLog().debug("Adding to classpath dependency file: " + filePath);
545 
546                 urls.add(Resource.toURL(artifact.getFile()));
547             }
548         }
549         return urls;
550     }
551     
552     
553     
554     /**
555      * @return
556      * @throws MalformedURLException
557      */
558     private Set<URL> getPluginJars () throws MalformedURLException
559     {
560         HashSet<URL> pluginJars = new HashSet<>();
561         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
562         {
563             Artifact pluginArtifact = iter.next();
564             if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
565             {
566                 if (getLog().isDebugEnabled()) { getLog().debug("Adding plugin artifact "+pluginArtifact);}
567                 pluginJars.add(pluginArtifact.getFile().toURI().toURL());
568             }
569         }
570         
571         return pluginJars;
572     }
573     
574     
575     
576     /**
577      * @param pluginJars
578      * @return
579      * @throws MalformedURLException
580      */
581     private Set<URL>  getProvidedScopeJars (Set<URL> pluginJars) throws MalformedURLException
582     {
583         if (!useProvidedScope)
584             return Collections.emptySet();
585         
586         HashSet<URL> providedJars = new HashSet<>();
587         
588         for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
589         {                   
590             Artifact artifact = iter.next();
591             if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
592             {
593                 //test to see if the provided artifact was amongst the plugin artifacts
594                 URL jar = artifact.getFile().toURI().toURL();
595                 if (!pluginJars.contains(jar))
596                 {
597                     providedJars.add(jar);
598                     if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
599                 }  
600                 else
601                 {
602                     if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);}
603                 }
604             }
605         }
606         return providedJars;
607     }
608 
609     
610     
611     private File getWebXmlFile ()
612     throws IOException
613     {
614         File file = null;
615         File baseDir = project.getBasedir().getCanonicalFile();
616         File defaultWebAppSrcDir = new File (baseDir, "src/main/webapp").getCanonicalFile();
617         File webAppSrcDir = new File (webAppSourceDirectory).getCanonicalFile();
618         File defaultWebXml = new File (defaultWebAppSrcDir, "web.xml").getCanonicalFile();
619         
620         //If the web.xml has been changed from the default, try that
621         File webXmlFile = new File (webXml).getCanonicalFile();
622         if (webXmlFile.compareTo(defaultWebXml) != 0)
623         {
624             file = new File (webXml);
625             return file;
626         }
627         
628         //If the web app src directory has not been changed from the default, use whatever
629         //is set for the web.xml location
630         file = new File (webAppSrcDir, "web.xml");
631         return file;
632     }
633 }