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.size() == 0) { 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 @SuppressWarnings("nullness") // arguments are in range, so result array contains no nulls 160 @NonNull String[] args = Arrays.copyOfRange(compiler, 1, compiler.length); 161 cmdLine.addArguments(args); 162 // add file name arguments 163 cmdLine.addArguments(filenames.toArray(new String[0])); 164 165 resultHandler = new DefaultExecuteResultHandler(); 166 executor = DefaultExecutor.builder().get(); 167 watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofMillis(timeLimit)).get(); 168 executor.setWatchdog(watchdog); 169 outStream = new ByteArrayOutputStream(); 170 errStream = new ByteArrayOutputStream(); 171 streamHandler = new PumpStreamHandler(outStream, errStream); 172 executor.setStreamHandler(streamHandler); 173 174 // System.out.println(); System.out.println("executing compile command: " + cmdLine); 175 try { 176 executor.execute(cmdLine, resultHandler); 177 } catch (IOException e) { 178 throw new UncheckedIOException("exception starting process: " + cmdLine, e); 179 } 180 181 int exitValue = -1; 182 try { 183 resultHandler.waitFor(); 184 exitValue = resultHandler.getExitValue(); 185 } catch (InterruptedException e) { 186 // Ignore exception, but watchdog.killedProcess() records that the process timed out. 187 } 188 boolean timedOut = executor.isFailure(exitValue) && watchdog.killedProcess(); 189 190 try { 191 @SuppressWarnings("DefaultCharset") // toString(Charset) was introduced in Java 10 192 String compile_errors_tmp = errStream.toString(); 193 compile_errors = compile_errors_tmp; 194 } catch (RuntimeException e) { 195 throw new Error("Exception getting process error output", e); 196 } 197 198 try { 199 @SuppressWarnings("DefaultCharset") // toString(Charset) was introduced in Java 10 200 String compile_output_tmp = errStream.toString(); 201 compile_output = compile_output_tmp; 202 } catch (RuntimeException e) { 203 throw new Error("Exception getting process standard output", e); 204 } 205 206 if (timedOut) { 207 // Print stderr and stdout if there is an unexpected exception (timeout). 208 System.out.println("Compile timed out after " + timeLimit + " msecs"); 209 // System.out.println ("Compile errors: " + compile_errors); 210 // System.out.println ("Compile output: " + compile_output); 211 ExecuteException e = resultHandler.getException(); 212 if (e != null) { 213 e.printStackTrace(); 214 } 215 runtime.exit(1); 216 } 217 return compile_errors; 218 } 219 220 /** 221 * Examine the errorString to identify the files that cannot compile, then recompile all the other 222 * files. This function is necessary when compiling with javac because javac does not compile all 223 * the files supplied to it if some of them contain errors. So some "good" files end up not being 224 * compiled. 225 * 226 * @param fileNames all the files that were attempted to be compiled 227 * @param errorString the error string that indicates which files could not be compiled 228 */ 229 private void recompile_without_errors(List<String> fileNames, String errorString) 230 throws IOException { 231 // search the error string and extract the files with errors. 232 if (errorString != null) { 233 HashSet<String> errorClasses = new HashSet<>(); 234 Matcher m = java_filename_pattern.matcher(errorString); 235 while (m.find()) { 236 @SuppressWarnings( 237 "nullness") // Regex Checker imprecision: find() guarantees that group 1 exists 238 @NonNull String sansExtension = m.group(1); 239 errorClasses.add(sansExtension); 240 } 241 // Collect all the files that were not compiled into retry 242 List<String> retry = new ArrayList<>(); 243 for (String sourceFileName : fileNames) { 244 sourceFileName = sourceFileName.trim(); 245 String classFilePath = getClassFilePath(sourceFileName); 246 if (!fileExists(classFilePath)) { 247 if (!errorClasses.contains(sourceFileName)) { 248 retry.add(sourceFileName); 249 } 250 } 251 } 252 253 if (retry.size() > 0) { 254 compile_source(retry); 255 } 256 } 257 } 258 259 /** 260 * Return the file path to where a class file for a source file at sourceFilePath would be 261 * generated. 262 * 263 * @param sourceFilePath the path to the .java file 264 * @return the path to the corresponding .class file 265 */ 266 private static String getClassFilePath(String sourceFilePath) { 267 int index = sourceFilePath.lastIndexOf('.'); 268 if (index == -1) { 269 throw new IllegalArgumentException( 270 "sourceFilePath: " + sourceFilePath + " must end with an extention."); 271 } 272 return sourceFilePath.substring(0, index) + ".class"; 273 } 274 275 /** 276 * Returns true if the given file exists. 277 * 278 * @param pathName path to check for existence 279 * @return true iff the file exists 280 */ 281 private static boolean fileExists(String pathName) { 282 return new File(pathName).exists(); 283 } 284}