//========================================================================
//$Id$
//Copyright 2006 Mort Bay Consulting Pty. Ltd.
//------------------------------------------------------------------------
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//http://www.apache.org/licenses/LICENSE-2.0
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.
//========================================================================
package org.mortbay.jetty.jspc.plugin;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.jasper.JspC;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.PatternMatcher;
import org.eclipse.jetty.util.resource.Resource;
/**
* <p>
* This goal will compile jsps for a webapp so that they can be included in a
* war.
* </p>
* <p>
* At runtime, the plugin will use the jsp2.0 jspc compiler if you are running
* on a 1.4 or lower jvm. If you are using a 1.5 jvm, then the jsp2.1 compiler
* will be selected. (this is the same behaviour as the <a
* href="http://jetty.mortbay.org/maven-plugin">jetty plugin</a> for executing
* webapps).
* </p>
* <p>
* Note that the same java compiler will be used as for on-the-fly compiled
* jsps, which will be the Eclipse java compiler.
* </p>
*
* <p>
* See <a
* href="http://docs.codehaus.org/display/JETTY/Maven+Jetty+Jspc+Plugin">Usage
* Guide</a> for instructions on using this plugin.
* </p>
*
* @author janb
*
* @goal jspc
* @phase process-classes
* @requiresDependencyResolution compile
* @description Runs jspc compiler to produce .java and .class files
*/
public class JspcMojo extends AbstractMojo
{
public static final String END_OF_WEBAPP = "</web-app>";
/**
* Whether or not to include dependencies on the plugin's classpath with <scope>provided</scope>
* Use WITH CAUTION as you may wind up with duplicate jars/classes.
*
* @since jetty-7.6.3
* @parameter default-value="false"
*/
private boolean useProvidedScope;
/**
* The artifacts for the project.
*
* @since jetty-7.6.3
* @parameter expression="${project.artifacts}"
* @readonly
*/
private Set projectArtifacts;
/**
* The maven project.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The artifacts for the plugin itself.
*
* @parameter expression="${plugin.artifacts}"
* @readonly
*/
private List pluginArtifacts;
/**
* File into which to generate the <servlet> and
* <servlet-mapping> tags for the compiled jsps
*
* @parameter default-value="${basedir}/target/webfrag.xml"
*/
private String webXmlFragment;
/**
* Optional. A marker string in the src web.xml file which indicates where
* to merge in the generated web.xml fragment. Note that the marker string
* will NOT be preserved during the insertion. Can be left blank, in which
* case the generated fragment is inserted just before the </web-app>
* line
*
* @parameter
*/
private String insertionMarker;
/**
* Merge the generated fragment file with the web.xml from
* webAppSourceDirectory. The merged file will go into the same directory as
* the webXmlFragment.
*
* @parameter default-value="true"
*/
private boolean mergeFragment;
/**
* The destination directory into which to put the compiled jsps.
*
* @parameter default-value="${project.build.outputDirectory}"
*/
private String generatedClasses;
/**
* Controls whether or not .java files generated during compilation will be
* preserved.
*
* @parameter default-value="false"
*/
private boolean keepSources;
/**
* Default root package for all generated classes
*
* @parameter default-value="jsp"
*/
private String packageRoot;
/**
* Root directory for all html/jsp etc files
*
* @parameter default-value="${basedir}/src/main/webapp"
*
*/
private String webAppSourceDirectory;
/**
* Location of web.xml. Defaults to src/main/webapp/web.xml.
* @parameter default-value="${basedir}/src/main/webapp/WEB-INF/web.xml"
*/
private String webXml;
/**
* The comma separated list of patterns for file extensions to be processed. By default
* will include all .jsp and .jspx files.
*
* @parameter default-value="**\/*.jsp, **\/*.jspx"
*/
private String includes;
/**
* The comma separated list of file name patters to exclude from compilation.
*
* @parameter default_value="**\/.svn\/**";
*/
private String excludes;
/**
* The location of the compiled classes for the webapp
*
* @parameter expression="${project.build.outputDirectory}"
*/
private File classesDirectory;
/**
* Whether or not to output more verbose messages during compilation.
*
* @parameter default-value="false";
*/
private boolean verbose;
/**
* If true, validates tlds when parsing.
*
* @parameter default-value="false";
*/
private boolean validateXml;
/**
* The encoding scheme to use.
*
* @parameter default-value="UTF-8"
*/
private String javaEncoding;
/**
* Whether or not to generate JSR45 compliant debug info
*
* @parameter default-value="true";
*/
private boolean suppressSmap;
/**
* Whether or not to ignore precompilation errors caused by jsp fragments.
*
* @parameter default-value="false"
*/
private boolean ignoreJspFragmentErrors;
/**
* Allows a prefix to be appended to the standard schema locations so that
* they can be loaded from elsewhere.
*
* @parameter
*/
private String schemaResourcePrefix;
/**
* Patterns of jars on the system path that contain tlds. Use | to separate each pattern.
*
* @parameter default-value=".*taglibs[^/]*\.jar|.*jstl-impl[^/]*\.jar$
*/
private String tldJarNamePatterns;
/**
* Should white spaces in template text between actions or directives be trimmed? Defaults to false.
* @parameter
*/
private boolean trimSpaces = false;
public void execute() throws MojoExecutionException, MojoFailureException
{
if (getLog().isDebugEnabled())
{
getLog().info("verbose=" + verbose);
getLog().info("webAppSourceDirectory=" + webAppSourceDirectory);
getLog().info("generatedClasses=" + generatedClasses);
getLog().info("webXmlFragment=" + webXmlFragment);
getLog().info("webXml="+webXml);
getLog().info("validateXml=" + validateXml);
getLog().info("packageRoot=" + packageRoot);
getLog().info("javaEncoding=" + javaEncoding);
getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker));
getLog().info("keepSources=" + keepSources);
getLog().info("mergeFragment=" + mergeFragment);
getLog().info("suppressSmap=" + suppressSmap);
getLog().info("ignoreJspFragmentErrors=" + ignoreJspFragmentErrors);
getLog().info("schemaResourcePrefix=" + schemaResourcePrefix);
getLog().info("trimSpaces=" + trimSpaces);
}
try
{
prepare();
compile();
cleanupSrcs();
mergeWebXml();
}
catch (Exception e)
{
throw new MojoExecutionException("Failure processing jsps", e);
}
}
public void compile() throws Exception
{
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
//set up the classpath of the webapp
List<URL> webAppUrls = setUpWebAppClassPath();
//set up the classpath of the container (ie jetty and jsp jars)
String sysClassPath = setUpSysClassPath();
//get the list of system classpath jars that contain tlds
List<URL> tldJarUrls = getSystemJarsWithTlds();
for (URL u:tldJarUrls)
{
if (getLog().isDebugEnabled())
getLog().debug(" sys jar with tlds: "+u);
webAppUrls.add(u);
}
//use the classpaths as the classloader
URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), currentClassLoader);
StringBuffer webAppClassPath = new StringBuffer();
for (int i = 0; i < webAppUrls.size(); i++)
{
if (getLog().isDebugEnabled())
getLog().debug("webappclassloader contains: " + webAppUrls.get(i));
webAppClassPath.append(new File(webAppUrls.get(i).toURI()).getCanonicalPath());
if (getLog().isDebugEnabled())
getLog().debug("added to classpath: " + ((URL) webAppUrls.get(i)).getFile());
if (i+1<webAppUrls.size())
webAppClassPath.append(System.getProperty("path.separator"));
}
Thread.currentThread().setContextClassLoader(webAppClassLoader);
JspC jspc = new JspC();
jspc.setWebXmlFragment(webXmlFragment);
jspc.setUriroot(webAppSourceDirectory);
jspc.setPackage(packageRoot);
jspc.setOutputDir(generatedClasses);
jspc.setValidateXml(validateXml);
jspc.setClassPath(webAppClassPath.toString());
jspc.setCompile(true);
jspc.setSmapSuppressed(suppressSmap);
jspc.setSmapDumped(!suppressSmap);
jspc.setJavaEncoding(javaEncoding);
jspc.setTrimSpaces(trimSpaces);
jspc.setSystemClassPath(sysClassPath);
// JspC#setExtensions() does not exist, so
// always set concrete list of files that will be processed.
String jspFiles = getJspFiles(webAppSourceDirectory);
getLog().info("Compiling "+jspFiles);
getLog().info("Includes="+includes);
getLog().info("Excludes="+excludes);
jspc.setJspFiles(jspFiles);
if (verbose)
{
getLog().info("Files selected to precompile: " + jspFiles);
}
try
{
jspc.setIgnoreJspFragmentErrors(ignoreJspFragmentErrors);
}
catch (NoSuchMethodError e)
{
getLog().debug("Tomcat Jasper does not support configuration option 'ignoreJspFragmentErrors': ignored");
}
try
{
if (schemaResourcePrefix != null)
jspc.setSchemaResourcePrefix(schemaResourcePrefix);
}
catch (NoSuchMethodError e)
{
getLog().debug("Tomcat Jasper does not support configuration option 'schemaResourcePrefix': ignored");
}
if (verbose)
jspc.setVerbose(99);
else
jspc.setVerbose(0);
jspc.execute();
Thread.currentThread().setContextClassLoader(currentClassLoader);
}
private String getJspFiles(String webAppSourceDirectory)
throws Exception
{
List fileNames = FileUtils.getFileNames(new File(webAppSourceDirectory),includes, excludes, false);
return StringUtils.join(fileNames.toArray(new String[0]), ",");
}
/**
* Until Jasper supports the option to generate the srcs in a different dir
* than the classes, this is the best we can do.
*
* @throws Exception
*/
public void cleanupSrcs() throws Exception
{
// delete the .java files - depending on keepGenerated setting
if (!keepSources)
{
File generatedClassesDir = new File(generatedClasses);
if(generatedClassesDir.exists() && generatedClassesDir.isDirectory())
{
delete(generatedClassesDir, new FileFilter()
{
public boolean accept(File f)
{
return f.isDirectory() || f.getName().endsWith(".java");
}
});
}
}
}
static void delete(File dir, FileFilter filter)
{
File[] files = dir.listFiles(filter);
for(int i=0; i<files.length; i++)
{
File f = files[i];
if(f.isDirectory())
delete(f, filter);
else
f.delete();
}
}
/**
* Take the web fragment and put it inside a copy of the web.xml.
*
* You can specify the insertion point by specifying the string in the
* insertionMarker configuration entry.
*
* If you dont specify the insertionMarker, then the fragment will be
* inserted at the end of the file just before the </webapp>
*
* @throws Exception
*/
public void mergeWebXml() throws Exception
{
if (mergeFragment)
{
// open the src web.xml
File webXml = getWebXmlFile();
if (!webXml.exists())
{
getLog().info(webXml.toString() + " does not exist, cannot merge with generated fragment");
return;
}
File fragmentWebXml = new File(webXmlFragment);
if (!fragmentWebXml.exists())
{
getLog().info("No fragment web.xml file generated");
}
File mergedWebXml = new File(fragmentWebXml.getParentFile(),
"web.xml");
BufferedReader webXmlReader = new BufferedReader(new FileReader(
webXml));
PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(
mergedWebXml));
// read up to the insertion marker or the </webapp> if there is no
// marker
boolean atInsertPoint = false;
boolean atEOF = false;
String marker = (insertionMarker == null
|| insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker);
while (!atInsertPoint && !atEOF)
{
String line = webXmlReader.readLine();
if (line == null)
atEOF = true;
else if (line.indexOf(marker) >= 0)
{
atInsertPoint = true;
}
else
{
mergedWebXmlWriter.println(line);
}
}
// put in the generated fragment
BufferedReader fragmentWebXmlReader = new BufferedReader(
new FileReader(fragmentWebXml));
IO.copy(fragmentWebXmlReader, mergedWebXmlWriter);
// if we inserted just before the </web-app>, put it back in
if (marker.equals(END_OF_WEBAPP))
mergedWebXmlWriter.println(END_OF_WEBAPP);
// copy in the rest of the original web.xml file
IO.copy(webXmlReader, mergedWebXmlWriter);
webXmlReader.close();
mergedWebXmlWriter.close();
fragmentWebXmlReader.close();
}
}
private void prepare() throws Exception
{
// For some reason JspC doesn't like it if the dir doesn't
// already exist and refuses to create the web.xml fragment
File generatedSourceDirectoryFile = new File(generatedClasses);
if (!generatedSourceDirectoryFile.exists())
generatedSourceDirectoryFile.mkdirs();
}
/**
* Set up the execution classpath for Jasper.
*
* Put everything in the classesDirectory and all of the dependencies on the
* classpath.
*
* @returns a list of the urls of the dependencies
* @throws Exception
*/
private List<URL> setUpWebAppClassPath() throws Exception
{
//add any classes from the webapp
List<URL> urls = new ArrayList<URL>();
String classesDir = classesDirectory.getCanonicalPath();
classesDir = classesDir + (classesDir.endsWith(File.pathSeparator) ? "" : File.separator);
urls.add(Resource.toURL(new File(classesDir)));
if (getLog().isDebugEnabled())
getLog().debug("Adding to classpath classes dir: " + classesDir);
//add the dependencies of the webapp (which will form WEB-INF/lib)
for (Iterator<Artifact> iter = project.getArtifacts().iterator(); iter.hasNext();)
{
Artifact artifact = (Artifact)iter.next();
// Include runtime and compile time libraries
if (!Artifact.SCOPE_TEST.equals(artifact.getScope()) && !Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
{
String filePath = artifact.getFile().getCanonicalPath();
if (getLog().isDebugEnabled())
getLog().debug("Adding to classpath dependency file: " + filePath);
urls.add(Resource.toURL(artifact.getFile()));
}
}
return urls;
}
private String setUpSysClassPath () throws Exception
{
StringBuffer buff = new StringBuffer();
//Put each of the plugin's artifacts onto the system classpath for jspc
for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
{
Artifact pluginArtifact = iter.next();
if ("jar".equalsIgnoreCase(pluginArtifact.getType()))
{
if (getLog().isDebugEnabled()) { getLog().debug("Adding plugin artifact "+pluginArtifact);}
buff.append(pluginArtifact.getFile().getAbsolutePath());
if (iter.hasNext())
buff.append(File.pathSeparator);
}
}
if (useProvidedScope)
{
for ( Iterator<Artifact> iter = projectArtifacts.iterator(); iter.hasNext(); )
{
Artifact artifact = iter.next();
if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
{
//test to see if the provided artifact was amongst the plugin artifacts
String path = artifact.getFile().getAbsolutePath();
if (! buff.toString().contains(path))
{
if (buff.length() != 0)
buff.append(File.pathSeparator);
buff.append(path);
if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);}
}
else
{
if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);}
}
}
}
}
return buff.toString();
}
/**
* Glassfish jsp requires that we set up the list of system jars that have
* tlds in them.
*
* This method is a little fragile, as it relies on knowing that the jstl jars
* are the only ones in the system path that contain tlds.
* @return
* @throws Exception
*/
private List<URL> getSystemJarsWithTlds() throws Exception
{
final List<URL> list = new ArrayList<URL>();
List<URI> artifactUris = new ArrayList<URI>();
Pattern pattern = Pattern.compile(tldJarNamePatterns);
for (Iterator<Artifact> iter = pluginArtifacts.iterator(); iter.hasNext(); )
{
Artifact pluginArtifact = iter.next();
artifactUris.add(Resource.newResource(pluginArtifact.getFile()).getURI());
}
PatternMatcher matcher = new PatternMatcher()
{
public void matched(URI uri) throws Exception
{
//uri of system artifact matches pattern defining list of jars known to contain tlds
list.add(uri.toURL());
}
};
matcher.match(pattern, artifactUris.toArray(new URI[artifactUris.size()]), false);
return list;
}
private File getWebXmlFile ()
throws IOException
{
File file = null;
File baseDir = project.getBasedir().getCanonicalFile();
File defaultWebAppSrcDir = new File (baseDir, "src/main/webapp").getCanonicalFile();
File webAppSrcDir = new File (webAppSourceDirectory).getCanonicalFile();
File defaultWebXml = new File (defaultWebAppSrcDir, "web.xml").getCanonicalFile();
//If the web.xml has been changed from the default, try that
File webXmlFile = new File (webXml).getCanonicalFile();
if (webXmlFile.compareTo(defaultWebXml) != 0)
{
file = new File (webXml);
return file;
}
//If the web app src directory has not been changed from the default, use whatever
//is set for the web.xml location
file = new File (webAppSrcDir, "web.xml");
return file;
}
}