View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.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  
83  
84      /**
85       * Whether or not to include dependencies on the plugin's classpath with &lt;scope&gt;provided&lt;/scope&gt;
86       * Use WITH CAUTION as you may wind up with duplicate jars/classes.
87       * 
88       * @since jetty-7.6.3
89       * @parameter  default-value="false"
90       */
91      private boolean useProvidedScope;
92      
93      /**
94       * The artifacts for the project.
95       * 
96       * @since jetty-7.6.3
97       * @parameter expression="${project.artifacts}"
98       * @readonly
99       */
100     private Set projectArtifacts;
101     
102     
103     /**
104      * The maven project.
105      * 
106      * @parameter expression="${project}"
107      * @required
108      * @readonly
109      */
110     private MavenProject project;
111 
112     
113 
114     /**
115      * The artifacts for the plugin itself.
116      * 
117      * @parameter expression="${plugin.artifacts}"
118      * @readonly
119      */
120     private List pluginArtifacts;
121     
122     
123     /**
124      * File into which to generate the &lt;servlet&gt; and
125      * &lt;servlet-mapping&gt; tags for the compiled jsps
126      * 
127      * @parameter default-value="${basedir}/target/webfrag.xml"
128      */
129     private String webXmlFragment;
130 
131     /**
132      * Optional. A marker string in the src web.xml file which indicates where
133      * to merge in the generated web.xml fragment. Note that the marker string
134      * will NOT be preserved during the insertion. Can be left blank, in which
135      * case the generated fragment is inserted just before the &lt;/web-app&gt;
136      * line
137      * 
138      * @parameter
139      */
140     private String insertionMarker;
141 
142     /**
143      * Merge the generated fragment file with the web.xml from
144      * webAppSourceDirectory. The merged file will go into the same directory as
145      * the webXmlFragment.
146      * 
147      * @parameter default-value="true"
148      */
149     private boolean mergeFragment;
150 
151     /**
152      * The destination directory into which to put the compiled jsps.
153      * 
154      * @parameter default-value="${project.build.outputDirectory}"
155      */
156     private String generatedClasses;
157 
158     /**
159      * Controls whether or not .java files generated during compilation will be
160      * preserved.
161      * 
162      * @parameter default-value="false"
163      */
164     private boolean keepSources;
165 
166     /**
167      * Default root package for all generated classes
168      * 
169      * @parameter default-value="jsp"
170      */
171     private String packageRoot;
172 
173     /**
174      * Root directory for all html/jsp etc files
175      * 
176      * @parameter default-value="${basedir}/src/main/webapp"
177      * 
178      */
179     private String webAppSourceDirectory;
180     
181    
182     
183     /**
184      * Location of web.xml. Defaults to src/main/webapp/web.xml.
185      * @parameter default-value="${basedir}/src/main/webapp/WEB-INF/web.xml"
186      */
187     private String webXml;
188 
189 
190     /**
191      * The comma separated list of patterns for file extensions to be processed. By default
192      * will include all .jsp and .jspx files.
193      * 
194      * @parameter default-value="**\/*.jsp, **\/*.jspx"
195      */
196     private String includes;
197 
198     /**
199      * The comma separated list of file name patters to exclude from compilation.
200      * 
201      * @parameter default_value="**\/.svn\/**";
202      */
203     private String excludes;
204 
205     /**
206      * The location of the compiled classes for the webapp
207      * 
208      * @parameter expression="${project.build.outputDirectory}"
209      */
210     private File classesDirectory;
211 
212     /**
213      * Whether or not to output more verbose messages during compilation.
214      * 
215      * @parameter default-value="false";
216      */
217     private boolean verbose;
218 
219     /**
220      * If true, validates tlds when parsing.
221      * 
222      * @parameter default-value="false";
223      */
224     private boolean validateXml;
225 
226     /**
227      * The encoding scheme to use.
228      * 
229      * @parameter default-value="UTF-8"
230      */
231     private String javaEncoding;
232 
233     /**
234      * Whether or not to generate JSR45 compliant debug info
235      * 
236      * @parameter default-value="true";
237      */
238     private boolean suppressSmap;
239 
240     /**
241      * Whether or not to ignore precompilation errors caused by jsp fragments.
242      * 
243      * @parameter default-value="false"
244      */
245     private boolean ignoreJspFragmentErrors;
246 
247     /**
248      * Allows a prefix to be appended to the standard schema locations so that
249      * they can be loaded from elsewhere.
250      * 
251      * @parameter
252      */
253     private String schemaResourcePrefix;
254     
255     /**
256      * Patterns of jars on the system path that contain tlds. Use | to separate each pattern.
257      * 
258      * @parameter default-value=".*taglibs[^/]*\.jar|.*jstl-impl[^/]*\.jar$
259      */
260     private String tldJarNamePatterns;
261 
262 
263 
264     /**
265      * Should white spaces in template text between actions or directives be trimmed? Defaults to false.
266      * @parameter
267      */
268     private boolean trimSpaces = false;
269     
270 
271     public void execute() throws MojoExecutionException, MojoFailureException
272     {
273         if (getLog().isDebugEnabled())
274         {
275             getLog().info("verbose=" + verbose);
276             getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
277             getLog().info("generatedClasses=" + generatedClasses);
278             getLog().info("webXmlFragment=" + webXmlFragment);
279             getLog().info("webXml="+webXml);
280             getLog().info("validateXml=" + validateXml);
281             getLog().info("packageRoot=" + packageRoot);
282             getLog().info("javaEncoding=" + javaEncoding);
283             getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker));
284             getLog().info("keepSources=" + keepSources);
285             getLog().info("mergeFragment=" + mergeFragment);
286             getLog().info("suppressSmap=" + suppressSmap);
287             getLog().info("ignoreJspFragmentErrors=" + ignoreJspFragmentErrors);
288             getLog().info("schemaResourcePrefix=" + schemaResourcePrefix);
289             getLog().info("trimSpaces=" + trimSpaces);
290         }
291         try
292         {
293             prepare();
294             compile();
295             cleanupSrcs();
296             mergeWebXml();
297         }
298         catch (Exception e)
299         {
300             throw new MojoExecutionException("Failure processing jsps", e);
301         }
302     }
303 
304     public void compile() throws Exception
305     {
306         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
307 
308         //set up the classpath of the webapp
309         List<URL> webAppUrls = setUpWebAppClassPath();
310         
311         //set up the classpath of the container (ie jetty and jsp jars)
312         String sysClassPath = setUpSysClassPath();
313         
314         //get the list of system classpath jars that contain tlds
315         List<URL> tldJarUrls = getSystemJarsWithTlds();
316         
317         for (URL u:tldJarUrls)
318         {
319             if (getLog().isDebugEnabled())
320                 getLog().debug(" sys jar with tlds: "+u);
321             webAppUrls.add(u);
322         }
323 
324       
325         //use the classpaths as the classloader
326         URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), currentClassLoader);
327         StringBuffer webAppClassPath = new StringBuffer();
328 
329         for (int i = 0; i < webAppUrls.size(); i++)
330         {
331             if (getLog().isDebugEnabled())
332                 getLog().debug("webappclassloader contains: " + webAppUrls.get(i));                
333             webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
334             if (getLog().isDebugEnabled())
335                 getLog().debug("added to classpath: " + ((URL) webAppUrls.get(i)).getFile());
336             if (i+1<webAppUrls.size())
337                 webAppClassPath.append(System.getProperty("path.separator"));
338         }
339 
340         Thread.currentThread().setContextClassLoader(webAppClassLoader);
341 
342         JspC jspc = new JspC();
343         jspc.setWebXmlFragment(webXmlFragment);
344         jspc.setUriroot(webAppSourceDirectory);
345         jspc.setPackage(packageRoot);
346         jspc.setOutputDir(generatedClasses);
347         jspc.setValidateXml(validateXml);
348         jspc.setClassPath(webAppClassPath.toString());
349         jspc.setCompile(true);
350         jspc.setSmapSuppressed(suppressSmap);
351         jspc.setSmapDumped(!suppressSmap);
352         jspc.setJavaEncoding(javaEncoding);
353         jspc.setTrimSpaces(trimSpaces);
354         jspc.setSystemClassPath(sysClassPath);
355         
356         
357 
358         // JspC#setExtensions() does not exist, so 
359         // always set concrete list of files that will be processed.
360         String jspFiles = getJspFiles(webAppSourceDirectory);
361         getLog().info("Compiling "+jspFiles);
362         getLog().info("Includes="+includes);
363         getLog().info("Excludes="+excludes);
364         jspc.setJspFiles(jspFiles);
365         if (verbose)
366         {
367             getLog().info("Files selected to precompile: " + jspFiles);
368         }
369         
370 
371         try
372         {
373             jspc.setIgnoreJspFragmentErrors(ignoreJspFragmentErrors);
374         }
375         catch (NoSuchMethodError e)
376         {
377             getLog().debug("Tomcat Jasper does not support configuration option 'ignoreJspFragmentErrors': ignored");
378         }
379 
380         try
381         {
382             if (schemaResourcePrefix != null)
383                 jspc.setSchemaResourcePrefix(schemaResourcePrefix);
384         }
385         catch (NoSuchMethodError e)
386         {
387             getLog().debug("Tomcat Jasper does not support configuration option 'schemaResourcePrefix': ignored");
388         }
389         if (verbose)
390             jspc.setVerbose(99);
391         else
392             jspc.setVerbose(0);
393 
394         jspc.execute();
395 
396         Thread.currentThread().setContextClassLoader(currentClassLoader);
397     }
398 
399     private String getJspFiles(String webAppSourceDirectory)
400     throws Exception
401     {
402         List fileNames =  FileUtils.getFileNames(new File(webAppSourceDirectory),includes, excludes, false);
403         return StringUtils.join(fileNames.toArray(new String[0]), ",");
404 
405     }
406 
407     /**
408      * Until Jasper supports the option to generate the srcs in a different dir
409      * than the classes, this is the best we can do.
410      * 
411      * @throws Exception
412      */
413     public void cleanupSrcs() throws Exception
414     {
415         // delete the .java files - depending on keepGenerated setting
416         if (!keepSources)
417         {
418             File generatedClassesDir = new File(generatedClasses);
419 
420             if(generatedClassesDir.exists() && generatedClassesDir.isDirectory())
421             {
422                 delete(generatedClassesDir, new FileFilter()
423                 {
424                     public boolean accept(File f)
425                     {
426                         return f.isDirectory() || f.getName().endsWith(".java");
427                     }                
428                 });
429             }
430         }
431     }
432     
433     static void delete(File dir, FileFilter filter)
434     {
435         File[] files = dir.listFiles(filter);
436         if (files != null)
437         {
438             for(File f: files)
439             {
440                 if(f.isDirectory())
441                     delete(f, filter);
442                 else
443                     f.delete();
444             }
445         }
446     }
447 
448     /**
449      * Take the web fragment and put it inside a copy of the web.xml.
450      * 
451      * You can specify the insertion point by specifying the string in the
452      * insertionMarker configuration entry.
453      * 
454      * If you dont specify the insertionMarker, then the fragment will be
455      * inserted at the end of the file just before the &lt;/webapp&gt;
456      * 
457      * @throws Exception
458      */
459     public void mergeWebXml() throws Exception
460     {
461         if (mergeFragment)
462         {
463             // open the src web.xml
464             File webXml = getWebXmlFile();
465            
466             if (!webXml.exists())
467             {
468                 getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
469                 return;
470             }
471 
472             File fragmentWebXml = new File(webXmlFragment);
473             if (!fragmentWebXml.exists())
474             {
475                 getLog().info("No fragment web.xml file generated");
476             }
477             File mergedWebXml = new File(fragmentWebXml.getParentFile(),
478             "web.xml");
479             try (BufferedReader webXmlReader = new BufferedReader(new FileReader(
480                     webXml));
481                  PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(
482                     mergedWebXml))) {
483 
484                 // read up to the insertion marker or the </webapp> if there is no
485                 // marker
486                 boolean atInsertPoint = false;
487                 boolean atEOF = false;
488                 String marker = (insertionMarker == null
489                         || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker);
490                 while (!atInsertPoint && !atEOF)
491                 {
492                     String line = webXmlReader.readLine();
493                     if (line == null)
494                         atEOF = true;
495                     else if (line.indexOf(marker) >= 0)
496                     {
497                         atInsertPoint = true;
498                     }
499                     else
500                     {
501                         mergedWebXmlWriter.println(line);
502                     }
503                 }
504 
505                 // put in the generated fragment
506                 try (BufferedReader fragmentWebXmlReader = new BufferedReader(
507                         new FileReader(fragmentWebXml))) {
508                     IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);
509 
510                     // if we inserted just before the </web-app>, put it back in
511                     if (marker.equals(END_OF_WEBAPP))
512                         mergedWebXmlWriter.println(END_OF_WEBAPP);
513 
514                     // copy in the rest of the original web.xml file
515                     IO.copy(webXmlReader, mergedWebXmlWriter);
516                 }
517             }
518         }
519     }
520 
521     private void prepare() throws Exception
522     {
523         // For some reason JspC doesn't like it if the dir doesn't
524         // already exist and refuses to create the web.xml fragment
525         File generatedSourceDirectoryFile = new File(generatedClasses);
526         if (!generatedSourceDirectoryFile.exists())
527             generatedSourceDirectoryFile.mkdirs();
528     }
529 
530     /**
531      * Set up the execution classpath for Jasper.
532      * 
533      * Put everything in the classesDirectory and all of the dependencies on the
534      * classpath.
535      * 
536      * @returns a list of the urls of the dependencies
537      * @throws Exception
538      */
539     private List<URL> setUpWebAppClassPath() throws Exception
540     {
541         //add any classes from the webapp
542         List<URL> urls = new ArrayList<URL>();
543         String classesDir = classesDirectory.getCanonicalPath();
544         classesDir = classesDir + (classesDir.endsWith(File.pathSeparator) ? "" : File.separator);
545         urls.add(Resource.toURL(new File(classesDir)));
546 
547         if (getLog().isDebugEnabled())
548             getLog().debug("Adding to classpath classes dir: " + classesDir);
549 
550         //add the dependencies of the webapp (which will form WEB-INF/lib)
551         for (Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext();)
552         {
553             Artifact artifact = (Artifact)iter.next();
554 
555             // Include runtime and compile time libraries
556             if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
557             {
558                 String filePath = artifact.getFile().getCanonicalPath();
559                 if (getLog().isDebugEnabled())
560                     getLog().debug("Adding to classpath dependency file: " + filePath);
561 
562                 urls.add(Resource.toURL(artifact.getFile()));
563             }
564         }
565         return urls;
566     }
567     
568     
569     private String setUpSysClassPath () throws Exception
570     {
571         StringBuffer buff = new StringBuffer();
572         
573         //Put each of the plugin's artifacts onto the system classpath for jspc
574         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
575         {
576             Artifact pluginArtifact = iter.next();
577             if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
578             {
579                 if (getLog().isDebugEnabled()) { getLog().debug("Adding plugin artifact "+pluginArtifact);}
580                 buff.append(pluginArtifact.getFile().getAbsolutePath());
581                 if (iter.hasNext())
582                     buff.append(File.pathSeparator);
583             }
584         }
585         
586         
587         if (useProvidedScope)
588         {
589             for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
590             {                   
591                 Artifact artifact = iter.next();
592                 if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
593                 {
594                     //test to see if the provided artifact was amongst the plugin artifacts
595                     String path = artifact.getFile().getAbsolutePath();
596                     if (! buff.toString().contains(path))
597                     {
598                         if (buff.length() != 0)
599                             buff.append(File.pathSeparator);
600                         buff.append(path);
601                         if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
602                     }  
603                     else
604                     {
605                         if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);}
606                     }
607                 }
608             }
609         }
610 
611         return buff.toString();
612     }
613 
614     
615     /**
616      * Glassfish jsp requires that we set up the list of system jars that have
617      * tlds in them.
618      * 
619      * This method is a little fragile, as it relies on knowing that the jstl jars
620      * are the only ones in the system path that contain tlds.
621      * @return
622      * @throws Exception
623      */
624     private List<URL> getSystemJarsWithTlds() throws Exception
625     {
626         final List<URL> list = new ArrayList<URL>();
627         List<URI> artifactUris = new ArrayList<URI>();
628         Pattern pattern = Pattern.compile(tldJarNamePatterns);
629         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
630         {
631             Artifact pluginArtifact = iter.next();
632             artifactUris.add(Resource.newResource(pluginArtifact.getFile()).getURI());
633         }
634         
635         PatternMatcher matcher = new PatternMatcher()
636         {
637             public void matched(URI uri) throws Exception
638             {
639                 //uri of system artifact matches pattern defining list of jars known to contain tlds
640                 list.add(uri.toURL());
641             }
642         };
643         matcher.match(pattern, artifactUris.toArray(new URI[artifactUris.size()]), false);
644         
645         return list;
646     }
647     
648     private File getWebXmlFile ()
649     throws IOException
650     {
651         File file = null;
652         File baseDir = project.getBasedir().getCanonicalFile();
653         File defaultWebAppSrcDir = new File (baseDir, "src/main/webapp").getCanonicalFile();
654         File webAppSrcDir = new File (webAppSourceDirectory).getCanonicalFile();
655         File defaultWebXml = new File (defaultWebAppSrcDir, "web.xml").getCanonicalFile();
656         
657         //If the web.xml has been changed from the default, try that
658         File webXmlFile = new File (webXml).getCanonicalFile();
659         if (webXmlFile.compareTo(defaultWebXml) != 0)
660         {
661             file = new File (webXml);
662             return file;
663         }
664         
665         //If the web app src directory has not been changed from the default, use whatever
666         //is set for the web.xml location
667         file = new File (webAppSrcDir, "web.xml");
668         return file;
669     }
670 }