blob: 1daf3ef77296074f374e85f0ab2f25a07643b15d [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.myfaces.extensions.scripting.loaders.java.compiler;
import org.apache.myfaces.extensions.scripting.api.CompilationException;
import org.apache.myfaces.extensions.scripting.api.CompilationResult;
import org.apache.myfaces.extensions.scripting.api.CompilerConst;
import org.apache.myfaces.extensions.scripting.api.ScriptingConst;
import org.apache.myfaces.extensions.scripting.core.util.ClassLoaderUtils;
import org.apache.myfaces.extensions.scripting.core.util.ClassUtils;
import org.apache.myfaces.extensions.scripting.core.util.FileUtils;
import org.apache.myfaces.extensions.scripting.core.util.WeavingContext;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>A compiler implementation that utilizes some internal classes that enable you to
* compile Java source code using the javac compiler being provided by your JDK. However,
* note that this only works if you're using a Sun JDK up to the version 1.5 (as of Java 6
* you should use the JSR-199 API).</p>
* <p/>
* <p>This class loads some internal classes from $JAVA_HOME$/lib/tools.jar so be sure to
* either include this JAR file in your classpath at startup or set the JAVA_HOME property
* accordingly so that it points to a valid JDK home directory (it doesn't work if you're
* just using a JRE!)</p>
*/
public class JavacCompiler implements org.apache.myfaces.extensions.scripting.api.Compiler {
/**
* The logger instance for this class.
*/
private static final Logger _logger = Logger.getLogger(JavacCompiler.class.getName());
/**
* The class name of the javac compiler. Note that this class
* is only available if you're using a Sun JDK.
*/
private static final String JAVAC_MAIN = "com.sun.tools.javac.Main";
/**
* The class reference to the internal Javac compiler.
*/
private Class _compilerClass;
// ------------------------------------------ Constructors
/**
* <p>Creates a new Javac compiler by searching for the required JAR file '$JAVA_HOME$/lib/tools.jar'
* automatically. Note that the user has to specify the JAVA_HOME property in this case.</p>
*/
public JavacCompiler() {
this(null);
}
/**
* <p>Creates a new Javac compiler by searching for internal classes in the given JAR file.</p>
*
* @param toolsJar the location of the JAR file '$JAVA_HOME$/lib/tools.jar' or <code>null</code>
* if you want it to be searched for automatically
*/
public JavacCompiler(URL toolsJar) {
ClassLoader classLoader;
try {
classLoader = createJavacAwareClassLoader(toolsJar);
} catch (MalformedURLException ex) {
throw new IllegalStateException("An error occured while trying to load the Javac compiler class.", ex);
}
try {
this._compilerClass = classLoader.loadClass(JAVAC_MAIN);
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("The Javac compiler class '" + JAVAC_MAIN + "' couldn't be found even though" +
"the required JAR file '$JAVA_HOME$/lib/tools.jar' has been put on the classpath. Are you sure that " +
"you're using a valid Sun JDK?");
}
}
// ------------------------------------------ Compiler methods
/**
* <p>Compiles the given file and creates an according class file in the given target path.</p>
*
* @param sourcePath the path to the source directory
* @param targetPath the path to the target directory
* @return the compilation result, i.e. as of now only the compiler output
*/
public CompilationResult compile(File sourcePath, File targetPath, ClassLoader loader) throws CompilationException {
FileUtils.assertPath(targetPath);
try {
StringWriter compilerOutput = new StringWriter();
// Invoke the Javac compiler
Method compile = _compilerClass.getMethod("compile", new Class[]{String[].class, PrintWriter.class});
Object[] compilerArguments = new Object[]{buildCompilerArgumentsWhitelisted(sourcePath, targetPath, loader), new PrintWriter(compilerOutput)};
logCommandLine(compilerArguments);
Integer returnCode = (Integer) compile.invoke(null, compilerArguments);
CompilationResult result = new CompilationResult(compilerOutput.toString());
if (returnCode == null || returnCode.intValue() != 0) {
result.registerError(new CompilationResult.CompilationMessage(-1,
"Executing the javac compiler failed. The return code is '" + returnCode + "'." + compilerOutput.toString()));
}
WeavingContext.setCompilationResult(ScriptingConst.ENGINE_TYPE_JSF_JAVA, result);
return result;
} catch (NoSuchMethodException ex) {
throw new CompilationException("The Javac compiler class '" + _compilerClass + "' doesn't provide the method " +
"compile(String, PrintWriter). Are you sure that you're using a valid Sun JDK?", ex);
} catch (InvocationTargetException ex) {
throw new CompilationException("An error occured while invoking the compile(String, PrintWriter) method of the " +
"Javac compiler class '" + _compilerClass + "'. Are you sure that you're using a valid Sun JDK?", ex);
} catch (IllegalAccessException ex) {
throw new CompilationException("An error occured while invoking the compile(String, PrintWriter) method of the " +
"Javac compiler class '" + _compilerClass + "'. Are you sure that you're using a valid Sun JDK?", ex);
}
}
/**
* <p>Compiles the given file and creates an according class file in the given target path.</p>
*
* @param sourcePath the path to the source directory
* @param targetPath the path to the target directory
* @param file the relative file name of the class you want to compile
* @return the compilation result, i.e. as of now only the compiler output
*/
public CompilationResult compile(File sourcePath, File targetPath, File file, ClassLoader loader) throws CompilationException {
// The destination directory must already exist as javac will not create the destination directory.
FileUtils.assertPath(targetPath);
try {
StringWriter compilerOutput = new StringWriter();
// Invoke the Javac compiler
Method compile = _compilerClass.getMethod("compile", new Class[]{String[].class, PrintWriter.class});
if (!targetPath.exists()) {
if (!targetPath.mkdirs()) {
throw new IllegalStateException("It wasn't possible to create the target " +
"directory for the compiler ['" + targetPath.getAbsolutePath() + "'].");
}
}
//TODO make a whitelist check here
Object[] compilerArguments = new Object[]{buildCompilerArguments(sourcePath, targetPath, file.getAbsolutePath(), loader), new PrintWriter(compilerOutput)};
logCommandLine(compilerArguments);
Integer returnCode = (Integer) compile.invoke(null,
compilerArguments);
CompilationResult result = new CompilationResult(compilerOutput.toString());
if (returnCode == null || returnCode != 0) {
result.registerError(new CompilationResult.CompilationMessage(-1,
"Executing the javac compiler failed. The return code is '" + returnCode + "'." + compilerOutput.toString()));
}
WeavingContext.setCompilationResult(ScriptingConst.ENGINE_TYPE_JSF_JAVA, result);
return result;
} catch (NoSuchMethodException ex) {
throw new IllegalStateException("The Javac compiler class '" + _compilerClass + "' doesn't provide the method " +
"compile(String, PrintWriter). Are you sure that you're using a valid Sun JDK?", ex);
} catch (InvocationTargetException ex) {
throw new IllegalStateException("An error occured while invoking the compile(String, PrintWriter) method of the " +
"Javac compiler class '" + _compilerClass + "'. Are you sure that you're using a valid Sun JDK?", ex);
} catch (IllegalAccessException ex) {
throw new IllegalStateException("An error occured while invoking the compile(String, PrintWriter) method of the " +
"Javac compiler class '" + _compilerClass + "'. Are you sure that you're using a valid Sun JDK?", ex);
}
}
private void logCommandLine(Object[] compilerArguments) {
if (_logger.isLoggable(Level.FINE)) {
StringBuilder commandLine = new StringBuilder();
commandLine.append("javac ");
for (String compilerArgument : (String[]) compilerArguments[0]) {
commandLine.append(compilerArgument);
commandLine.append(" ");
}
_logger.log(Level.FINE, commandLine.toString());
}
if (_logger.isLoggable(Level.INFO)) {
_logger.info("[EXT-SCRIPTING] compiling java");
}
}
// ------------------------------------------ Utility methods
/**
* <p/>
* Creates the arguments for the compiler, i.e. builds up an array of arguments
* that one would pass to the javac compiler to compile a full path instead of a single file
*
* @param sourcePath the path to the source directory
* @param targetPath the path to the target directory
* @return an array of arguments that you have to pass to the Javac compiler
*/
protected String[] buildCompilerArgumentsWhitelisted(File sourcePath, File targetPath, ClassLoader loader) {
List<File> sourceFiles = FileUtils.fetchSourceFiles(WeavingContext.getConfiguration().getWhitelistedSourceDirs(ScriptingConst.ENGINE_TYPE_JSF_JAVA), "*.java");
List arguments = getDefaultArguments(sourcePath, targetPath, loader);
// Append the source file that is to be compiled. Note that the user specifies only a relative file location.
for (File sourceFile : sourceFiles) {
arguments.add(sourceFile.getAbsolutePath());
}
return (String[]) argumentsToArray(arguments);
}
private Object[] argumentsToArray(List arguments) {
return arguments.toArray(new String[arguments.size()]);
}
/**
* <p/>
* Creates the arguments for the compiler, i.e. builds up an array of arguments
* that one would pass to the javac compiler to compile a full path instead of a single file
*
* @param sourcePath the path to the source directory
* @param targetPath the path to the target directory
* @return an array of arguments that you have to pass to the Javac compiler
*/
protected String[] buildCompilerArguments(File sourcePath, File targetPath, ClassLoader loader) {
List<File> sourceFiles = FileUtils.fetchSourceFiles(sourcePath, "*.java");
List arguments = getDefaultArguments(sourcePath, targetPath, loader);
// Append the source file that is to be compiled. Note that the user specifies only a relative file location.
for (File sourceFile : sourceFiles) {
arguments.add(sourceFile.getAbsolutePath());
}
return (String[]) argumentsToArray(arguments);
}
/**
* <p>Creates the arguments for the compiler, i.e. it builds an array of arguments that one would pass to
* the Javac compiler on the command line.</p>
*
* @param sourcePath the path to the source directory
* @param targetPath the path to the target directory
* @param loader the classpath holder for the compiler
* @param file the relative file name of the class you want to compile
* @return an array of arguments that you have to pass to the Javac compiler
*/
protected String[] buildCompilerArguments(File sourcePath, File targetPath, String file, ClassLoader loader) {
List arguments = getDefaultArguments(sourcePath, targetPath, loader);
// Append the source file that is to be compiled. Note that the user specifies only a relative file location.
arguments.add(new File(sourcePath, file).getAbsolutePath());
return (String[]) argumentsToArray(arguments);
}
/**
* <p>
* Determination of the default arguments
* which have to be the same over all
* different compilation strategies
* </p>
*
* @param sourcePath the path to the source directory
* @param targetPath the path to the target directory
* @param loader the classloader holding the classpath
* @return
*/
private List getDefaultArguments(File sourcePath, File targetPath, ClassLoader loader) {
List arguments = new ArrayList();
// Set both the source code path to search for class or interface
// definitions and the destination directory for class files.
arguments.add(CompilerConst.JC_SOURCEPATH);
arguments.add(sourcePath.getAbsolutePath());
arguments.add(CompilerConst.JC_TARGET_PATH);
arguments.add(targetPath.getAbsolutePath());
arguments.add(CompilerConst.JC_CLASSPATH);
arguments.add(ClassLoaderUtils.buildClasspath(loader));
// Enable verbose output.
arguments.add(CompilerConst.JC_VERBOSE);
// Generate all debugging information, including local variables.
arguments.add(CompilerConst.JC_DEBUG);
return arguments;
}
/**
* <p>Returns a possibly newly created classloader that you can use in order to load the
* Javac compiler class. Usually the user would have to put the JAR file
* '$JAVA_HOME$/lib/tools.jar' on the classpath but this method recognizes this on its own
* and loads the JAR file if necessary. However, it's not guaranteed that the Javac compiler
* class is available (e.g. if one is providing a wrong tools.jar file that doesn't contain
* the required classes).</p>
*
* @param toolsJar the location of the JAR file '$JAVA_HOME$/lib/tools.jar' or <code>null</code>
* if you want it to be searched for automatically
* @return a classloader that you can use in order to load the Javac compiler class
* @throws MalformedURLException if an error occurred while constructing the URL
*/
private static ClassLoader createJavacAwareClassLoader(URL toolsJar) throws MalformedURLException {
// If the user has already included the tools.jar in the classpath we don't have
// to create a custom class loader as the class is already available.
if (ClassUtils.isPresent(JAVAC_MAIN)) {
if (_logger.isLoggable(Level.FINE)) {
_logger.log(Level.FINE, "Seemingly the required JAR file '$JAVA_HOME$/lib/tools.jar' has already been "
+ "put on the classpath as the class '" + JAVAC_MAIN + "' is present. So there's no "
+ "need to create a custom classloader for the Javac compiler.");
}
return ClassUtils.getContextClassLoader();
} else {
// The compiler isn't available in the current classpath, but the user could have specified the tools.jar file.
if (toolsJar == null) {
String javaHome = System.getProperty("java.home");
if (javaHome.toLowerCase(Locale.getDefault()).endsWith(File.separator + "jre")) {
// Note that even if the user has installed a valid JDK the $JAVA_HOME$ property might reference
// the JRE, e.g. '/usr/lib/jvm/java-6-sun-1.6.0.16/jre'. However, in this case we just have to
// remove the last four characters (i.e. the '/jre').
javaHome = javaHome.substring(0, javaHome.length() - 4);
}
// If the user hasn't specified the URL to the tools.jar file, we'll try to find it on our own.
File toolsJarFile = new File(javaHome, "lib" + File.separatorChar + "tools.jar");
if (toolsJarFile.exists()) {
if (_logger.isLoggable(Level.FINE)) {
_logger.log(Level.FINE,
"The required JAR file '$JAVA_HOME$/lib/tools.jar' has been found ['" + toolsJarFile.getAbsolutePath()
+ "']. A custom URL classloader will be created for the Javac compiler.");
}
return new URLClassLoader(
new URL[]{toolsJarFile.toURI().toURL()}, ClassUtils.getContextClassLoader());
} else {
throw new IllegalStateException("The Javac compiler class '" + JAVAC_MAIN + "' and the required JAR file " +
"'$JAVA_HOME$/lib/tools.jar' couldn't be found. Are you sure that you're using a valid Sun JDK? " +
"[$JAVA_HOME$: '" + System.getProperty("java.home") + "']");
}
} else {
if (_logger.isLoggable(Level.FINE)) {
_logger.log(Level.FINE, "The user has specified the required JAR file '$JAVA_HOME$/lib/tools.jar' ['"
+ toolsJar.toExternalForm() + "']. A custom URL classloader will be created for the Javac compiler.");
}
return new URLClassLoader(new URL[]{toolsJar}, ClassUtils.getContextClassLoader());
}
}
}
}