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