001package daikon.split; 002 003import java.io.ByteArrayOutputStream; 004import java.io.File; 005import java.io.IOException; 006import java.io.UncheckedIOException; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.HashSet; 010import java.util.List; 011import java.util.regex.Matcher; 012import java.util.regex.Pattern; 013import java.util.regex.PatternSyntaxException; 014import org.apache.commons.exec.CommandLine; 015import org.apache.commons.exec.DefaultExecuteResultHandler; 016import org.apache.commons.exec.DefaultExecutor; 017import org.apache.commons.exec.ExecuteException; 018import org.apache.commons.exec.ExecuteWatchdog; 019import org.apache.commons.exec.PumpStreamHandler; 020import org.checkerframework.checker.index.qual.Positive; 021import org.checkerframework.checker.nullness.qual.NonNull; 022import org.checkerframework.checker.regex.qual.Regex; 023import org.checkerframework.common.value.qual.MinLen; 024 025/** 026 * This class has method {@link #compileFiles(List)} that compiles Java source files. It invokes a 027 * user-specified external command, such as {@code javac} or {@code jikes}. 028 */ 029public final class FileCompiler { 030 031 /** The Runtime of the JVM. */ 032 public static Runtime runtime = java.lang.Runtime.getRuntime(); 033 034 /** Matches the names of Java source files. Match group 1 is the complete filename. */ 035 static @Regex(1) Pattern java_filename_pattern; 036 037 /** 038 * External command used to compile Java files, and command-line arguments. Guaranteed to be 039 * non-empty. 040 */ 041 private String @MinLen(1) [] compiler; 042 043 /** Time limit for compilation jobs. */ 044 private long timeLimit; 045 046 static { 047 try { 048 @Regex(1) String java_filename_re 049 // A javac error message may consist of several lines of output. 050 // The filename will be found at the beginning of the first line, 051 // the additional lines of information will all be indented. 052 // (?m) turns on MULTILINE mode so the first "^" matches the 053 // start of each error line output by javac. The blank space after 054 // the second "^" is intentional; together with the first "^", this 055 // says a filename can only be found at the start of a non-indented 056 // line as noted above. 057 = "(?m)^([^ ]+?\\.java)"; 058 java_filename_pattern = Pattern.compile(java_filename_re); 059 } catch (PatternSyntaxException me) { 060 me.printStackTrace(); 061 throw new Error("Error in regexp", me); 062 } 063 } 064 065 /** 066 * Creates a new FileCompiler. Compared to {@link #FileCompiler(String,long)}, this constructor 067 * permits spaces and other special characters in the command and arguments. 068 * 069 * @param compiler an array of Strings representing a command that runs a Java compiler (it could 070 * be the full path name or whatever is used on the commandline), plus any command-line 071 * options 072 * @param timeLimit the maximum permitted compilation time, in msec 073 */ 074 public FileCompiler(String @MinLen(1) [] compiler, @Positive long timeLimit) { 075 if (compiler.length == 0) { 076 throw new Error("no compile command was provided"); 077 } 078 079 this.compiler = compiler; 080 this.timeLimit = timeLimit; 081 } 082 083 /** 084 * Creates a new FileCompiler. Compared to {@link #FileCompiler(String,long)}, this constructor 085 * permits spaces and other special characters in the command and arguments. 086 * 087 * @param compiler a list of Strings representing a command that runs a Java compiler (it could be 088 * the full path name or whatever is used on the commandline), plus any command-line options 089 * @param timeLimit the maximum permitted compilation time, in msec 090 */ 091 @SuppressWarnings("value") // no index checker list support 092 public FileCompiler(/*(at)MinLen(1)*/ ArrayList<String> compiler, @Positive long timeLimit) { 093 this(compiler.toArray(new String[0]), timeLimit); 094 } 095 096 /** 097 * Creates a new FileCompiler. 098 * 099 * @param compiler a command that runs a Java compiler; for instance, it could be the full path 100 * name or whatever is used on the commandline. It may contain command-line arguments, and is 101 * split on spaces. 102 * @param timeLimit the maximum permitted compilation time, in msec 103 */ 104 public FileCompiler(String compiler, @Positive long timeLimit) { 105 this(compiler.trim().split(" +"), timeLimit); 106 } 107 108 /** 109 * Compiles the files given by fileNames. Returns the error output. 110 * 111 * @param fileNames paths to the files to be compiled as Strings 112 * @return the error output from compiling the files 113 * @throws IOException if there is a problem reading a file 114 */ 115 public String compileFiles(List<String> fileNames) throws IOException { 116 117 // System.out.printf("compileFiles: %s%n", fileNames); 118 119 // Start a process to compile all of the files (in one command) 120 String compile_errors = compile_source(fileNames); 121 122 // javac tends to stop without completing the compilation if there 123 // is an error in one of the files. Remove all the erring files 124 // and recompile only the good ones. 125 if (compiler[0].indexOf("javac") != -1) { 126 recompile_without_errors(fileNames, compile_errors); 127 } 128 129 return compile_errors; 130 } 131 132 /** 133 * Returns the error output from compiling the files. 134 * 135 * @param filenames the paths of the Java source to be compiled as Strings 136 * @return the error output from compiling the files 137 * @throws Error if an empty list of filenames is provided 138 */ 139 private String compile_source(List<String> filenames) throws IOException { 140 /* Apache Commons Exec objects */ 141 CommandLine cmdLine; 142 DefaultExecuteResultHandler resultHandler; 143 DefaultExecutor executor; 144 ExecuteWatchdog watchdog; 145 ByteArrayOutputStream outStream; 146 ByteArrayOutputStream errStream; 147 PumpStreamHandler streamHandler; 148 String compile_errors; 149 @SuppressWarnings("UnusedVariable") // for debugging 150 String compile_output; 151 152 if (filenames.size() == 0) { 153 throw new Error("no files to compile were provided"); 154 } 155 156 cmdLine = new CommandLine(compiler[0]); // constructor requires executable name 157 // add rest of compiler command arguments 158 @SuppressWarnings("nullness") // arguments are in range, so result array contains no nulls 159 @NonNull String[] args = Arrays.copyOfRange(compiler, 1, compiler.length); 160 cmdLine.addArguments(args); 161 // add file name arguments 162 cmdLine.addArguments(filenames.toArray(new String[0])); 163 164 resultHandler = new DefaultExecuteResultHandler(); 165 executor = new DefaultExecutor(); 166 watchdog = new ExecuteWatchdog(timeLimit); 167 executor.setWatchdog(watchdog); 168 outStream = new ByteArrayOutputStream(); 169 errStream = new ByteArrayOutputStream(); 170 streamHandler = new PumpStreamHandler(outStream, errStream); 171 executor.setStreamHandler(streamHandler); 172 173 // System.out.println(); System.out.println("executing compile command: " + cmdLine); 174 try { 175 executor.execute(cmdLine, resultHandler); 176 } catch (IOException e) { 177 throw new UncheckedIOException("exception starting process: " + cmdLine, e); 178 } 179 180 int exitValue = -1; 181 try { 182 resultHandler.waitFor(); 183 exitValue = resultHandler.getExitValue(); 184 } catch (InterruptedException e) { 185 // Ignore exception, but watchdog.killedProcess() records that the process timed out. 186 } 187 boolean timedOut = executor.isFailure(exitValue) && watchdog.killedProcess(); 188 189 try { 190 @SuppressWarnings("DefaultCharset") // toString(Charset) was introduced in Java 10 191 String compile_errors_tmp = errStream.toString(); 192 compile_errors = compile_errors_tmp; 193 } catch (RuntimeException e) { 194 throw new Error("Exception getting process error output", e); 195 } 196 197 try { 198 @SuppressWarnings("DefaultCharset") // toString(Charset) was introduced in Java 10 199 String compile_output_tmp = errStream.toString(); 200 compile_output = compile_output_tmp; 201 } catch (RuntimeException e) { 202 throw new Error("Exception getting process standard output", e); 203 } 204 205 if (timedOut) { 206 // Print stderr and stdout if there is an unexpected exception (timeout). 207 System.out.println("Compile timed out after " + timeLimit + " msecs"); 208 // System.out.println ("Compile errors: " + compile_errors); 209 // System.out.println ("Compile output: " + compile_output); 210 ExecuteException e = resultHandler.getException(); 211 if (e != null) { 212 e.printStackTrace(); 213 } 214 runtime.exit(1); 215 } 216 return compile_errors; 217 } 218 219 /** 220 * Examine the errorString to identify the files that cannot compile, then recompile all the other 221 * files. This function is necessary when compiling with javac because javac does not compile all 222 * the files supplied to it if some of them contain errors. So some "good" files end up not being 223 * compiled. 224 * 225 * @param fileNames all the files that were attempted to be compiled 226 * @param errorString the error string that indicates which files could not be compiled 227 */ 228 private void recompile_without_errors(List<String> fileNames, String errorString) 229 throws IOException { 230 // search the error string and extract the files with errors. 231 if (errorString != null) { 232 HashSet<String> errorClasses = new HashSet<>(); 233 Matcher m = java_filename_pattern.matcher(errorString); 234 while (m.find()) { 235 @SuppressWarnings( 236 "nullness") // Regex Checker imprecision: find() guarantees that group 1 exists 237 @NonNull String sansExtension = m.group(1); 238 errorClasses.add(sansExtension); 239 } 240 // Collect all the files that were not compiled into retry 241 List<String> retry = new ArrayList<>(); 242 for (String sourceFileName : fileNames) { 243 sourceFileName = sourceFileName.trim(); 244 String classFilePath = getClassFilePath(sourceFileName); 245 if (!fileExists(classFilePath)) { 246 if (!errorClasses.contains(sourceFileName)) { 247 retry.add(sourceFileName); 248 } 249 } 250 } 251 252 if (retry.size() > 0) { 253 compile_source(retry); 254 } 255 } 256 } 257 258 /** 259 * Return the file path to where a class file for a source file at sourceFilePath would be 260 * generated. 261 * 262 * @param sourceFilePath the path to the .java file 263 * @return the path to the corresponding .class file 264 */ 265 private static String getClassFilePath(String sourceFilePath) { 266 int index = sourceFilePath.lastIndexOf('.'); 267 if (index == -1) { 268 throw new IllegalArgumentException( 269 "sourceFilePath: " + sourceFilePath + " must end with an extention."); 270 } 271 return sourceFilePath.substring(0, index) + ".class"; 272 } 273 274 /** 275 * Returns true if the given file exists. 276 * 277 * @param pathName path to check for existence 278 * @return true iff the file exists 279 */ 280 private static boolean fileExists(String pathName) { 281 return new File(pathName).exists(); 282 } 283}