/**********************************************************************
* Copyright (c) 2005-2009 ant4eclipse project team.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Nils Hartmann, Daniel Kasmeroglu, Gerd Wuetherich
**********************************************************************/
package org.ant4eclipse.ant.jdt.ecj;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.ant4eclipse.ant.core.AntConfigurator;
import org.ant4eclipse.ant.jdt.EcjAdditionalCompilerArguments;
import org.ant4eclipse.lib.core.Assure;
import org.ant4eclipse.lib.core.exception.Ant4EclipseException;
import org.ant4eclipse.lib.core.logging.A4ELogging;
import org.ant4eclipse.lib.core.util.PerformanceLogging;
import org.ant4eclipse.lib.core.util.StringMap;
import org.ant4eclipse.lib.core.util.Utilities;
import org.ant4eclipse.lib.jdt.ecj.ClassFileLoader;
import org.ant4eclipse.lib.jdt.ecj.ClassFileLoaderFactory;
import org.ant4eclipse.lib.jdt.ecj.CompileJobDescription;
import org.ant4eclipse.lib.jdt.ecj.CompileJobResult;
import org.ant4eclipse.lib.jdt.ecj.DefaultCompileJobDescription;
import org.ant4eclipse.lib.jdt.ecj.EcjAdapter;
import org.ant4eclipse.lib.jdt.ecj.EcjExceptionCodes;
import org.ant4eclipse.lib.jdt.ecj.SourceFile;
import org.ant4eclipse.lib.jdt.ecj.SourceFileFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.compilers.DefaultCompilerAdapter;
import org.apache.tools.ant.taskdefs.condition.Os;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.resources.FileResource;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
/**
* <p>
* Basic implementation of a compiler adapter used within A4E.
* </p>
*
* @author Gerd Wütherich (gerd@gerd-wuetherich.de)
* @author Daniel Kasmeroglu (Daniel.Kasmeroglu@Kasisoft.net)
*/
public abstract class A4ECompilerAdapter extends DefaultCompilerAdapter {
private static final String ANT4ECLIPSE_DEFAULT_FILE_ENCODING = "ant4eclipse.default.file.encoding";
private static final String ANT4ECLIPSE_COMPILE_ERRORS_FILE = "ant4eclipse.compile.errors.file";
/** format of the compile problem message */
private static final String COMPILE_PROBLEM_MESSAGE = "----------\n%s. %s in %s (at line %s)\n%s\n%s\n%s\n";
/** the compiler argument separator */
private static final String COMPILER_ARGS_SEPARATOR = "=";
/** the refid key for the additional compiler arguments */
private static final String COMPILER_ARGS_REFID_KEY = "compiler.args.refid";
/** the refid key for compiler.options.file */
private static final String COMPILER_OPTIONS_FILE = "compiler.options.file";
/**
* the refid key for default compiler options file.
*
* <p>
* If not set or set to an empty string no default compiler settings will be used
* </p>
*/
private static final String DEFAULT_COMPILER_OPTIONS_FILE = "default.compiler.options.file";
private boolean _warnings = true;
/**
* Enables/disables the generation of warn messages.
*
* @param newwarnings
* <code>true</code> <=> Enable warn messages.
*/
public void setWarnings(boolean newwarnings) {
this._warnings = newwarnings;
}
/**
* <p>
* Checks the preconditions of the A4ECompilerAdapter
* </p>
*
* @throws BuildException
*/
private void preconditions() throws BuildException {
// source path is not supported!
if (getJavac().getSourcepath() != null) {
throw new Ant4EclipseException(EcjExceptionCodes.JAVAC_SOURCE_PATH_NOT_SUPPORTED_EXCEPTION);
}
}
/**
* {@inheritDoc}
*/
public boolean execute() throws BuildException {
// Step 1: check preconditions
preconditions();
// Step 2: Configure ant4eclipse: This is unnecessary if the adapter is launched within the
// JdtCompilerTask as it's using the defining classpath. But it's still useful in case someone
// wants to make use of the original Javac task. Anyway there's no harm in using this function
// anymore.
AntConfigurator.configureAnt4Eclipse(getProject());
// Step 3: Fetch compiler arguments
EcjAdditionalCompilerArguments ecjAdditionalCompilerArguments = fetchEcjAdditionalCompilerArguments();
// Step 4: create CompileJobDescription
DefaultCompileJobDescription compileJobDescription = new DefaultCompileJobDescription();
SourceFile[] sourceFiles = getSourceFilesToCompile(ecjAdditionalCompilerArguments);
compileJobDescription.setSourceFiles(sourceFiles);
compileJobDescription.setClassFileLoader(createClassFileLoader(ecjAdditionalCompilerArguments));
// Step 5: set the compiler options
String compilerOptionsFileName = extractJavacCompilerArg(COMPILER_OPTIONS_FILE, null);
String defaultCompilerOptionsFileName = extractJavacCompilerArg(DEFAULT_COMPILER_OPTIONS_FILE, null);
StringMap compilerOptions = CompilerOptionsProvider.getCompilerOptions(getJavac(), compilerOptionsFileName,
defaultCompilerOptionsFileName);
compileJobDescription.setCompilerOptions(compilerOptions);
if (A4ELogging.isTraceingEnabled()) {
A4ELogging.trace("CompileJobDescription: %s", compileJobDescription);
}
// Step 6: Compile
CompileJobResult compileJobResult = compile(compileJobDescription);
// Step 7: dump result
CategorizedProblem[] categorizedProblems = compileJobResult.getCategorizedProblems();
// Buffer for messages
StringBuilder builder = new StringBuilder();
for (int i = 0; i < categorizedProblems.length; i++) {
CategorizedProblem categorizedProblem = categorizedProblems[i];
if (categorizedProblem.isError() || (categorizedProblem.isWarning() && !getJavac().getNowarn())) {
String fileName = String.valueOf(categorizedProblem.getOriginatingFileName());
for (SourceFile sourceFile : sourceFiles) {
if (fileName.equals(sourceFile.getSourceFileName())) {
if (!categorizedProblem.isError()) {
if (!this._warnings) {
continue;
}
}
Object[] args = new Object[7];
args[0] = Integer.valueOf(i + 1);
args[1] = categorizedProblem.isError() ? "ERROR" : "WARNING";
args[2] = sourceFile.getSourceFile().getAbsolutePath();
args[3] = Integer.valueOf(categorizedProblem.getSourceLineNumber());
String[] problematicLine = readProblematicLine(sourceFile, categorizedProblem);
args[4] = problematicLine[0];
args[5] = problematicLine[1];
args[6] = categorizedProblem.getMessage();
builder.append(String.format(COMPILE_PROBLEM_MESSAGE, args));
if (i + 1 == categorizedProblems.length) {
builder.append("----------\n");
}
}
}
}
}
// Dump error messages if any
if (builder.length() > 0) {
// Dump to logging system
A4ELogging.error(builder.toString());
// Optional: dump to specified file
String compilerErrorFile = System.getProperty(ANT4ECLIPSE_COMPILE_ERRORS_FILE);
if (compilerErrorFile != null) {
Utilities.appendFile(new File(compilerErrorFile), builder.toString().getBytes());
}
}
// if the destination directory has been specified for the javac task we might need
// to copy the generated class files
if (compileJobResult.succeeded() && (getJavac().getDestdir() != null)) {
/**
* @todo [12-Apr-2011:KASI] This needs to be supported for Javac, too. It would be possible to use the destdir
* alternatively but references like the EcjAdditionalCompilerArguments need to be adopted in this case.
*/
File destdir = Utilities.getCanonicalFile(getJavac().getDestdir());
PerformanceLogging.start(A4ECompilerAdapter.class, "cloneClasses");
try {
cloneClasses(destdir, compileJobResult.getCompiledClassFiles());
} finally {
PerformanceLogging.stop(A4ECompilerAdapter.class, "cloneClasses");
}
}
// throw Exception if compilation was not successful
if (!compileJobResult.succeeded()) {
throw new Ant4EclipseException(EcjExceptionCodes.COMPILATION_WAS_NOT_SUCCESFUL);
}
// Step 8: Return
return true;
}
/**
* Clones all generated class files while copying them into the user specified directory.
*
* @param destdir
* The destination director to save the classes to. Not <code>null</code>.
* @param compiledclasses
* A map which provides all compiled classes. Not <code>null</code>.
*/
private void cloneClasses(File destdir, Map<String, File> compiledclasses) {
if (!destdir.isAbsolute()) {
destdir = destdir.getAbsoluteFile();
}
for (Map.Entry<String, File> entry : compiledclasses.entrySet()) {
File destfile = Utilities.getCanonicalFile(new File(destdir, entry.getKey()));
Utilities.mkdirs(destfile.getParentFile());
if (!destfile.equals(entry.getValue())) {
Utilities.copy(entry.getValue(), destfile);
}
}
}
/**
* Runs the compilation according to the supplied compilation description.
*
* @param description
* The description which provides all necessary information for the compilation.
*
* @return A descriptional instance which provides some information which came up during the compilation.
*/
protected abstract CompileJobResult compile(CompileJobDescription description);
/**
* <p>
* Returns an array with all the source files to compile.
* </p>
*
* @param compilerArguments
* can be null
* @return the source files to compile
*/
private SourceFile[] getSourceFilesToCompile(EcjAdditionalCompilerArguments compilerArguments) {
// get default destination folder
File defaultDestinationFolder = getJavac().getDestdir();
// get the files to compile
List<SourceFile> sourceFiles = new LinkedList<SourceFile>();
File[] filelist = getJavac().getFileList();
for (File file : filelist) {
A4ELogging.debug("Sourcefile provided by the javac-task: %s", file);
}
// support for filtered filesets
if ((compilerArguments != null) && compilerArguments.hasSourceFilteredFilesetPath()) {
// create the result list
List<File> files = new LinkedList<File>();
Path path = compilerArguments.getSourceFilteredFilesetPath();
Iterator<?> iterator = path.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
if (object instanceof FileResource) {
FileResource fileResource = (FileResource) object;
File sourceFile = fileResource.getFile();
// only handle java files
if (sourceFile.getName().endsWith(".java")) {
files.add(fileResource.getFile());
}
}
}
// set the file list
filelist = files.toArray(new File[files.size()]);
for (File file : filelist) {
A4ELogging.debug("Sourcefile which passed the filters: %s", file);
}
// log
A4ELogging.info("Compiling %s source %s (filtered %s source %s from source file list).", Integer
.valueOf(filelist.length), filelist.length > 1 ? "files" : "file", Integer.valueOf(this.compileList.length
- filelist.length), this.compileList.length - filelist.length > 1 ? "files" : "file");
}
// iterate over all the source files and create SourceFile
for (File file : filelist) {
if (!hasSourceFolder(file)) {
// the user has restricted the source folders for the compilation.
// f.e. the project has two source folders while the user only compiles one at
// a time
continue;
}
// get the source folder
File sourceFolder = getSourceFolder(file);
// get the relative source file name
String sourceFileName = file.getAbsolutePath().substring(
sourceFolder.getAbsolutePath().length() + File.separator.length());
// get the destination folder
File destinationFolder = compilerArguments != null ? compilerArguments.getOutputFolder(sourceFolder)
: defaultDestinationFolder;
// make sure a destination folder is set
if (destinationFolder == null) {
throw new Ant4EclipseException(EcjExceptionCodes.NO_DEST_PATH_SET);
}
// BUG-FIX for http://www.javakontor.org:8080/jira/browse/AE-203
// compile package-info.java first
if (sourceFileName.endsWith("package-info.java")) {
// add the new source file
sourceFiles.add(0,
SourceFileFactory.createSourceFile(sourceFolder, sourceFileName, destinationFolder, getDefaultEncoding()));
}
// END BUG-FIX
else {
// add the new source file
sourceFiles.add(SourceFileFactory.createSourceFile(sourceFolder, sourceFileName, destinationFolder,
getDefaultEncoding()));
}
}
// return the result
return sourceFiles.toArray(new SourceFile[sourceFiles.size()]);
}
/**
* <p>
* Returns the source folder for the given source file.
* </p>
*
* @param sourceFile
* the source file.
* @return the source folder
*/
private File getSourceFolder(File sourceFile) {
// get the absolute path
String absolutePath = sourceFile.getAbsolutePath();
// get the list of all source directories
String[] srcDirs = getJavac().getSrcdir().list();
// find the 'right' source directory
for (String srcDir : srcDirs) {
if (absolutePath.startsWith(srcDir) && absolutePath.charAt(srcDir.length()) == File.separatorChar) {
return new File(srcDir);
}
}
// source folder for source file does not exist...
throw new Ant4EclipseException(EcjExceptionCodes.SOURCE_FOLDER_FOR_SOURCE_FILE_DOES_NOT_EXIST,
sourceFile.getAbsolutePath());
}
/**
* <p>
* Returns <code>true</code> if there's a source folder for the given source file.
* </p>
*
* @param sourceFile
* the source file.
* @return <code>true</code> the source folder exists for the given source file.
*/
private boolean hasSourceFolder(File sourceFile) {
// get the absolute path
String absolutePath = sourceFile.getAbsolutePath();
// get the list of all source directories
String[] srcDirs = getJavac().getSrcdir().list();
// find the 'right' source directory
for (String srcDir : srcDirs) {
if (absolutePath.startsWith(srcDir) && absolutePath.charAt(srcDir.length()) == File.separatorChar) {
return true;
}
}
// source folder for source file does not exist...
return false;
}
/**
* <p>
* Creates class file loader.
* </p>
*
* @param compilerArguments
* the compiler arguments, can be <code>null</code>.
* @return the class file loader.
*/
@SuppressWarnings("unchecked")
private ClassFileLoader createClassFileLoader(EcjAdditionalCompilerArguments compilerArguments) {
// Step 1: create class file loader list
List<ClassFileLoader> classFileLoaderList = new LinkedList<ClassFileLoader>();
// Step 2: add boot class loader
if (getJavac().getBootclasspath() != null) {
classFileLoaderList.add(createBootClassLoader(compilerArguments));
}
// Step 3: add class loader for class path entries
if (getJavac().getClasspath() != null) {
Iterator<FileResource> iterator = getJavac().getClasspath().iterator();
while (iterator.hasNext()) {
// get the file resource that contains the class files
FileResource fileResource = iterator.next();
File classesFile = fileResource.getFile();
ClassFileLoader myclassFileLoader = null;
// jar files
if (classesFile.isFile()) {
// if (ClassFileLoaderCache.getInstance().hasClassFileLoader(classesFile)) {
// myclassFileLoader = ClassFileLoaderCache.getInstance().getClassFileLoader(classesFile);
// } else {
myclassFileLoader = ClassFileLoaderFactory.createClasspathClassFileLoader(classesFile, EcjAdapter.LIBRARY,
new File[] { classesFile }, new File[] {});
// ClassFileLoaderCache.getInstance().storeClassFileLoader(classesFile, myclassFileLoader);
// }
} else {
// get source folders if available
File[] sourceFolders = new File[] {};
if ((compilerArguments != null) && compilerArguments.hasSourceFoldersForOutputFolder(classesFile)) {
sourceFolders = compilerArguments.getSourceFoldersForOutputFolder(classesFile);
}
// create class file loader for file resource
// TODO: LIBRARY AND PROJECT
myclassFileLoader = ClassFileLoaderFactory.createClasspathClassFileLoader(classesFile, EcjAdapter.LIBRARY,
new File[] { classesFile }, sourceFolders);
}
// create and add FilteringClassFileLoader is necessary
if (compilerArguments != null && compilerArguments.hasAccessRestrictions(fileResource.getFile())) {
classFileLoaderList.add(ClassFileLoaderFactory.createFilteringClassFileLoader(myclassFileLoader,
compilerArguments.getAccessRestrictions(fileResource.getFile())));
}
// else add class file loader
else {
classFileLoaderList.add(myclassFileLoader);
}
}
}
// Step 4: return the compound class file loader
return ClassFileLoaderFactory.createCompoundClassFileLoader(classFileLoaderList.toArray(new ClassFileLoader[0]));
}
/**
* <p>
* Create a boot class loader.
* </p>
*
* @param compilerArguments
* the compiler arguments , can be <code>null</code>.
* @return the boot class loader
*/
@SuppressWarnings("unchecked")
private ClassFileLoader createBootClassLoader(EcjAdditionalCompilerArguments compilerArguments) {
// Step 1: get the boot class path as specified in the javac task
Path bootclasspath = getJavac().getBootclasspath();
// if (ClassFileLoaderCache.getInstance().hasClassFileLoader(bootclasspath.toString())) {
// return ClassFileLoaderCache.getInstance().getClassFileLoader(bootclasspath.toString());
// }
// Step 2: create ClassFileLoaders for each entry in the boot class path
List<ClassFileLoader> bootClassFileLoaders = new LinkedList<ClassFileLoader>();
// Step 3: iterate over the boot class path entries as specified in the ant path
for (Iterator<FileResource> iterator = bootclasspath.iterator(); iterator.hasNext();) {
// get the file resource
FileResource fileResource = iterator.next();
// create class file loader
if (fileResource.getFile().exists()) {
ClassFileLoader classFileLoader = ClassFileLoaderFactory.createClasspathClassFileLoader(fileResource.getFile(),
EcjAdapter.LIBRARY);
bootClassFileLoaders.add(classFileLoader);
}
}
// Step 4: create compound class file loader
ClassFileLoader classFileLoader = ClassFileLoaderFactory.createCompoundClassFileLoader(bootClassFileLoaders
.toArray(new ClassFileLoader[0]));
// Step 5: create FilteringClassFileLoader is necessary
if (compilerArguments != null && compilerArguments.hasBootClassPathAccessRestrictions()) {
// Step 4: debug
if (A4ELogging.isDebuggingEnabled()) {
A4ELogging.debug("Boot class path access restrictions: '%s'",
compilerArguments.getBootClassPathAccessRestrictions());
}
classFileLoader = ClassFileLoaderFactory.createFilteringClassFileLoader(classFileLoader,
compilerArguments.getBootClassPathAccessRestrictions());
}
// ClassFileLoaderCache.getInstance().storeClassFileLoader(bootclasspath.toString(), classFileLoader);
return classFileLoader;
}
/**
* <p>
* Helper method that reads the compiler argument with the specified name from the ant's javac task.
* </p>
* <p>
* Compiler arguments can be specified using <code><compilerarg/></code> subelement:
*
* <pre>
* <code> <javac destdir="${executeJdtProject.default.output.directory}"
* debug="on"
* source="1.5">
* ...
* <compilerarg value="compiler.args.refid=executeJdtProject.compiler.args"
* compiler="org.ant4eclipse.ant.jdt.ecj.EcjCompilerAdapter" />
* </javac>
* </code>
* </pre>
*
* </p>
*
* @param argumentName
* @param defaultValue
* @return
*/
private String extractJavacCompilerArg(String argumentName, String defaultValue) {
Assure.notNull("argumentName", argumentName);
// Step 1: Get all compilerArguments
String[] currentCompilerArgs = getJavac().getCurrentCompilerArgs();
// Step 2: Find the 'right' one
for (String compilerArg : currentCompilerArgs) {
// split the argument
String[] args = compilerArg.split(COMPILER_ARGS_SEPARATOR);
// requested one?
if (args.length > 1 && argumentName.equalsIgnoreCase(args[0])) {
// return the argument
return args[1];
}
}
// Step 3: Return defaultValue
return defaultValue;
}
/**
* <p>
* Helper method that fetches the {@link EcjAdditionalCompilerArguments} from the underlying ant project. The
* {@link EcjAdditionalCompilerArguments} are set when a JDT class path is resolved by ant4eclipse.
* </p>
* <p>
* If no {@link EcjAdditionalCompilerArguments} are set, <code>null</code> will be returned.
* </p>
*
* @return the {@link EcjAdditionalCompilerArguments}
*/
private EcjAdditionalCompilerArguments fetchEcjAdditionalCompilerArguments() {
// Step 1: Fetch the CompilerArgument key
String compilerArgsRefid = extractJavacCompilerArg(COMPILER_ARGS_REFID_KEY, null);
// Step 2: Return null, if no EcjAdditionalCompilerArguments are set
if (compilerArgsRefid == null) {
return null;
}
// Step 3: Fetch the compiler arguments
EcjAdditionalCompilerArguments compilerArguments = (EcjAdditionalCompilerArguments) getProject().getReference(
compilerArgsRefid);
// Step 4: Throw exception if null
if (compilerArguments == null) {
throw new Ant4EclipseException(EcjExceptionCodes.NO_ECJ_ADDITIONAL_COMPILER_ARGUMENTS_OBJECT, compilerArgsRefid);
}
A4ELogging.debug("Using compilerArguments '%s'", compilerArguments);
// Step 5: Return the result
return compilerArguments;
}
/**
* <p>
* </p>
*
* @param sourceFile
* @param lineNumber
* @param sourceStart
* @param sourceEnd
* @return
*/
private String[] readProblematicLine(SourceFile sourceFile, CategorizedProblem categorizedProblem) {
Assure.notNull("sourceFile", sourceFile);
Assure.notNull("categorizedProblem", categorizedProblem);
int lineNumber = categorizedProblem.getSourceLineNumber();
int sourceStart = categorizedProblem.getSourceStart();
int sourceEnd = categorizedProblem.getSourceEnd();
try {
// Open the file that is the first
// command line parameter
FileInputStream fstream = new FileInputStream(sourceFile.getSourceFile());
// Get the object of DataInputStream
DataInputStream in = new DataInputStream(fstream);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
int lineStart = 0;
String strLine = "";
// Read File Line By Line
for (int i = 0; i < lineNumber; i++) {
String newLine = br.readLine();
lineStart = lineStart + strLine.length();
if (i + 1 != lineNumber) {
lineStart = lineStart + 1;
}
strLine = newLine;
}
Utilities.close(in);
StringBuilder underscoreLine = new StringBuilder();
for (int i = lineStart; i < sourceStart; i++) {
if (strLine.charAt(i - lineStart) == '\t') {
underscoreLine.append('\t');
} else {
underscoreLine.append(' ');
}
}
for (int i = sourceStart; i <= sourceEnd; i++) {
underscoreLine.append('^');
}
return new String[] { strLine, underscoreLine.toString() };
} catch (Exception e) {// Catch exception if any
return new String[] { "", "" };
}
}
/**
* <p>
* Helper method. Returns the default encoding of the eclipse workspace.
* </p>
*
* @return the default encoding
*/
private String getDefaultEncoding() {
// Step 1: is the 'ANT4ECLIPSE_DEFAULT_FILE_ENCODING' property set?
String property = getProject().getProperty(ANT4ECLIPSE_DEFAULT_FILE_ENCODING);
if (property != null) {
return property;
}
// Step 2: is the encoding set in the javac task?
String encoding = getJavac().getEncoding();
if (encoding != null) {
return encoding;
}
// Step 3: try to resolve the os specific eclipse encoding
if (Os.isFamily(Os.FAMILY_WINDOWS) && Charset.isSupported("Cp1252")) {
return "Cp1252";
} else if (Os.isFamily(Os.FAMILY_UNIX) && Charset.isSupported("UTF-8")) {
return "UTF-8";
} else if (Os.isFamily(Os.FAMILY_MAC) && Charset.isSupported("MacRoman")) {
return "MacRoman";
}
// Step 4: last resort: return the default file encoding
return System.getProperty("file.encoding");
}
} /* ENDCLASS */