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