// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.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.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.jasper.JspC; import org.apache.jasper.servlet.JspCServletContext; import org.apache.jasper.servlet.TldScanner; 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.apache.tomcat.JarScanner; import org.apache.tomcat.util.scan.StandardJarScanner; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.StringUtils; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.resource.Resource; /** * This goal will compile jsps for a webapp so that they can be included in a * war. * <p> * At runtime, the plugin will use the jspc compiler to precompile jsps and tags. * </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> * See <a * href="https://www.eclipse.org/jetty/documentation/current/jetty-jspc-maven-plugin.html">Usage * Guide</a> for instructions on using this plugin. * </p> * @goal jspc * @phase process-classes * @requiresDependencyResolution compile+runtime * @description Runs jspc compiler to produce .java and .class files */ public class JspcMojo extends AbstractMojo { public static final String END_OF_WEBAPP = "</web-app>"; public static final String PRECOMPILED_FLAG = "org.eclipse.jetty.jsp.precompiled"; /** * JettyJspC * * Add some extra setters to standard JspC class to help configure it * for running in maven. * * TODO move all setters on the plugin onto this jspc class instead. */ public static class JettyJspC extends JspC { private boolean scanAll; public void setClassLoader (ClassLoader loader) { this.loader = loader; } public void setScanAllDirectories (boolean scanAll) { this.scanAll = scanAll; } public boolean getScanAllDirectories () { return this.scanAll; } @Override protected TldScanner newTldScanner(JspCServletContext context, boolean namespaceAware, boolean validate, boolean blockExternal) { if (context != null && context.getAttribute(JarScanner.class.getName()) == null) { StandardJarScanner jarScanner = new StandardJarScanner(); jarScanner.setScanAllDirectories(getScanAllDirectories()); context.setAttribute(JarScanner.class.getName(), jarScanner); } return super.newTldScanner(context, namespaceAware, validate, blockExternal); } } /** * 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 default-value="${project.artifacts}" * @readonly */ private Set projectArtifacts; /** * The maven project. * * @parameter default-value="${project}" * @required * @readonly */ private MavenProject project; /** * The artifacts for the plugin itself. * * @parameter default-value="${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; /** * 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 default-value="${project.build.outputDirectory}" */ private File classesDirectory; /** * Patterns of jars on the system path that contain tlds. Use | to separate each pattern. * * @parameter default-value=".*taglibs[^/]*\.jar|.*jstl[^/]*\.jar$ */ private String tldJarNamePatterns; /** * Source version - if not set defaults to jsp default (currently 1.7) * @parameter */ private String sourceVersion; /** * Target version - if not set defaults to jsp default (currently 1.7) * @parameter */ private String targetVersion; /** * * The JspC instance being used to compile the jsps. * * @parameter */ private JettyJspC jspc; /** * Whether dirs on the classpath should be scanned as well as jars. * True by default. This allows for scanning for tlds of dependent projects that * are in the reactor as unassembled jars. * * @parameter default-value=true */ private boolean scanAllDirectories; public void execute() throws MojoExecutionException, MojoFailureException { if (getLog().isDebugEnabled()) { getLog().info("webAppSourceDirectory=" + webAppSourceDirectory); getLog().info("generatedClasses=" + generatedClasses); getLog().info("webXmlFragment=" + webXmlFragment); getLog().info("webXml="+webXml); getLog().info("insertionMarker="+ (insertionMarker == null || insertionMarker.equals("") ? END_OF_WEBAPP : insertionMarker)); getLog().info("keepSources=" + keepSources); getLog().info("mergeFragment=" + mergeFragment); if (sourceVersion != null) getLog().info("sourceVersion="+sourceVersion); if (targetVersion != null) getLog().info("targetVersion="+targetVersion); } 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) Set<URL> pluginJars = getPluginJars(); Set<URL> providedJars = getProvidedScopeJars(pluginJars); //Make a classloader so provided jars will be on the classpath List<URL> sysUrls = new ArrayList<URL>(); sysUrls.addAll(providedJars); URLClassLoader sysClassLoader = new URLClassLoader((URL[])sysUrls.toArray(new URL[0]), currentClassLoader); //make a classloader with the webapp classpath URLClassLoader webAppClassLoader = new URLClassLoader((URL[]) webAppUrls.toArray(new URL[0]), sysClassLoader); 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")); } //Interpose a fake classloader as the webapp class loader. This is because the Apache JspC class //uses a TldScanner which ignores jars outside of the WEB-INF/lib path on the webapp classloader. //It will, however, look at all jars on the parents of the webapp classloader. URLClassLoader fakeWebAppClassLoader = new URLClassLoader(new URL[0], webAppClassLoader); Thread.currentThread().setContextClassLoader(fakeWebAppClassLoader); if (jspc == null) jspc = new JettyJspC(); jspc.setWebXmlFragment(webXmlFragment); jspc.setUriroot(webAppSourceDirectory); jspc.setOutputDir(generatedClasses); jspc.setClassLoader(fakeWebAppClassLoader); jspc.setScanAllDirectories(scanAllDirectories); jspc.setCompile(true); if (sourceVersion != null) jspc.setCompilerSourceVM(sourceVersion); if (targetVersion != null) jspc.setCompilerTargetVM(targetVersion); // JspC#setExtensions() does not exist, so // always set concrete list of files that will be processed. String jspFiles = getJspFiles(webAppSourceDirectory); try { if (jspFiles == null | jspFiles.equals("")) { getLog().info("No files selected to precompile"); } else { getLog().info("Compiling "+jspFiles+" from includes="+includes+" excludes="+excludes); jspc.setJspFiles(jspFiles); jspc.execute(); } } finally { 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 if unable to clean srcs */ 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); if (files != null) { for(File f: files) { 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 if unable to merge the web xml */ 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); File mergedWebXml = new File(fragmentWebXml.getParentFile(), "web.xml"); try (BufferedReader webXmlReader = new BufferedReader(new FileReader(webXml)); PrintWriter mergedWebXmlWriter = new PrintWriter(new FileWriter(mergedWebXml))) { if (!fragmentWebXml.exists()) { getLog().info("No fragment web.xml file generated"); //just copy existing web.xml to expected position IO.copy(webXmlReader, mergedWebXmlWriter); } else { // 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); } } if (atEOF && !atInsertPoint) throw new IllegalStateException("web.xml does not contain insertionMarker "+insertionMarker); //put in a context init-param to flag that the contents have been precompiled mergedWebXmlWriter.println("<context-param><param-name>"+PRECOMPILED_FLAG+"</param-name><param-value>true</param-value></context-param>"); // put in the generated fragment try (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); } } } } } 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; } /** * @return * @throws MalformedURLException */ private Set<URL> getPluginJars () throws MalformedURLException { HashSet<URL> pluginJars = new HashSet<>(); 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);} pluginJars.add(pluginArtifact.getFile().toURI().toURL()); } } return pluginJars; } /** * @param pluginJars * @return * @throws MalformedURLException */ private Set<URL> getProvidedScopeJars (Set<URL> pluginJars) throws MalformedURLException { if (!useProvidedScope) return Collections.emptySet(); HashSet<URL> providedJars = new HashSet<>(); 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 URL jar = artifact.getFile().toURI().toURL(); if (!pluginJars.contains(jar)) { providedJars.add(jar); if (getLog().isDebugEnabled()) { getLog().debug("Adding provided artifact: "+artifact);} } else { if (getLog().isDebugEnabled()) { getLog().debug("Skipping provided artifact: "+artifact);} } } } return providedJars; } 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; } }