package plume;
import java.io.*;
import java.util.*;
import java.util.regex.*;
/**
* This class has methods {@link #compile_source(String)} and {@link
* #compile_source(List)} that compile Java source files.
* It invokes a user-specified external command, such as <tt>javac</tt> or
* <tt>jikes</tt>.
**/
public final class FileCompiler {
public static Runtime runtime = java.lang.Runtime.getRuntime();
/**
* Matches the names of Java source files, without directory name.
* Match group 1 is the class name: the basse filename without the
* ".java" extension..
**/
static Pattern java_filename_pattern;
/** External command used to compile Java files. **/
private String compiler;
private long timeLimit;
static {
try {
java_filename_pattern
= Pattern.compile("([^" + UtilMDE.escapeNonJava(File.separator)
+ "]+)\\.java");
} catch (PatternSyntaxException me) {
me.printStackTrace();
throw new Error("Error in regexp", me);
}
}
/**
* Creates a new FileCompiler. Equivalent to FileCompiler("javac", 6000).
* @see #FileCompiler(String, long)
**/
public FileCompiler() {
this("javac", 6000);
}
/**
* Creates a new FileCompiler.
* @param compiler A command that runs a Java compiler; for instance, it
* could be the full path name or whatever is used on the commandline.
* @param timeLimit The maximum permitted compilation time, in msec.
**/
public FileCompiler(String compiler, long timeLimit) {
this.compiler = compiler;
this.timeLimit = timeLimit;
}
/**
* Compiles the files given by fileNames.
* @param fileNames pathes to the files to be compiled as Strings.
*/
public void compileFiles(List<String> fileNames) throws IOException {
// Start a process to compile all of the files (in one command)
TimeLimitProcess p = compile_source (fileNames);
String compile_errors = "";
String compile_output = "";
// Read stderr and stdout (if any) while waiting for the process to
// complete. Print both if there is an unexpected exception (timeout)
try {
compile_errors = UtilMDE.streamString (p.getErrorStream());
compile_output = UtilMDE.streamString (p.getInputStream());
int result = p.waitFor();
} catch (Throwable e) {
System.out.println ("Unexpected exception while compiling " + e);
if (p.timed_out())
System.out.println ("Compile timed out after " + p.timeout_msecs()
+ " msecs");
System.out.println ("Compile errors: " + compile_errors);
System.out.println ("Compile output: " + compile_output);
e.printStackTrace();
runtime.exit (1);
}
// javac tends to stop without completing the compilation if there
// is an error in one of the files. Remove all the erring files
// and recompile only the good ones.
if (compiler.equals("javac")) {
recompile_without_errors (fileNames, compile_errors);
}
}
/**
* @param filename the path of the Java source to be compiled
**/
private TimeLimitProcess compile_source(String filename) throws IOException {
String command = compiler + " " + filename;
// System.out.println ("\nexecuting compile command: " + command);
return new TimeLimitProcess(runtime.exec(command), timeLimit, true);
}
/**
* @param filenames the paths of the java source to be compiled as Strings.
* @return The process that executed the external compile command.
* @throws Error if an empty list of filenames is provided.
**/
private TimeLimitProcess compile_source(List<String> filenames) throws IOException {
int num_files = filenames.size();
if (num_files == 0) {
throw new Error("no files to compile were provided");
}
String to_compile = filenames.get(0);
for (int i = 1; i < num_files; i++) {
to_compile += (" " + filenames.get(i));
}
String command = compiler + " " + to_compile;
// System.out.println ("\nexecuting compile command: " + command);
return new TimeLimitProcess(runtime.exec(command), timeLimit, true);
}
/**
* Examine the errorString to identify the files that cannot
* compile, then recompile all the other files. This function is
* necessary when compiling with javac because javac does not
* compile all the files supplied to it if some of them contain
* errors. So some "good" files end up not being compiled.
*/
private void recompile_without_errors (List<String> fileNames, String errorString) throws IOException {
// search the error string and extract the files with errors.
if (errorString != null) {
HashSet<String> errorClasses = new HashSet<String>();
Matcher m = java_filename_pattern.matcher(errorString);
while (m.find()) {
@SuppressWarnings("nullness") // group 1 always matches in regexp
/*@NonNull*/ String sansExtension = m.group(1);
errorClasses.add(sansExtension);
}
// Collect all the files that were not compiled into retry
List<String> retry = new ArrayList<String>();
String filenames = "";
for (String sourceFileName : fileNames) {
sourceFileName = sourceFileName.trim();
String classFilePath = getClassFilePath(sourceFileName);
if (! fileExists(classFilePath)) {
if (! errorClasses.contains(getClassName(sourceFileName))) {
retry.add(sourceFileName);
filenames += " " + sourceFileName;
}
}
}
if (retry.size() > 0) {
TimeLimitProcess tp = compile_source(retry);
try {
tp.waitFor();
} catch (InterruptedException e) {
System.out.println ("Compile of " + filenames + " interrupted: "
+ e);
}
}
}
}
/**
* Return the file path to where a class file for a source
* file at sourceFilePath would be generated.
*/
private static String getClassFilePath(String sourceFilePath) {
int index = sourceFilePath.lastIndexOf('.');
if (index == -1) {
throw new IllegalArgumentException("sourceFilePath: "
+ sourceFilePath +
" must end with an extention.");
}
String classFilePath = sourceFilePath.substring(0, index);
classFilePath = classFilePath + ".class";
return classFilePath;
}
/**
* Returns the name of the class that sourceFilePath is
* a path for.
*/
private static String getClassName(String sourceFilePath) {
int dotIndex = sourceFilePath.lastIndexOf('.');
int slashIndex = sourceFilePath.lastIndexOf(File.separator);
if (dotIndex == -1) {
throw new IllegalArgumentException("sourceFilePath: " +
sourceFilePath +
" must end with an extention.");
} else if (slashIndex == -1) {
slashIndex = 0;
}
if (dotIndex < slashIndex) {
throw new IllegalArgumentException("sourceFilePath: " +
sourceFilePath +
" must end with an extention.");
}
return sourceFilePath.substring(slashIndex + 1, dotIndex);
}
private static boolean fileExists(String pathName) {
return (new File(pathName)).exists();
}
}