blob: e21dc94f0b324a3655db955652d17dc7528dce2a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sling.maven.jspc;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.commons.logging.impl.LogFactoryImpl;
import org.apache.commons.logging.impl.SimpleLog;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.sling.commons.classloader.ClassLoaderWriter;
import org.apache.sling.commons.compiler.JavaCompiler;
import org.apache.sling.commons.compiler.impl.EclipseJavaCompiler;
import org.apache.sling.scripting.jsp.jasper.Constants;
import org.apache.sling.scripting.jsp.jasper.IOProvider;
import org.apache.sling.scripting.jsp.jasper.JasperException;
import org.apache.sling.scripting.jsp.jasper.JspCompilationContext;
import org.apache.sling.scripting.jsp.jasper.Options;
import org.apache.sling.scripting.jsp.jasper.compiler.JspConfig;
import org.apache.sling.scripting.jsp.jasper.compiler.JspRuntimeContext;
import org.apache.sling.scripting.jsp.jasper.compiler.TagPluginManager;
import org.apache.sling.scripting.jsp.jasper.compiler.TldLocationsCache;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.StringUtils;
/**
* The <code>JspcMojo</code> is implements the Sling Maven JspC goal
* <code>jspc</code> compiling JSP into the target and creating a component
* descriptor for Declarative Services to use the JSP with the help of the
* appropriate adapter as component.
*/
@Mojo(name = "jspc", defaultPhase = LifecyclePhase.COMPILE, requiresDependencyResolution = ResolutionScope.COMPILE)
public class JspcMojo extends AbstractMojo implements Options {
/**
* The Maven project.
*/
@Parameter( defaultValue = "${project}", readonly = true )
private MavenProject project;
/**
* Location of the JSP source file.
*/
@Parameter( property = "jspc.sourceDirectory", defaultValue = "${project.build.scriptSourceDirectory}")
private File sourceDirectory;
/**
* List of alternative resource directories used during compiling.
*/
@Parameter
private File[] resourceDirectories = new File[0];
/**
* Target directory for the compiled JSP classes.
*/
@Parameter ( property = "jspc.outputDirectory", defaultValue = "${project.build.outputDirectory}")
private String outputDirectory;
@Parameter ( property = "jspc.jasper.classdebuginfo", defaultValue = "true")
private boolean jasperClassDebugInfo;
@Parameter ( property = "jspc.jasper.enablePooling", defaultValue = "true")
private boolean jasperEnablePooling;
@Parameter ( property = "jspc.jasper.ieClassId", defaultValue = "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93")
private String jasperIeClassId;
@Parameter ( property = "jspc.jasper.genStringAsCharArray", defaultValue = "false")
private boolean jasperGenStringAsCharArray;
@Parameter ( property = "jspc.jasper.keepgenerated", defaultValue = "true")
private boolean jasperKeepGenerated;
@Parameter ( property = "jspc.jasper.mappedfile", defaultValue = "true")
private boolean jasperMappedFile;
@Parameter ( property = "jspc.jasper.trimSpaces", defaultValue = "false")
private boolean jasperTrimSpaces;
@Parameter ( property = "jspc.jasper.suppressSmap", defaultValue = "false")
private boolean jasperSuppressSmap;
@Parameter ( property = "jspc.failOnError", defaultValue = "true")
private boolean failOnError;
@Parameter ( property = "jspc.showSuccess", defaultValue = "false")
private boolean showSuccess;
/**
* The Target Virtual Machine Version to generate class files for.
*/
@Parameter ( property = "jspc.compilerTargetVM", defaultValue = "1.8")
private String compilerTargetVM;
/**
* The Compiler Source Version of the Java source generated from the JSP files before compiling into classes.
*/
@Parameter ( property = "jspc.compilerSourceVM", defaultValue = "1.8")
private String compilerSourceVM;
/**
* Prints a compilation report by listing all the packages and dependencies that were used during processing the JSPs.
*/
@Parameter ( property = "jspc.printCompilationReport", defaultValue = "false")
private boolean printCompilationReport;
/**
* Comma separated list of extensions of files to be compiled by the plugin.
* @deprecated Use the {@link #includes} filter instead.
*/
@Deprecated
@Parameter ( property = "jspc.jspFileExtensions", defaultValue = "jsp,jspx")
private String jspFileExtensions;
/**
* When defined, this will set the value for the {@link org.apache.sling.scripting.jsp.jasper.Constants#JSP_PACKAGE_NAME_PROPERTY_NAME}
* system property of the Jasper compiler, which defines the prefix package under which compiled JSPs will be generated.
*/
@Parameter(property = "jspc.servletPackage")
private String servletPackage;
/**
* Included JSPs, defaults to <code>"**&#47;*.jsp"</code>
*/
@Parameter
private String[] includes;
/**
* Excluded JSPs, empty by default
*/
@Parameter
private String[] excludes;
private String uriSourceRoot;
private List<String> pages = new ArrayList<String>();
private JspCServletContext context;
private JspRuntimeContext rctxt;
private TrackingClassLoader loader;
private List<Artifact> jspcCompileArtifacts;
/**
* Cache for the TLD locations
*/
private TldLocationsCache tldLocationsCache;
private JspConfig jspConfig;
private TagPluginManager tagPluginManager;
/*
* (non-Javadoc)
*
* @see org.apache.maven.plugin.Mojo#execute()
*/
public void execute() throws MojoExecutionException {
try {
uriSourceRoot = sourceDirectory.getCanonicalPath();
} catch (Exception e) {
uriSourceRoot = sourceDirectory.getAbsolutePath();
}
// ensure output directory
File outputDirectoryFile = new File(outputDirectory);
if (!outputDirectoryFile.isDirectory()) {
if (outputDirectoryFile.exists()) {
throw new MojoExecutionException(outputDirectory
+ " exists but is not a directory");
}
if (!outputDirectoryFile.mkdirs()) {
throw new MojoExecutionException(
"Cannot create output directory " + outputDirectory);
}
}
// have the files compiled
String previousJasperPackageName = null;
String oldValue = System.getProperty(LogFactoryImpl.LOG_PROPERTY);
try {
// ensure the JSP Compiler does not try to use Log4J
System.setProperty(LogFactoryImpl.LOG_PROPERTY, SimpleLog.class.getName());
previousJasperPackageName =
System.setProperty(Constants.JSP_PACKAGE_NAME_PROPERTY_NAME, (servletPackage != null ? servletPackage : ""));
executeInternal();
if (printCompilationReport) {
printCompilationReport();
}
} catch (JasperException je) {
getLog().error("Compilation Failure", je);
throw new MojoExecutionException(je.getMessage(), je);
} finally {
if (oldValue == null) {
System.clearProperty(LogFactoryImpl.LOG_PROPERTY);
} else {
System.setProperty(LogFactoryImpl.LOG_PROPERTY, oldValue);
}
if (previousJasperPackageName == null) {
System.clearProperty(Constants.JSP_PACKAGE_NAME_PROPERTY_NAME);
} else {
System.setProperty(Constants.JSP_PACKAGE_NAME_PROPERTY_NAME, previousJasperPackageName);
}
}
project.addCompileSourceRoot(outputDirectory);
}
/**
* Locate all jsp files in the webapp. Used if no explicit jsps are
* specified.
*/
private void scanFiles(File base) {
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir(base);
scanner.setIncludes(includes);
scanner.setExcludes(excludes);
scanner.scan();
Collections.addAll(pages, scanner.getIncludedFiles());
}
/**
* Executes the compilation.
*
* @throws JasperException If an error occurs
*/
private void executeInternal() throws JasperException {
if (getLog().isDebugEnabled()) {
getLog().debug("execute() starting for " + pages.size() + " pages.");
}
try {
if (context == null) {
initServletContext();
}
if (includes == null) {
includes = new String[]{ "**/*.jsp" };
}
// No explicit pages, we'll process all .jsp in the webapp
if (pages.size() == 0) {
scanFiles(sourceDirectory);
}
File uriRootF = new File(uriSourceRoot);
if (!uriRootF.exists() || !uriRootF.isDirectory()) {
throw new JasperException("The source location '"
+ uriSourceRoot + "' must be an existing directory");
}
pages.stream().parallel().forEach(nextjsp -> {
File fjsp = new File(nextjsp);
if (!fjsp.isAbsolute()) {
fjsp = new File(uriRootF, nextjsp);
}
if (!fjsp.exists()) {
if (getLog().isWarnEnabled()) {
getLog().warn("JSP file " + fjsp + " does not exist");
}
} else {
String s = fjsp.getAbsolutePath();
if (s.startsWith(uriSourceRoot)) {
nextjsp = s.substring(uriSourceRoot.length());
}
if (nextjsp.startsWith("." + File.separatorChar)) {
nextjsp = nextjsp.substring(2);
}
try {
processFile(nextjsp);
} catch (JasperException e) {
throw new RuntimeException(e);
}
}
});
} catch (JasperException je) {
Throwable rootCause = je;
while (rootCause instanceof JasperException
&& ((JasperException) rootCause).getRootCause() != null) {
rootCause = ((JasperException) rootCause).getRootCause();
}
if (rootCause != je) {
rootCause.printStackTrace();
}
throw je;
} catch (/* IO */Exception ioe) {
throw new JasperException(ioe);
}
}
private void processFile(final String file) throws JasperException {
try {
final String jspUri = file.replace('\\', '/');
final JspCompilationContext clctxt = new JspCompilationContext(jspUri, false, this, context, rctxt, false);
JasperException error = clctxt.compile();
if (error != null) {
throw error;
}
if (showSuccess) {
getLog().info("Built File: " + file);
}
} catch (final JasperException je) {
Throwable rootCause = je;
while (rootCause instanceof JasperException
&& ((JasperException) rootCause).getRootCause() != null) {
rootCause = ((JasperException) rootCause).getRootCause();
}
if (rootCause != je) {
getLog().error("General problem compiling " + file, rootCause);
}
// Bugzilla 35114.
if (failOnError) {
throw je;
}
// just log otherwise
getLog().error(je.getMessage());
} catch (Exception e) {
throw new JasperException(e);
}
}
// ---------- Additional Settings ------------------------------------------
private void initServletContext() throws IOException, DependencyResolutionRequiredException {
if (loader == null) {
initClassLoader();
}
context = new JspCServletContext(getLog(), new URL("file:" + uriSourceRoot.replace('\\', '/') + '/'));
for (File resourceDir: resourceDirectories) {
String root = resourceDir.getCanonicalPath().replace('\\', '/');
URL altUrl = new URL("file:" + root + "/");
context.addAlternativeBaseURL(altUrl);
}
tldLocationsCache = new JspCTldLocationsCache(context, true, loader);
JavaCompiler compiler = new EclipseJavaCompiler();
ClassLoaderWriter writer = new JspCClassLoaderWriter(loader, new File(outputDirectory));
IOProvider ioProvider = new JspCIOProvider(loader, compiler, writer);
rctxt = new JspRuntimeContext(context, this, ioProvider);
jspConfig = new JspConfig(context);
tagPluginManager = new TagPluginManager(context);
}
/**
* Initializes the classloader as/if needed for the given compilation
* context.
*
* @throws IOException If an error occurs
*/
private void initClassLoader() throws IOException,
DependencyResolutionRequiredException {
List<URL> classPath = new ArrayList<URL>();
// add output directory to classpath
final String targetDirectory = project.getBuild().getOutputDirectory();
classPath.add(new File(targetDirectory).toURI().toURL());
// add artifacts from project
Set<Artifact> artifacts = project.getArtifacts();
jspcCompileArtifacts = new ArrayList<Artifact>(artifacts.size());
for (Artifact a: artifacts) {
final String scope = a.getScope();
if ("provided".equals(scope) || "runtime".equals(scope) || "compile".equals(scope)) {
// we need to exclude the javax.servlet.jsp API, otherwise the taglib parser causes problems (see note below)
if (containsProblematicPackage(a.getFile())) {
continue;
}
classPath.add(a.getFile().toURI().toURL());
jspcCompileArtifacts.add(a);
}
}
if (getLog().isDebugEnabled()) {
getLog().debug("Compiler classpath:");
for (URL u: classPath) {
getLog().debug(" " + u);
}
}
// this is dangerous to use this classloader as parent as the compilation will depend on the classes provided
// in the plugin dependencies. but if we omit this, we get errors by not being able to load the TagExtraInfo classes.
// this is because this plugin uses classes from the javax.servlet.jsp that are also loaded via the TLDs.
loader = new TrackingClassLoader(classPath.toArray(new URL[classPath.size()]), this.getClass().getClassLoader());
}
/**
* Checks if the given jar file contains a problematic java API that should be excluded from the classloader.
* @param file the file to check
* @return {@code true} if it contains a problematic package
* @throws IOException if an error occurrs.
*/
private boolean containsProblematicPackage(File file) throws IOException {
JarFile jar = new JarFile(file);
boolean isJSPApi = jar.getEntry("/javax/servlet/jsp/JspPage.class") != null;
jar.close();
return isJSPApi;
}
/**
* Prints the dependency report.
*/
private void printCompilationReport() {
if (loader == null) {
return;
}
// first scan all the dependencies for their classes
Map<String, Set<String>> artifactsByPackage = new HashMap<String, Set<String>>();
Set<String> usedDependencies = new HashSet<String>();
for (Artifact a: jspcCompileArtifacts) {
scanArtifactPackages(artifactsByPackage, a);
usedDependencies.add(a.getId());
}
// create the report
StringBuilder report = new StringBuilder("JSP compilation report:\n\n");
List<String> packages = new ArrayList<String>(loader.getPackageNames());
int pad = 10;
for (String packageName: artifactsByPackage.keySet()) {
pad = Math.max(pad, packageName.length());
}
pad += 2;
report.append(StringUtils.rightPad("Package", pad)).append("Dependency");
report.append("\n---------------------------------------------------------------\n");
Collections.sort(packages);
for (String packageName: packages) {
report.append(StringUtils.rightPad(packageName, pad));
Set<String> a = artifactsByPackage.get(packageName);
if (a == null || a.isEmpty()) {
report.append("n/a");
} else {
StringBuilder ids = new StringBuilder();
for (String id: a) {
usedDependencies.remove(id);
if (ids.length() > 0) {
ids.append(", ");
}
ids.append(id);
}
report.append(ids);
}
report.append("\n");
}
// print the unused dependencies
report.append("\n");
report.append(usedDependencies.size()).append(" dependencies not used by JSPs:\n");
if (!usedDependencies.isEmpty()) {
report.append("---------------------------------------------------------------\n");
for (String id: usedDependencies) {
report.append(id).append("\n");
}
}
// create the package list that are double defined
int doubleDefined = 0;
StringBuilder msg = new StringBuilder();
packages = new ArrayList<String>(artifactsByPackage.keySet());
Collections.sort(packages);
for (String packageName: packages) {
Set<String> a = artifactsByPackage.get(packageName);
if (a != null && a.size() > 1) {
doubleDefined++;
msg.append(StringUtils.rightPad(packageName, pad));
msg.append(StringUtils.join(a.iterator(), ", ")).append("\n");
}
}
report.append("\n");
report.append(doubleDefined).append(" packages are multiply defined by dependencies:\n");
if (doubleDefined > 0) {
report.append("---------------------------------------------------------------\n");
report.append(msg);
}
getLog().info(report);
}
/**
* Scans the given artifact for classes and add their packages to the given map.
* @param artifactsByPackage the package to artifact lookup map
* @param a the artifact to scan
*/
private void scanArtifactPackages(Map<String, Set<String>> artifactsByPackage, Artifact a) {
try {
JarFile jar = new JarFile(a.getFile());
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry e = entries.nextElement();
if (e.isDirectory()) {
continue;
}
String path = e.getName();
if (path.endsWith(".class")) {
path = StringUtils.chomp(path, "/");
if (path.charAt(0) == '/') {
path = path.substring(1);
}
String packageName = path.replaceAll("/", ".");
Set<String> artifacts = artifactsByPackage.get(packageName);
if (artifacts == null) {
artifacts = new HashSet<String>();
artifactsByPackage.put(packageName, artifacts);
}
artifacts.add(a.getId());
}
}
} catch (IOException e) {
getLog().error("Error while accessing jar file: " + e.getMessage());
}
}
// ---------- Options interface --------------------------------------------
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#genStringAsCharArray()
*/
public boolean genStringAsCharArray() {
return jasperGenStringAsCharArray;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getClassDebugInfo()
*/
public boolean getClassDebugInfo() {
return jasperClassDebugInfo;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getCompiler()
*/
public String getCompiler() {
// use JDTCompiler, which is the default
return null;
}
public String getCompilerClassName() {
// use JDTCompiler, which is the default
return null;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getCompilerSourceVM()
*/
public String getCompilerSourceVM() {
return compilerSourceVM;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getCompilerTargetVM()
*/
public String getCompilerTargetVM() {
return compilerTargetVM;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getErrorOnUseBeanInvalidClassAttribute()
*/
public boolean getErrorOnUseBeanInvalidClassAttribute() {
// not configurable
return true;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getFork()
*/
public boolean getFork() {
// certainly don't fork (not required anyway)
return false;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getIeClassId()
*/
public String getIeClassId() {
return jasperIeClassId;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getJavaEncoding()
*/
public String getJavaEncoding() {
return "UTF-8";
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getJspConfig()
*/
public JspConfig getJspConfig() {
return jspConfig;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getKeepGenerated()
*/
public boolean getKeepGenerated() {
return jasperKeepGenerated;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getMappedFile()
*/
public boolean getMappedFile() {
return jasperMappedFile;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getScratchDir()
*/
public String getScratchDir() {
return outputDirectory;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getSendErrorToClient()
*/
public boolean getSendErrorToClient() {
// certainly output any problems
return true;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getTagPluginManager()
*/
public TagPluginManager getTagPluginManager() {
return tagPluginManager;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getTldLocationsCache()
*/
public TldLocationsCache getTldLocationsCache() {
return tldLocationsCache;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#getTrimSpaces()
*/
public boolean getTrimSpaces() {
return jasperTrimSpaces;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#isPoolingEnabled()
*/
public boolean isPoolingEnabled() {
return jasperEnablePooling;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#isSmapDumped()
*/
public boolean isSmapDumped() {
// always include the SMAP (optionally, limit to if debugging)
return true;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#isSmapSuppressed()
*/
public boolean isSmapSuppressed() {
// require SMAP
return jasperSuppressSmap;
}
/*
* (non-Javadoc)
*
* @see org.apache.jasper.Options#isXpoweredBy()
*/
public boolean isXpoweredBy() {
// no XpoweredBy setting please
return false;
}
public boolean getDisplaySourceFragment() {
// Display the source fragment on errors for maven compilation
return true;
}
}