001package daikon.split; 002 003import java.io.ByteArrayOutputStream; 004import java.io.File; 005import java.io.IOException; 006import java.io.UncheckedIOException; 007import java.time.Duration; 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.HashSet; 011import java.util.List; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014import java.util.regex.PatternSyntaxException; 015import org.apache.commons.exec.CommandLine; 016import org.apache.commons.exec.DefaultExecuteResultHandler; 017import org.apache.commons.exec.DefaultExecutor; 018import org.apache.commons.exec.ExecuteException; 019import org.apache.commons.exec.ExecuteWatchdog; 020import org.apache.commons.exec.PumpStreamHandler; 021import org.checkerframework.checker.index.qual.Positive; 022import org.checkerframework.checker.nullness.qual.NonNull; 023import org.checkerframework.checker.regex.qual.Regex; 024import org.checkerframework.common.value.qual.MinLen; 025 026/** 027 * This class has method {@link #compileFiles(List)} that compiles Java source files. It invokes a 028 * user-specified external command, such as {@code javac} or {@code jikes}. 029 */ 030public final class FileCompiler { 031 032 /** The Runtime of the JVM. */ 033 public static Runtime runtime = java.lang.Runtime.getRuntime(); 034 035 /** Matches the names of Java source files. Match group 1 is the complete filename. */ 036 static @Regex(1) Pattern java_filename_pattern; 037 038 /** 039 * External command used to compile Java files, and command-line arguments. Guaranteed to be 040 * non-empty. 041 */ 042 private String @MinLen(1) [] compiler; 043 044 /** Time limit for compilation jobs. */ 045 private long timeLimit; 046 047 static { 048 try { 049 @Regex(1) String java_filename_re 050 // A javac error message may consist of several lines of output. 051 // The filename will be found at the beginning of the first line, 052 // the additional lines of information will all be indented. 053 // (?m) turns on MULTILINE mode so the first "^" matches the 054 // start of each error line output by javac. The blank space after 055 // the second "^" is intentional; together with the first "^", this 056 // says a filename can only be found at the start of a non-indented 057 // line as noted above. 058 = "(?m)^([^ ]+?\\.java)"; 059 java_filename_pattern = Pattern.compile(java_filename_re); 060 } catch (PatternSyntaxException me) { 061 me.printStackTrace(); 062 throw new Error("Error in regexp", me); 063 } 064 } 065 066 /** 067 * Creates a new FileCompiler. Compared to {@link #FileCompiler(String,long)}, this constructor 068 * permits spaces and other special characters in the command and arguments. 069 * 070 * @param compiler an array of Strings representing a command that runs a Java compiler (it could 071 * be the full path name or whatever is used on the commandline), plus any command-line 072 * options 073 * @param timeLimit the maximum permitted compilation time, in msec 074 */ 075 public FileCompiler(String @MinLen(1) [] compiler, @Positive long timeLimit) { 076 if (compiler.length == 0) { 077 throw new Error("no compile command was provided"); 078 } 079 080 this.compiler = compiler; 081 this.timeLimit = timeLimit; 082 } 083 084 /** 085 * Creates a new FileCompiler. Compared to {@link #FileCompiler(String,long)}, this constructor 086 * permits spaces and other special characters in the command and arguments. 087 * 088 * @param compiler a list of Strings representing a command that runs a Java compiler (it could be 089 * the full path name or whatever is used on the commandline), plus any command-line options 090 * @param timeLimit the maximum permitted compilation time, in msec 091 */ 092 @SuppressWarnings("value") // no index checker list support 093 public FileCompiler(/*(at)MinLen(1)*/ List<String> compiler, @Positive long timeLimit) { 094 this(compiler.toArray(new String[0]), timeLimit); 095 } 096 097 /** 098 * Creates a new FileCompiler. 099 * 100 * @param compiler a command that runs a Java compiler; for instance, it could be the full path 101 * name or whatever is used on the commandline. It may contain command-line arguments, and is 102 * split on spaces. 103 * @param timeLimit the maximum permitted compilation time, in msec 104 */ 105 public FileCompiler(String compiler, @Positive long timeLimit) { 106 this(compiler.trim().split(" +"), timeLimit); 107 } 108 109 /** 110 * Compiles the files given by fileNames. Returns the error output. 111 * 112 * @param fileNames paths to the files to be compiled as Strings 113 * @return the error output from compiling the files 114 * @throws IOException if there is a problem reading a file 115 */ 116 public String compileFiles(List<String> fileNames) throws IOException { 117 118 // System.out.printf("compileFiles: %s%n", fileNames); 119 120 // Start a process to compile all of the files (in one command) 121 String compile_errors = compile_source(fileNames); 122 123 // javac tends to stop without completing the compilation if there 124 // is an error in one of the files. Remove all the erring files 125 // and recompile only the good ones. 126 if (compiler[0].indexOf("javac") != -1) { 127 recompile_without_errors(fileNames, compile_errors); 128 } 129 130 return compile_errors; 131 } 132 133 /** 134 * Returns the error output from compiling the files. 135 * 136 * @param filenames the paths of the Java source to be compiled as Strings 137 * @return the error output from compiling the files 138 * @throws Error if an empty list of filenames is provided 139 */ 140 private String compile_source(List<String> filenames) throws IOException { 141 /* Apache Commons Exec objects */ 142 CommandLine cmdLine; 143 DefaultExecuteResultHandler resultHandler; 144 DefaultExecutor executor; 145 ExecuteWatchdog watchdog; 146 ByteArrayOutputStream outStream; 147 ByteArrayOutputStream errStream; 148 PumpStreamHandler streamHandler; 149 String compile_errors; 150 @SuppressWarnings("UnusedVariable") // for debugging 151 String compile_output; 152 153 if (filenames.isEmpty()) { 154 throw new Error("no files to compile were provided"); 155 } 156 157 cmdLine = new CommandLine(compiler[0]); // constructor requires executable name 158 // add rest of compiler command arguments 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 = DefaultExecutor.builder().get(); 166 watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofMillis(timeLimit)).get(); 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.isEmpty()) { 253 compile_source(retry); 254 } 255 } 256 } 257 258 /** 259 * Returns 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 extension."); 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}