| /* |
| * 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.compiler; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.myfaces.extensions.scripting.loader.ClassLoaderUtils; |
| import org.apache.myfaces.extensions.scripting.compiler.CompilationException; |
| |
| import java.io.File; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.lang.*; |
| 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; |
| |
| /** |
| * <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 Compiler { |
| |
| /** |
| * The logger instance for this class. |
| */ |
| private static final Log logger = LogFactory.getLog(JavacCompiler.class); |
| |
| /** |
| * 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 occurred 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 |
| * @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, String file, ClassLoader classLoader) |
| throws CompilationException { |
| return compile(sourcePath, targetPath, new File(sourcePath, file), classLoader); |
| } |
| |
| /** |
| * <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 file 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 classLoader) |
| throws CompilationException { |
| // The destination directory must already exist as javac will not create the destination directory. |
| if (!targetPath.exists()) { |
| if (!targetPath.mkdirs()) { |
| throw new IllegalStateException("It wasn't possible to create the target " + |
| "directory for the compiler ['" + targetPath.getAbsolutePath() + "']."); |
| } |
| |
| // If we've created the destination directory, we'll delete it as well once the application exits |
| targetPath.deleteOnExit(); |
| } |
| |
| try { |
| StringWriter compilerOutput = new StringWriter(); |
| |
| // Invoke the Javac compiler |
| Method compile = compilerClass.getMethod("compile", new Class[]{String[].class, PrintWriter.class}); |
| Integer returnCode = (Integer) compile.invoke(null, |
| new Object[]{buildCompilerArguments(sourcePath, targetPath, file, classLoader), |
| new PrintWriter(compilerOutput)}); |
| |
| 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 + "'.")); |
| } |
| |
| 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 occurred 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 occurred 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); |
| } |
| } |
| |
| // ------------------------------------------ Utility methods |
| |
| /** |
| * <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 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, File file, ClassLoader classLoader) { |
| List<String> arguments = new ArrayList<String>(); |
| |
| // Note that we're knowingly not specifying the sourcepath as the compiler really should compile |
| // only a single file (see 'file'). The dependent classes are available on the classpath anyway. |
| // Otherwise the compiler would also compile dependent classes, which we want to avoid! This |
| // would result in different versions of a Class file being in use (the system doesn't know that |
| // it has to update itself due to a newer version of a Class file whilst the dynamic class loader |
| // will already start using it!) |
| // arguments.add("-sourcepath"); |
| // arguments.add(sourcePath.getAbsolutePath()); |
| |
| // Set the destination / target directory for the compiled .class files. |
| arguments.add("-d"); |
| arguments.add(targetPath.getAbsolutePath()); |
| |
| // Specify the classpath of the given class loader. This enables the user to write new Java |
| // "scripts" that depend on classes that have already been loaded previously. Otherwise he |
| // wouldn't be able to use for example classes that are available in a library. |
| arguments.add("-classpath"); |
| arguments.add(ClassLoaderUtils.buildClasspath(classLoader)); |
| |
| // Enable verbose output. |
| arguments.add("-verbose"); |
| |
| // Generate all debugging information, including local variables. |
| arguments.add("-g"); |
| |
| // Append the source file that is to be compiled. Note that the user specifies only a relative file location. |
| arguments.add(file.getAbsolutePath()); |
| |
| return arguments.toArray(new String[arguments.size()]); |
| } |
| |
| /** |
| * <p>Returns a possibly newly created class loader 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 class loader 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 (ClassLoaderUtils.isClassAvailable(JAVAC_MAIN, ClassLoaderUtils.getDefaultClassLoader())) { |
| if (logger.isDebugEnabled()) { |
| logger.debug("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 class loader for the Javac compiler."); |
| } |
| |
| return ClassLoaderUtils.getDefaultClassLoader(); |
| } 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().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.isDebugEnabled()) { |
| logger.debug( |
| "The required JAR file '$JAVA_HOME$/lib/tools.jar' has been found ['" + toolsJarFile.getAbsolutePath() |
| + "']. A custom URL class loader will be created for the Javac compiler."); |
| } |
| |
| return new URLClassLoader( |
| new URL[]{toolsJarFile.toURI().toURL()}, ClassLoaderUtils.getDefaultClassLoader()); |
| } 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.isDebugEnabled()) { |
| logger.debug("The user has specified the required JAR file '$JAVA_HOME$/lib/tools.jar' ['" |
| + toolsJar.toExternalForm() + "']. A custom URL class loader will be created for the Javac compiler."); |
| } |
| |
| return new URLClassLoader(new URL[]{toolsJar}, ClassLoaderUtils.getDefaultClassLoader()); |
| } |
| } |
| } |
| |
| } |