Compiler.java
/*
* Janino - An embedded Java[TM] compiler
*
* Copyright (c) 2019 Arno Unkrig. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.codehaus.commons.compiler.jdk;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import org.codehaus.commons.compiler.AbstractCompiler;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.ErrorHandler;
import org.codehaus.commons.compiler.ICompiler;
import org.codehaus.commons.compiler.Location;
import org.codehaus.commons.compiler.WarningHandler;
import org.codehaus.commons.compiler.jdk.util.JavaFileManagers;
import org.codehaus.commons.compiler.jdk.util.JavaFileObjects;
import org.codehaus.commons.compiler.jdk.util.JavaFileObjects.ResourceJavaFileObject;
import org.codehaus.commons.compiler.util.reflect.ApiLog;
import org.codehaus.commons.compiler.util.resource.Resource;
import org.codehaus.commons.compiler.util.resource.ResourceCreator;
import org.codehaus.commons.compiler.util.resource.ResourceFinder;
import org.codehaus.commons.nullanalysis.Nullable;
/**
* {@code javax.tools}-based implementation of the {@link ICompiler}.
*/
public
class Compiler extends AbstractCompiler {
private Collection<String> compilerOptions = new ArrayList<>();
private final JavaCompiler compiler;
public
Compiler() {
JavaCompiler c = ToolProvider.getSystemJavaCompiler();
if (c == null) {
throw new RuntimeException(
"JDK Java compiler not available - probably you're running a JRE, not a JDK",
null
);
}
this.compiler = c;
}
/**
* Initializes with a <em>different</em>, {@code javax.tools.JavaCompiler}-compatible Java compiler.
*/
public
Compiler(JavaCompiler compiler) { this.compiler = compiler; }
@Override public void
setVerbose(boolean verbose) {}
/**
* Adds command line options that are passed unchecked to the {@link java.lang.Compiler}.
* <p>
* Notice: Don't use the '-g' options - these are controlled through {@link #setDebugLines(boolean)}, {@link
* #setDebugVars(boolean)} and {@link #setDebugSource(boolean)}.
* </p>
*
* @param compilerOptions All command line options supported by the JDK JAVAC tool
*/
public void
setCompilerOptions(String[] compilerOptions) { this.compilerOptions = Arrays.asList(compilerOptions); }
@Override public void
compile(final Resource[] sourceResources) throws CompileException, IOException {
this.compile(sourceResources, null);
}
public void
compile(final Resource[] sourceResources, @Nullable SortedSet<Location> offsets) throws CompileException, IOException {
// Compose the effective compiler options.
List<String> options = new ArrayList<>(this.compilerOptions);
// Debug options.
{
List<String> l = new ArrayList<>();
if (this.debugLines) l.add("lines");
if (this.debugSource) l.add("source");
if (this.debugVars) l.add("vars");
if (l.isEmpty()) l.add("none");
Iterator<String> it = l.iterator();
String o = "-g:" + it.next();
while (it.hasNext()) o += "," + it.next();
options.add(o);
}
// Source / target version options.
{
if (this.sourceVersion != -1) {
options.add("-source");
options.add(Integer.toString(this.sourceVersion));
}
if (this.targetVersion != -1) {
options.add("-target");
options.add(Integer.toString(this.targetVersion));
}
}
// Bootclasspath.
{
File[] bcp = this.bootClassPath;
if (bcp != null) {
options.add("-bootclasspath");
options.add(Compiler.filesToPath(bcp));
}
}
// Classpath.
options.add("-classpath");
options.add(Compiler.filesToPath(this.classPath));
Compiler.compile(
this.compiler,
options,
this.sourceFinder,
this.sourceCharset,
this.classFileFinder,
this.classFileCreator,
sourceResources,
this.compileErrorHandler,
this.warningHandler,
offsets
);
}
static void
compile(
JavaCompiler compiler,
List<String> options,
ResourceFinder sourceFinder,
Charset sourceFileCharset,
ResourceFinder classFileFinder,
ResourceCreator classFileCreator,
Resource[] sourceFiles,
@Nullable ErrorHandler compileErrorHandler,
@Nullable WarningHandler warningHandler,
@Nullable SortedSet<Location> offsets
) throws CompileException, IOException {
// Wrap the source files in JavaFileObjects.
Collection<JavaFileObject> sourceFileObjects = new ArrayList<>();
for (int i = 0; i < sourceFiles.length; i++) {
Resource sourceResource = sourceFiles[i];
String fn = sourceResource.getFileName();
String className = fn.substring(fn.lastIndexOf(File.separatorChar) + 1).replace('/', '.');
if (className.endsWith(".java")) className = className.substring(0, className.length() - 5);
sourceFileObjects.add(JavaFileObjects.fromResource(
sourceResource,
className,
Kind.SOURCE,
sourceFileCharset
));
}
final JavaFileManager fileManager = Compiler.getJavaFileManager(
compiler,
sourceFinder,
sourceFileCharset,
classFileFinder,
classFileCreator
);
try {
Compiler.compile(
compiler,
options,
sourceFileObjects,
fileManager,
compileErrorHandler,
warningHandler,
offsets
);
} finally {
fileManager.close();
}
}
/**
* Creates a {@link JavaFileManager} that implements the given <var>sourceFileFinder</var>, <var>sourceFileCharset</var>,
* <var>classFileFinder</var> and <var>classFileCreator</var>.
*/
private static JavaFileManager
getJavaFileManager(
JavaCompiler compiler,
ResourceFinder sourceFileFinder,
Charset sourceFileCharset,
ResourceFinder classFileFinder,
ResourceCreator classFileCreator
) {
// Get the original FM, which reads class files through this JVM's BOOTCLASSPATH and
// CLASSPATH.
JavaFileManager jfm = compiler.getStandardFileManager(null, null, null);
// Store .class file via the classFileCreator.
jfm = JavaFileManagers.fromResourceCreator(
jfm,
StandardLocation.CLASS_OUTPUT,
Kind.CLASS,
classFileCreator,
Charset.defaultCharset()
);
// classFileFinder = ResourceFinders.debugResourceFinder(classFileFinder);
// Find existing .class files through the classFileFinder.
jfm = JavaFileManagers.fromResourceFinder(
jfm,
StandardLocation.CLASS_PATH,
Kind.CLASS,
classFileFinder,
Charset.defaultCharset() // irrelevant
);
// sourceFileFinder = ResourceFinders.debugResourceFinder(sourceFileFinder);
// Wrap it in a file manager that finds source files through the .sourceFinder.
jfm = JavaFileManagers.fromResourceFinder(
jfm,
StandardLocation.SOURCE_PATH,
Kind.SOURCE,
sourceFileFinder,
sourceFileCharset
);
return jfm;
}
/**
* Compiles on the {@link JavaFileManager} / {@link JavaFileObject} level.
*/
static void
compile(
JavaCompiler compiler,
List<String> options,
Collection<JavaFileObject> sourceFileObjects,
JavaFileManager fileManager,
@Nullable final ErrorHandler compileErrorHandler,
@Nullable final WarningHandler warningHandler,
@Nullable final SortedSet<Location> offsets
) throws CompileException, IOException {
fileManager = (JavaFileManager) ApiLog.logMethodInvocations(fileManager);
final int[] compileErrorCount = new int[1];
final DiagnosticListener<JavaFileObject> dl = new DiagnosticListener<JavaFileObject>() {
@Override public void
report(@Nullable Diagnostic<? extends JavaFileObject> diagnostic) {
assert diagnostic != null;
JavaFileObject source = diagnostic.getSource();
Location loc = new Location(
( // fileName
source == null ? null :
source instanceof ResourceJavaFileObject ? ((ResourceJavaFileObject) source).getResourceFileName() :
source.getName()
),
(short) diagnostic.getLineNumber(),
(short) diagnostic.getColumnNumber()
);
// Manipulate the diagnostic location to accomodate for the "offsets" (see "addOffset(String)"):
if (offsets != null) {
SortedSet<Location> hs = offsets.headSet(loc);
if (!hs.isEmpty()) {
Location co = hs.last();
loc = new Location(
co.getFileName(),
loc.getLineNumber() - co.getLineNumber() + 1,
(
loc.getLineNumber() == co.getLineNumber()
? loc.getColumnNumber() - co.getColumnNumber() + 1
: loc.getColumnNumber()
)
);
}
}
String message = diagnostic.getMessage(null) + " (" + diagnostic.getCode() + ")";
try {
switch (diagnostic.getKind()) {
case ERROR:
compileErrorCount[0]++;
if (compileErrorHandler == null) throw new CompileException(message, loc);
compileErrorHandler.handleError(diagnostic.toString(), loc);
break;
case MANDATORY_WARNING:
case WARNING:
if (warningHandler != null) warningHandler.handleWarning(null, message, loc);
break;
case NOTE:
case OTHER:
default:
break;
}
} catch (CompileException ce) {
// Wrap the CompileException in a RuntimeException in order to "tunnel" it through the JAVAC
// error handling.
//
// Unfortunately this does not work in VERY specific circumstances, namely test case
// "org.codehaus.commons.compiler.tests.JlsTest.test_9_3_1__Initialization_of_Fields_in_Interfaces__2()".
// The reason being is that "com.sun.tools.javac.api.ClientCodeWrapper.WrappedDiagnosticListener.report(Diagnostic<? extends T>)"
// wraps the RuntimeException in a com.sun.tools.javac.util.ClientCodeException, and
// "com.sun.tools.javac.code.Symbol.VarSymbol.getConstValue()" catches that and throws an
// AssertionError, which leads to a stack trace on STDERR.
//
// There is no obvious way to fix this.
// This problem exists for (at least) JAVA 7, 8, 11 an 17.
throw new RuntimeException(ce);
}
}
};
// Run the compiler.
try {
if (!compiler.getTask(
null, // out
fileManager, // fileManager
dl, // diagnosticListener
options, // options
null, // classes
sourceFileObjects // compilationUnits
).call() && compileErrorCount[0] == 0) throw new CompileException("Compilation failed for an unknown reason", null);
} catch (RuntimeException rte) {
// Unwrap the compilation exception and throw it.
for (Throwable t = rte.getCause(); t != null; t = t.getCause()) {
if (t instanceof CompileException) {
throw (CompileException) t; // SUPPRESS CHECKSTYLE AvoidHidingCause
}
if (t instanceof IOException) {
throw (IOException) t; // SUPPRESS CHECKSTYLE AvoidHidingCause
}
}
throw rte;
}
if (compileErrorCount[0] > 0) {
throw new CompileException("Compilation failed with " + compileErrorCount[0] + " errors", null);
}
}
private static String
filesToPath(File[] files) {
StringBuilder sb = new StringBuilder();
for (File cpe : files) {
if (sb.length() > 0) sb.append(File.pathSeparatorChar);
sb.append(cpe.getPath());
}
return sb.toString();
}
}