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     /**
168      * Root directory for all html/jsp etc files
169      * 
170      * @parameter default-value="${basedir}/src/main/webapp"
171      * 
172      */
173     private String webAppSourceDirectory;
174     
175    
176     
177     /**
178      * Location of web.xml. Defaults to src/main/webapp/web.xml.
179      * @parameter default-value="${basedir}/src/main/webapp/WEB-INF/web.xml"
180      */
181     private String webXml;
182 
183 
184     /**
185      * The comma separated list of patterns for file extensions to be processed. By default
186      * will include all .jsp and .jspx files.
187      * 
188      * @parameter default-value="**\/*.jsp, **\/*.jspx"
189      */
190     private String includes;
191 
192     /**
193      * The comma separated list of file name patters to exclude from compilation.
194      * 
195      * @parameter default_value="**\/.svn\/**";
196      */
197     private String excludes;
198 
199     /**
200      * The location of the compiled classes for the webapp
201      * 
202      * @parameter expression="${project.build.outputDirectory}"
203      */
204     private File classesDirectory;
205 
206     
207     /**
208      * Patterns of jars on the system path that contain tlds. Use | to separate each pattern.
209      * 
210      * @parameter default-value=".*taglibs[^/]*\.jar|.*jstl-impl[^/]*\.jar$
211      */
212     private String tldJarNamePatterns;
213     
214     
215     /**
216      * 
217      * The JspC instance being used to compile the jsps.
218      * 
219      * @parameter
220      */
221     private JspC jspc;
222 
223 
224 
225     
226 
227     public void execute() throws MojoExecutionException, MojoFailureException
228     {
229         if (getLog().isDebugEnabled())
230         {
231 
232             getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
233             getLog().info("generatedClasses=" + generatedClasses);
234             getLog().info("webXmlFragment=" + webXmlFragment);
235             getLog().info("webXml="+webXml);
236             getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker));
237             getLog().info("keepSources=" + keepSources);
238             getLog().info("mergeFragment=" + mergeFragment);            
239         }
240         try
241         {
242             prepare();
243             compile();
244             cleanupSrcs();
245             mergeWebXml();
246         }
247         catch (Exception e)
248         {
249             throw new MojoExecutionException("Failure processing jsps", e);
250         }
251     }
252 
253     public void compile() throws Exception
254     {
255         ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
256 
257         //set up the classpath of the webapp
258         List<URL> webAppUrls = setUpWebAppClassPath();
259         
260         //set up the classpath of the container (ie jetty and jsp jars)
261         String sysClassPath = setUpSysClassPath();
262         
263         //get the list of system classpath jars that contain tlds
264         List<URL> tldJarUrls = getSystemJarsWithTlds();
265         
266         for (URL u:tldJarUrls)
267         {
268             if (getLog().isDebugEnabled())
269                 getLog().debug(" sys jar with tlds: "+u);
270             webAppUrls.add(u);
271         }
272 
273       
274         //use the classpaths as the classloader
275         URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), currentClassLoader);
276         StringBuffer webAppClassPath = new StringBuffer();
277 
278         for (int i = 0; i < webAppUrls.size(); i++)
279         {
280             if (getLog().isDebugEnabled())
281                 getLog().debug("webappclassloader contains: " + webAppUrls.get(i));                
282             webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
283             if (getLog().isDebugEnabled())
284                 getLog().debug("added to classpath: " + ((URL) webAppUrls.get(i)).getFile());
285             if (i+1<webAppUrls.size())
286                 webAppClassPath.append(System.getProperty("path.separator"));
287         }
288 
289         Thread.currentThread().setContextClassLoader(webAppClassLoader);
290   
291         if (jspc == null)
292             jspc = new JspC();
293         
294         jspc.setWebXmlFragment(webXmlFragment);
295         jspc.setUriroot(webAppSourceDirectory);     
296         jspc.setOutputDir(generatedClasses);
297         jspc.setClassPath(webAppClassPath.toString());
298         jspc.setCompile(true);
299         jspc.setSystemClassPath(sysClassPath);
300                
301 
302         // JspC#setExtensions() does not exist, so 
303         // always set concrete list of files that will be processed.
304         String jspFiles = getJspFiles(webAppSourceDirectory);
305         getLog().info("Compiling "+jspFiles);
306         getLog().info("Includes="+includes);
307         getLog().info("Excludes="+excludes);
308         jspc.setJspFiles(jspFiles);
309 
310         getLog().info("Files selected to precompile: " + jspFiles);
311 
312         jspc.execute();
313 
314         Thread.currentThread().setContextClassLoader(currentClassLoader);
315     }
316 
317     private String getJspFiles(String webAppSourceDirectory)
318     throws Exception
319     {
320         List fileNames =  FileUtils.getFileNames(new File(webAppSourceDirectory),includes, excludes, false);
321         return StringUtils.join(fileNames.toArray(new String[0]), ",");
322 
323     }
324 
325     /**
326      * Until Jasper supports the option to generate the srcs in a different dir
327      * than the classes, this is the best we can do.
328      * 
329      * @throws Exception
330      */
331     public void cleanupSrcs() throws Exception
332     {
333         // delete the .java files - depending on keepGenerated setting
334         if (!keepSources)
335         {
336             File generatedClassesDir = new File(generatedClasses);
337 
338             if(generatedClassesDir.exists() && generatedClassesDir.isDirectory())
339             {
340                 delete(generatedClassesDir, new FileFilter()
341                 {
342                     public boolean accept(File f)
343                     {
344                         return f.isDirectory() || f.getName().endsWith(".java");
345                     }                
346                 });
347             }
348         }
349     }
350     
351     static void delete(File dir, FileFilter filter)
352     {
353         File[] files = dir.listFiles(filter);
354         if (files != null)
355         {
356             for(File f: files)
357             {
358                 if(f.isDirectory())
359                     delete(f, filter);
360                 else
361                     f.delete();
362             }
363         }
364     }
365 
366     /**
367      * Take the web fragment and put it inside a copy of the web.xml.
368      * 
369      * You can specify the insertion point by specifying the string in the
370      * insertionMarker configuration entry.
371      * 
372      * If you dont specify the insertionMarker, then the fragment will be
373      * inserted at the end of the file just before the &lt;/webapp&gt;
374      * 
375      * @throws Exception
376      */
377     public void mergeWebXml() throws Exception
378     {
379         if (mergeFragment)
380         {
381             // open the src web.xml
382             File webXml = getWebXmlFile();
383            
384             if (!webXml.exists())
385             {
386                 getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
387                 return;
388             }
389 
390             File fragmentWebXml = new File(webXmlFragment);
391             if (!fragmentWebXml.exists())
392             {
393                 getLog().info("No fragment web.xml file generated");
394             }
395             File mergedWebXml = new File(fragmentWebXml.getParentFile(),
396             "web.xml");
397             try (BufferedReader webXmlReader = new BufferedReader(new FileReader(
398                     webXml));
399                  PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(
400                     mergedWebXml))) {
401 
402                 // read up to the insertion marker or the </webapp> if there is no
403                 // marker
404                 boolean atInsertPoint = false;
405                 boolean atEOF = false;
406                 String marker = (insertionMarker == null
407                         || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker);
408                 while (!atInsertPoint && !atEOF)
409                 {
410                     String line = webXmlReader.readLine();
411                     if (line == null)
412                         atEOF = true;
413                     else if (line.indexOf(marker) >= 0)
414                     {
415                         atInsertPoint = true;
416                     }
417                     else
418                     {
419                         mergedWebXmlWriter.println(line);
420                     }
421                 }
422 
423                 // put in the generated fragment
424                 try (BufferedReader fragmentWebXmlReader = new BufferedReader(
425                         new FileReader(fragmentWebXml))) {
426                     IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);
427 
428                     // if we inserted just before the </web-app>, put it back in
429                     if (marker.equals(END_OF_WEBAPP))
430                         mergedWebXmlWriter.println(END_OF_WEBAPP);
431 
432                     // copy in the rest of the original web.xml file
433                     IO.copy(webXmlReader, mergedWebXmlWriter);
434                 }
435             }
436         }
437     }
438 
439     private void prepare() throws Exception
440     {
441         // For some reason JspC doesn't like it if the dir doesn't
442         // already exist and refuses to create the web.xml fragment
443         File generatedSourceDirectoryFile = new File(generatedClasses);
444         if (!generatedSourceDirectoryFile.exists())
445             generatedSourceDirectoryFile.mkdirs();
446     }
447 
448     /**
449      * Set up the execution classpath for Jasper.
450      * 
451      * Put everything in the classesDirectory and all of the dependencies on the
452      * classpath.
453      * 
454      * @returns a list of the urls of the dependencies
455      * @throws Exception
456      */
457     private List<URL> setUpWebAppClassPath() throws Exception
458     {
459         //add any classes from the webapp
460         List<URL> urls = new ArrayList<URL>();
461         String classesDir = classesDirectory.getCanonicalPath();
462         classesDir = classesDir + (classesDir.endsWith(File.pathSeparator) ? "" : File.separator);
463         urls.add(Resource.toURL(new File(classesDir)));
464 
465         if (getLog().isDebugEnabled())
466             getLog().debug("Adding to classpath classes dir: " + classesDir);
467 
468         //add the dependencies of the webapp (which will form WEB-INF/lib)
469         for (Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext();)
470         {
471             Artifact artifact = (Artifact)iter.next();
472 
473             // Include runtime and compile time libraries
474             if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
475             {
476                 String filePath = artifact.getFile().getCanonicalPath();
477                 if (getLog().isDebugEnabled())
478                     getLog().debug("Adding to classpath dependency file: " + filePath);
479 
480                 urls.add(Resource.toURL(artifact.getFile()));
481             }
482         }
483         return urls;
484     }
485     
486     
487     private String setUpSysClassPath () throws Exception
488     {
489         StringBuffer buff = new StringBuffer();
490         
491         //Put each of the plugin's artifacts onto the system classpath for jspc
492         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
493         {
494             Artifact pluginArtifact = iter.next();
495             if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
496             {
497                 if (getLog().isDebugEnabled()) { getLog().debug("Adding plugin artifact "+pluginArtifact);}
498                 buff.append(pluginArtifact.getFile().getAbsolutePath());
499                 if (iter.hasNext())
500                     buff.append(File.pathSeparator);
501             }
502         }
503         
504         
505         if (useProvidedScope)
506         {
507             for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
508             {                   
509                 Artifact artifact = iter.next();
510                 if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
511                 {
512                     //test to see if the provided artifact was amongst the plugin artifacts
513                     String path = artifact.getFile().getAbsolutePath();
514                     if (! buff.toString().contains(path))
515                     {
516                         if (buff.length() != 0)
517                             buff.append(File.pathSeparator);
518                         buff.append(path);
519                         if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
520                     }  
521                     else
522                     {
523                         if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);}
524                     }
525                 }
526             }
527         }
528 
529         return buff.toString();
530     }
531 
532     
533     /**
534      * Glassfish jsp requires that we set up the list of system jars that have
535      * tlds in them.
536      * 
537      * This method is a little fragile, as it relies on knowing that the jstl jars
538      * are the only ones in the system path that contain tlds.
539      * @return
540      * @throws Exception
541      */
542     private List<URL> getSystemJarsWithTlds() throws Exception
543     {
544         final List<URL> list = new ArrayList<URL>();
545         List<URI> artifactUris = new ArrayList<URI>();
546         Pattern pattern = Pattern.compile(tldJarNamePatterns);
547         for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
548         {
549             Artifact pluginArtifact = iter.next();
550             artifactUris.add(Resource.newResource(pluginArtifact.getFile()).getURI());
551         }
552         
553         PatternMatcher matcher = new PatternMatcher()
554         {
555             public void matched(URI uri) throws Exception
556             {
557                 //uri of system artifact matches pattern defining list of jars known to contain tlds
558                 list.add(uri.toURL());
559             }
560         };
561         matcher.match(pattern, artifactUris.toArray(new URI[artifactUris.size()]), false);
562         
563         return list;
564     }
565     
566     private File getWebXmlFile ()
567     throws IOException
568     {
569         File file = null;
570         File baseDir = project.getBasedir().getCanonicalFile();
571         File defaultWebAppSrcDir = new File (baseDir, "src/main/webapp").getCanonicalFile();
572         File webAppSrcDir = new File (webAppSourceDirectory).getCanonicalFile();
573         File defaultWebXml = new File (defaultWebAppSrcDir, "web.xml").getCanonicalFile();
574         
575         //If the web.xml has been changed from the default, try that
576         File webXmlFile = new File (webXml).getCanonicalFile();
577         if (webXmlFile.compareTo(defaultWebXml) != 0)
578         {
579             file = new File (webXml);
580             return file;
581         }
582         
583         //If the web app src directory has not been changed from the default, use whatever
584         //is set for the web.xml location
585         file = new File (webAppSrcDir, "web.xml");
586         return file;
587     }
588 }