1 /* 2 * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 import java.io.BufferedInputStream; 25 import java.io.BufferedReader; 26 import java.io.BufferedWriter; 27 import java.io.ByteArrayInputStream; 28 import java.io.ByteArrayOutputStream; 29 import java.io.File; 30 import java.io.FilterOutputStream; 31 import java.io.FilterWriter; 32 import java.io.IOError; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.InputStreamReader; 36 import java.io.OutputStream; 37 import java.io.PrintStream; 38 import java.io.PrintWriter; 39 import java.io.StringWriter; 40 import java.io.Writer; 41 import java.net.URI; 42 import java.nio.charset.Charset; 43 import java.nio.file.FileVisitResult; 44 import java.nio.file.Files; 45 import java.nio.file.Path; 46 import java.nio.file.Paths; 47 import java.nio.file.SimpleFileVisitor; 48 import java.nio.file.StandardCopyOption; 49 import java.nio.file.attribute.BasicFileAttributes; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collections; 53 import java.util.EnumMap; 54 import java.util.EnumSet; 55 import java.util.HashMap; 56 import java.util.LinkedHashSet; 57 import java.util.List; 58 import java.util.ListIterator; 59 import java.util.Locale; 60 import java.util.Map; 61 import java.util.Objects; 62 import java.util.Set; 63 import java.util.jar.Attributes; 64 import java.util.jar.JarEntry; 65 import java.util.jar.JarOutputStream; 66 import java.util.jar.Manifest; 67 import java.util.regex.Matcher; 68 import java.util.regex.Pattern; 69 import java.util.stream.Collectors; 70 import java.util.stream.Stream; 71 import java.util.stream.StreamSupport; 72 73 import javax.tools.FileObject; 74 import javax.tools.ForwardingJavaFileManager; 75 import javax.tools.JavaCompiler; 76 import javax.tools.JavaFileManager; 77 import javax.tools.JavaFileObject; 78 import javax.tools.JavaFileObject.Kind; 79 import javax.tools.JavaFileManager.Location; 80 import javax.tools.SimpleJavaFileObject; 81 import javax.tools.StandardJavaFileManager; 82 import javax.tools.StandardLocation; 83 84 import com.sun.tools.javac.api.JavacTaskImpl; 85 import com.sun.tools.javac.api.JavacTool; 86 87 /** 88 * Utility methods and classes for writing jtreg tests for 89 * javac, javah, javap, and sjavac. (For javadoc support, 90 * see JavadocTester.) 91 * 92 * <p>There is support for common file operations similar to 93 * shell commands like cat, cp, diff, mv, rm, grep. 94 * 95 * <p>There is also support for invoking various tools, like 96 * javac, javah, javap, jar, java and other JDK tools. 97 * 98 * <p><em>File separators</em>: for convenience, many operations accept strings 99 * to represent filenames. On all platforms on which JDK is supported, 100 * "/" is a legal filename component separator. In particular, even 101 * on Windows, where the official file separator is "\", "/" is a legal 102 * alternative. It is therefore recommended that any client code using 103 * strings to specify filenames should use "/". 104 * 105 * @author Vicente Romero (original) 106 * @author Jonathan Gibbons (revised) 107 */ 108 public class ToolBox { 109 /** The platform line separator. */ 110 public static final String lineSeparator = System.getProperty("line.separator"); 111 /** The platform OS name. */ 112 public static final String osName = System.getProperty("os.name"); 113 114 /** The location of the class files for this test, or null if not set. */ 115 public static final String testClasses = System.getProperty("test.classes"); 116 /** The location of the source files for this test, or null if not set. */ 117 public static final String testSrc = System.getProperty("test.src"); 118 /** The location of the test JDK for this test, or null if not set. */ 119 public static final String testJDK = System.getProperty("test.jdk"); 120 121 /** The current directory. */ 122 public static final Path currDir = Paths.get("."); 123 124 /** The stream used for logging output. */ 125 public PrintStream out = System.err; 126 127 JavaCompiler compiler; 128 StandardJavaFileManager standardJavaFileManager; 129 130 /** 131 * Checks if the host OS is some version of Windows. 132 * @return true if the host OS is some version of Windows 133 */ 134 public boolean isWindows() { 135 return osName.toLowerCase(Locale.ENGLISH).startsWith("windows"); 136 } 137 138 /** 139 * Splits a string around matches of the given regular expression. 140 * If the string is empty, an empty list will be returned. 141 * @param text the string to be split 142 * @param sep the delimiting regular expression 143 * @return the strings between the separators 144 */ 145 public List<String> split(String text, String sep) { 146 if (text.isEmpty()) 147 return Collections.emptyList(); 148 return Arrays.asList(text.split(sep)); 149 } 150 151 /** 152 * Checks if two lists of strings are equal. 153 * @param l1 the first list of strings to be compared 154 * @param l2 the second list of strings to be compared 155 * @throws Error if the lists are not equal 156 */ 157 public void checkEqual(List<String> l1, List<String> l2) throws Error { 158 if (!Objects.equals(l1, l2)) { 159 // l1 and l2 cannot both be null 160 if (l1 == null) 161 throw new Error("comparison failed: l1 is null"); 162 if (l2 == null) 163 throw new Error("comparison failed: l2 is null"); 164 // report first difference 165 for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) { 166 String s1 = l1.get(i); 167 String s2 = l1.get(i); 168 if (!Objects.equals(s1, s2)) { 169 throw new Error("comparison failed, index " + i + 170 ", (" + s1 + ":" + s2 + ")"); 171 } 172 } 173 throw new Error("comparison failed: l1.size=" + l1.size() + ", l2.size=" + l2.size()); 174 } 175 } 176 177 /** 178 * Filters a list of strings according to the given regular expression. 179 * @param regex the regular expression 180 * @param lines the strings to be filtered 181 * @return the strings matching the regular expression 182 */ 183 public List<String> grep(String regex, List<String> lines) { 184 return grep(Pattern.compile(regex), lines); 185 } 186 187 /** 188 * Filters a list of strings according to the given regular expression. 189 * @param pattern the regular expression 190 * @param lines the strings to be filtered 191 * @return the strings matching the regular expression 192 */ 193 public List<String> grep(Pattern pattern, List<String> lines) { 194 return lines.stream() 195 .filter(s -> pattern.matcher(s).find()) 196 .collect(Collectors.toList()); 197 } 198 199 /** 200 * Copies a file. 201 * If the given destination exists and is a directory, the copy is created 202 * in that directory. Otherwise, the copy will be placed at the destination, 203 * possibly overwriting any existing file. 204 * <p>Similar to the shell "cp" command: {@code cp from to}. 205 * @param from the file to be copied 206 * @param to where to copy the file 207 * @throws IOException if any error occurred while copying the file 208 */ 209 public void copyFile(String from, String to) throws IOException { 210 copyFile(Paths.get(from), Paths.get(to)); 211 } 212 213 /** 214 * Copies a file. 215 * If the given destination exists and is a directory, the copy is created 216 * in that directory. Otherwise, the copy will be placed at the destination, 217 * possibly overwriting any existing file. 218 * <p>Similar to the shell "cp" command: {@code cp from to}. 219 * @param from the file to be copied 220 * @param to where to copy the file 221 * @throws IOException if an error occurred while copying the file 222 */ 223 public void copyFile(Path from, Path to) throws IOException { 224 if (Files.isDirectory(to)) { 225 to = to.resolve(from.getFileName()); 226 } else { 227 Files.createDirectories(to.getParent()); 228 } 229 Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); 230 } 231 232 /** 233 * Creates one of more directories. 234 * For each of the series of paths, a directory will be created, 235 * including any necessary parent directories. 236 * <p>Similar to the shell command: {@code mkdir -p paths}. 237 * @param paths the directories to be created 238 * @throws IOException if an error occurred while creating the directories 239 */ 240 public void createDirectories(String... paths) throws IOException { 241 if (paths.length == 0) 242 throw new IllegalArgumentException("no directories specified"); 243 for (String p : paths) 244 Files.createDirectories(Paths.get(p)); 245 } 246 247 /** 248 * Creates one or more directories. 249 * For each of the series of paths, a directory will be created, 250 * including any necessary parent directories. 251 * <p>Similar to the shell command: {@code mkdir -p paths}. 252 * @param paths the directories to be created 253 * @throws IOException if an error occurred while creating the directories 254 */ 255 public void createDirectories(Path... paths) throws IOException { 256 if (paths.length == 0) 257 throw new IllegalArgumentException("no directories specified"); 258 for (Path p : paths) 259 Files.createDirectories(p); 260 } 261 262 /** 263 * Deletes one or more files. 264 * Any directories to be deleted must be empty. 265 * <p>Similar to the shell command: {@code rm files}. 266 * @param files the files to be deleted 267 * @throws IOException if an error occurred while deleting the files 268 */ 269 public void deleteFiles(String... files) throws IOException { 270 if (files.length == 0) 271 throw new IllegalArgumentException("no files specified"); 272 for (String file : files) 273 Files.delete(Paths.get(file)); 274 } 275 276 /** 277 * Moves a file. 278 * If the given destination exists and is a directory, the file will be moved 279 * to that directory. Otherwise, the file will be moved to the destination, 280 * possibly overwriting any existing file. 281 * <p>Similar to the shell "mv" command: {@code mv from to}. 282 * @param from the file to be moved 283 * @param to where to move the file 284 * @throws IOException if an error occurred while moving the file 285 */ 286 public void moveFile(String from, String to) throws IOException { 287 moveFile(Paths.get(from), Paths.get(to)); 288 } 289 290 /** 291 * Moves a file. 292 * If the given destination exists and is a directory, the file will be moved 293 * to that directory. Otherwise, the file will be moved to the destination, 294 * possibly overwriting any existing file. 295 * <p>Similar to the shell "mv" command: {@code mv from to}. 296 * @param from the file to be moved 297 * @param to where to move the file 298 * @throws IOException if an error occurred while moving the file 299 */ 300 public void moveFile(Path from, Path to) throws IOException { 301 if (Files.isDirectory(to)) { 302 to = to.resolve(from.getFileName()); 303 } else { 304 Files.createDirectories(to.getParent()); 305 } 306 Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); 307 } 308 309 /** 310 * Reads the lines of a file. 311 * The file is read using the default character encoding. 312 * @param path the file to be read 313 * @return the lines of the file. 314 * @throws IOException if an error occurred while reading the file 315 */ 316 public List<String> readAllLines(String path) throws IOException { 317 return readAllLines(path, null); 318 } 319 320 /** 321 * Reads the lines of a file. 322 * The file is read using the default character encoding. 323 * @param path the file to be read 324 * @return the lines of the file. 325 * @throws IOException if an error occurred while reading the file 326 */ 327 public List<String> readAllLines(Path path) throws IOException { 328 return readAllLines(path, null); 329 } 330 331 /** 332 * Reads the lines of a file using the given encoding. 333 * @param path the file to be read 334 * @param encoding the encoding to be used to read the file 335 * @return the lines of the file. 336 * @throws IOException if an error occurred while reading the file 337 */ 338 public List<String> readAllLines(String path, String encoding) throws IOException { 339 return readAllLines(Paths.get(path), encoding); 340 } 341 342 /** 343 * Reads the lines of a file using the given encoding. 344 * @param path the file to be read 345 * @param encoding the encoding to be used to read the file 346 * @return the lines of the file. 347 * @throws IOException if an error occurred while reading the file 348 */ 349 public List<String> readAllLines(Path path, String encoding) throws IOException { 350 return Files.readAllLines(path, getCharset(encoding)); 351 } 352 353 private Charset getCharset(String encoding) { 354 return (encoding == null) ? Charset.defaultCharset() : Charset.forName(encoding); 355 } 356 357 /** 358 * Writes a file containing the given content. 359 * Any necessary directories for the file will be created. 360 * @param path where to write the file 361 * @param content the content for the file 362 * @throws IOException if an error occurred while writing the file 363 */ 364 public void writeFile(String path, String content) throws IOException { 365 writeFile(Paths.get(path), content); 366 } 367 368 /** 369 * Writes a file containing the given content. 370 * Any necessary directories for the file will be created. 371 * @param path where to write the file 372 * @param content the content for the file 373 * @throws IOException if an error occurred while writing the file 374 */ 375 public void writeFile(Path path, String content) throws IOException { 376 Path dir = path.getParent(); 377 if (dir != null) 378 Files.createDirectories(dir); 379 try (BufferedWriter w = Files.newBufferedWriter(path)) { 380 w.write(content); 381 } 382 } 383 384 /** 385 * Writes one or more files containing Java source code. 386 * For each file to be written, the filename will be inferred from the 387 * given base directory, the package declaration (if present) and from the 388 * the name of the first class, interface or enum declared in the file. 389 * <p>For example, if the base directory is /my/dir/ and the content 390 * contains "package p; class C { }", the file will be written to 391 * /my/dir/p/C.java. 392 * <p>Note: the content is analyzed using regular expressions; 393 * errors can occur if any contents have initial comments that might trip 394 * up the analysis. 395 * @param dir the base directory 396 * @param contents the contents of the files to be written 397 * @throws IOException if an error occurred while writing any of the files. 398 */ 399 public void writeJavaFiles(Path dir, String... contents) throws IOException { 400 if (contents.length == 0) 401 throw new IllegalArgumentException("no content specified for any files"); 402 for (String c : contents) { 403 new JavaSource(c).write(dir); 404 } 405 } 406 407 /** 408 * Returns the path for the binary of a JDK tool within {@link testJDK}. 409 * @param tool the name of the tool 410 * @return the path of the tool 411 */ 412 public Path getJDKTool(String tool) { 413 return Paths.get(testJDK, "bin", tool); 414 } 415 416 /** 417 * Returns a string representing the contents of an {@code Iterable} as a list. 418 * @param <T> the type parameter of the {@code Iterable} 419 * @param items the iterable 420 * @return the string 421 */ 422 <T> String toString(Iterable<T> items) { 423 return StreamSupport.stream(items.spliterator(), false) 424 .map(Objects::toString) 425 .collect(Collectors.joining(",", "[", "]")); 426 } 427 428 /** 429 * The supertype for tasks. 430 * Complex operations are modelled by building and running a "Task" object. 431 * Tasks are typically configured in a fluent series of calls. 432 */ 433 public interface Task { 434 /** 435 * Returns the name of the task. 436 * @return the name of the task 437 */ 438 String name(); 439 440 /** 441 * Executes the task as currently configured. 442 * @return a Result object containing the results of running the task 443 * @throws TaskError if the outcome of the task was not as expected 444 */ 445 Result run() throws TaskError; 446 } 447 448 /** 449 * Exception thrown by {@code Task.run} when the outcome is not as 450 * expected. 451 */ 452 public static class TaskError extends Error { 453 /** 454 * Creates a TaskError object with the given message. 455 * @param message the message 456 */ 457 public TaskError(String message) { 458 super(message); 459 } 460 } 461 462 /** 463 * An enum to indicate the mode a task should use it is when executed. 464 */ 465 public enum Mode { 466 /** 467 * The task should use the interface used by the command 468 * line launcher for the task. 469 * For example, for javac: com.sun.tools.javac.Main.compile 470 */ 471 CMDLINE, 472 /** 473 * The task should use a publicly defined API for the task. 474 * For example, for javac: javax.tools.JavaCompiler 475 */ 476 API, 477 /** 478 * The task should use the standard launcher for the task. 479 * For example, $JAVA_HOME/bin/javac 480 */ 481 EXEC 482 } 483 484 /** 485 * An enum to indicate the expected success or failure of executing a task. 486 */ 487 public enum Expect { 488 /** It is expected that the task will complete successfully. */ 489 SUCCESS, 490 /** It is expected that the task will not complete successfully. */ 491 FAIL 492 } 493 494 /** 495 * An enum to identify the streams that may be written by a {@code Task}. 496 */ 497 public enum OutputKind { 498 /** Identifies output written to {@code System.out} or {@code stdout}. */ 499 STDOUT, 500 /** Identifies output written to {@code System.err} or {@code stderr}. */ 501 STDERR, 502 /** Identifies output written to a stream provided directly to the task. */ 503 DIRECT 504 }; 505 506 /** 507 * The results from running a {@link Task}. 508 * The results contain the exit code returned when the tool was invoked, 509 * and a map containing the output written to any streams during the 510 * execution of the tool. 511 * All tools support "stdout" and "stderr". 512 * Tools that take an explicit PrintWriter save output written to that 513 * stream as "main". 514 */ 515 public class Result { 516 517 final Task task; 518 final int exitCode; 519 final Map<OutputKind, String> outputMap; 520 521 Result(Task task, int exitCode, Map<OutputKind, String> outputMap) { 522 this.task = task; 523 this.exitCode = exitCode; 524 this.outputMap = outputMap; 525 } 526 527 /** 528 * Returns the content of a specified stream. 529 * @param outputKind the kind of the selected stream 530 * @return the content that was written to that stream when the tool 531 * was executed. 532 */ 533 public String getOutput(OutputKind outputKind) { 534 return outputMap.get(outputKind); 535 } 536 537 /** 538 * Returns the content of a named stream as a list of lines. 539 * @param outputKind the kind of the selected stream 540 * @return the content that was written to that stream when the tool 541 * was executed. 542 */ 543 public List<String> getOutputLines(OutputKind outputKind) { 544 return Arrays.asList(outputMap.get(outputKind).split(lineSeparator)); 545 } 546 547 /** 548 * Writes the content of the specified stream to the log. 549 * @param kind the kind of the selected stream 550 * @return this Result object 551 */ 552 public Result write(OutputKind kind) { 553 String text = getOutput(kind); 554 if (text == null || text.isEmpty()) 555 out.println("[" + task.name() + ":" + kind + "]: empty"); 556 else { 557 out.println("[" + task.name() + ":" + kind + "]:"); 558 out.print(text); 559 } 560 return this; 561 } 562 563 /** 564 * Writes the content of all streams with any content to the log. 565 * @return this Result object 566 */ 567 public Result writeAll() { 568 outputMap.forEach((name, text) -> { 569 if (!text.isEmpty()) { 570 out.println("[" + name + "]:"); 571 out.print(text); 572 } 573 }); 574 return this; 575 } 576 } 577 578 /** 579 * A utility base class to simplify the implementation of tasks. 580 * Provides support for running the task in a process and for 581 * capturing output written by the task to stdout, stderr and 582 * other writers where applicable. 583 * @param <T> the implementing subclass 584 */ 585 protected static abstract class AbstractTask<T extends AbstractTask<T>> implements Task { 586 protected final Mode mode; 587 private final Map<OutputKind, String> redirects = new EnumMap<>(OutputKind.class); 588 private final Map<String, String> envVars = new HashMap<>(); 589 private Expect expect = Expect.SUCCESS; 590 int expectedExitCode = 0; 591 592 /** 593 * Create a task that will execute in the specified mode. 594 * @param mode the mode 595 */ 596 protected AbstractTask(Mode mode) { 597 this.mode = mode; 598 } 599 600 /** 601 * Sets the expected outcome of the task and calls {@code run()}. 602 * @param expect the expected outcome 603 * @return the result of calling {@code run()} 604 */ 605 public Result run(Expect expect) { 606 expect(expect, Integer.MIN_VALUE); 607 return run(); 608 } 609 610 /** 611 * Sets the expected outcome of the task and calls {@code run()}. 612 * @param expect the expected outcome 613 * @param exitCode the expected exit code if the expected outcome 614 * is {@code FAIL} 615 * @return the result of calling {@code run()} 616 */ 617 public Result run(Expect expect, int exitCode) { 618 expect(expect, exitCode); 619 return run(); 620 } 621 622 /** 623 * Sets the expected outcome and expected exit code of the task. 624 * The exit code will not be checked if the outcome is 625 * {@code Expect.SUCCESS} or if the exit code is set to 626 * {@code Integer.MIN_VALUE}. 627 * @param expect the expected outcome 628 * @param exitCode the expected exit code 629 */ 630 protected void expect(Expect expect, int exitCode) { 631 this.expect = expect; 632 this.expectedExitCode = exitCode; 633 } 634 635 /** 636 * Checks the exit code contained in a {@code Result} against the 637 * expected outcome and exit value 638 * @param result the result object 639 * @return the result object 640 * @throws TaskError if the exit code stored in the result object 641 * does not match the expected outcome and exit code. 642 */ 643 protected Result checkExit(Result result) throws TaskError { 644 switch (expect) { 645 case SUCCESS: 646 if (result.exitCode != 0) { 647 result.writeAll(); 648 throw new TaskError("Task " + name() + " failed: rc=" + result.exitCode); 649 } 650 break; 651 652 case FAIL: 653 if (result.exitCode == 0) { 654 result.writeAll(); 655 throw new TaskError("Task " + name() + " succeeded unexpectedly"); 656 } 657 658 if (expectedExitCode != Integer.MIN_VALUE 659 && result.exitCode != expectedExitCode) { 660 result.writeAll(); 661 throw new TaskError("Task " + name() + "failed with unexpected exit code " 662 + result.exitCode + ", expected " + expectedExitCode); 663 } 664 break; 665 } 666 return result; 667 } 668 669 /** 670 * Sets an environment variable to be used by this task. 671 * @param name the name of the environment variable 672 * @param value the value for the environment variable 673 * @return this task object 674 * @throws IllegalStateException if the task mode is not {@code EXEC} 675 */ 676 protected T envVar(String name, String value) { 677 if (mode != Mode.EXEC) 678 throw new IllegalStateException(); 679 envVars.put(name, value); 680 return (T) this; 681 } 682 683 /** 684 * Redirects output from an output stream to a file. 685 * @param outputKind the name of the stream to be redirected. 686 * @param path the file 687 * @return this task object 688 * @throws IllegalStateException if the task mode is not {@code EXEC} 689 */ 690 protected T redirect(OutputKind outputKind, String path) { 691 if (mode != Mode.EXEC) 692 throw new IllegalStateException(); 693 redirects.put(outputKind, path); 694 return (T) this; 695 } 696 697 /** 698 * Returns a {@code ProcessBuilder} initialized with any 699 * redirects and environment variables that have been set. 700 * @return a {@code ProcessBuilder} 701 */ 702 protected ProcessBuilder getProcessBuilder() { 703 if (mode != Mode.EXEC) 704 throw new IllegalStateException(); 705 ProcessBuilder pb = new ProcessBuilder(); 706 if (redirects.get(OutputKind.STDOUT) != null) 707 pb.redirectOutput(new File(redirects.get(OutputKind.STDOUT))); 708 if (redirects.get(OutputKind.STDERR) != null) 709 pb.redirectError(new File(redirects.get(OutputKind.STDERR))); 710 pb.environment().putAll(envVars); 711 return pb; 712 } 713 714 /** 715 * Collects the output from a process and saves it in a {@code Result}. 716 * @param tb the {@code ToolBox} containing the task {@code t} 717 * @param t the task initiating the process 718 * @param p the process 719 * @return a Result object containing the output from the process and its 720 * exit value. 721 * @throws InterruptedException if the thread is interrupted 722 */ 723 protected Result runProcess(ToolBox tb, Task t, Process p) throws InterruptedException { 724 if (mode != Mode.EXEC) 725 throw new IllegalStateException(); 726 ProcessOutput sysOut = new ProcessOutput(p.getInputStream()).start(); 727 ProcessOutput sysErr = new ProcessOutput(p.getErrorStream()).start(); 728 sysOut.waitUntilDone(); 729 sysErr.waitUntilDone(); 730 int rc = p.waitFor(); 731 Map<OutputKind, String> outputMap = new EnumMap<>(OutputKind.class); 732 outputMap.put(OutputKind.STDOUT, sysOut.getOutput()); 733 outputMap.put(OutputKind.STDERR, sysErr.getOutput()); 734 return checkExit(tb.new Result(t, rc, outputMap)); 735 } 736 737 /** 738 * Thread-friendly class to read the output from a process until the stream 739 * is exhausted. 740 */ 741 static class ProcessOutput implements Runnable { 742 ProcessOutput(InputStream from) { 743 in = new BufferedReader(new InputStreamReader(from)); 744 out = new StringBuilder(); 745 } 746 747 ProcessOutput start() { 748 new Thread(this).start(); 749 return this; 750 } 751 752 @Override 753 public void run() { 754 try { 755 String line; 756 while ((line = in.readLine()) != null) { 757 out.append(line).append(lineSeparator); 758 } 759 } catch (IOException e) { 760 } 761 synchronized (this) { 762 done = true; 763 notifyAll(); 764 } 765 } 766 767 synchronized void waitUntilDone() throws InterruptedException { 768 boolean interrupted = false; 769 770 // poll interrupted flag, while waiting for copy to complete 771 while (!(interrupted = Thread.interrupted()) && !done) 772 wait(1000); 773 774 if (interrupted) 775 throw new InterruptedException(); 776 } 777 778 String getOutput() { 779 return out.toString(); 780 } 781 782 private BufferedReader in; 783 private final StringBuilder out; 784 private boolean done; 785 } 786 787 /** 788 * Utility class to simplify the handling of temporarily setting a 789 * new stream for System.out or System.err. 790 */ 791 static class StreamOutput { 792 // Functional interface to set a stream. 793 // Expected use: System::setOut, System::setErr 794 private interface Initializer { 795 void set(PrintStream s); 796 } 797 798 private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 799 private final PrintStream ps = new PrintStream(baos); 800 private final PrintStream prev; 801 private final Initializer init; 802 803 StreamOutput(PrintStream s, Initializer init) { 804 prev = s; 805 init.set(ps); 806 this.init = init; 807 } 808 809 /** 810 * Closes the stream and returns the contents that were written to it. 811 * @return the contents that were written to it. 812 */ 813 String close() { 814 init.set(prev); 815 ps.close(); 816 return baos.toString(); 817 } 818 } 819 820 /** 821 * Utility class to simplify the handling of creating an in-memory PrintWriter. 822 */ 823 static class WriterOutput { 824 private final StringWriter sw = new StringWriter(); 825 final PrintWriter pw = new PrintWriter(sw); 826 827 /** 828 * Closes the stream and returns the contents that were written to it. 829 * @return the contents that were written to it. 830 */ 831 String close() { 832 pw.close(); 833 return sw.toString(); 834 } 835 } 836 } 837 838 /** 839 * A task to configure and run the Java compiler, javac. 840 */ 841 public class JavacTask extends AbstractTask<JavacTask> { 842 private boolean includeStandardOptions; 843 private String classpath; 844 private String sourcepath; 845 private String outdir; 846 private List<String> options; 847 private List<String> classes; 848 private List<String> files; 849 private List<JavaFileObject> fileObjects; 850 private JavaFileManager fileManager; 851 852 /** 853 * Creates a task to execute {@code javac} using API mode. 854 */ 855 public JavacTask() { 856 super(Mode.API); 857 } 858 859 /** 860 * Creates a task to execute {@code javac} in a specified mode. 861 * @param mode the mode to be used 862 */ 863 public JavacTask(Mode mode) { 864 super(mode); 865 } 866 867 /** 868 * Sets the classpath. 869 * @param classpath the classpath 870 * @return this task object 871 */ 872 public JavacTask classpath(String classpath) { 873 this.classpath = classpath; 874 return this; 875 } 876 877 /** 878 * Sets the sourcepath. 879 * @param sourcepath the sourcepath 880 * @return this task object 881 */ 882 public JavacTask sourcepath(String sourcepath) { 883 this.sourcepath = sourcepath; 884 return this; 885 } 886 887 /** 888 * Sets the output directory. 889 * @param outdir the output directory 890 * @return this task object 891 */ 892 public JavacTask outdir(String outdir) { 893 this.outdir = outdir; 894 return this; 895 } 896 897 /** 898 * Sets the options. 899 * @param options the options 900 * @return this task object 901 */ 902 public JavacTask options(String... options) { 903 this.options = Arrays.asList(options); 904 return this; 905 } 906 907 /** 908 * Sets the classes to be analyzed. 909 * @param classes the classes 910 * @return this task object 911 */ 912 public JavacTask classes(String... classes) { 913 this.classes = Arrays.asList(classes); 914 return this; 915 } 916 917 /** 918 * Sets the files to be compiled or analyzed. 919 * @param files the files 920 * @return this task object 921 */ 922 public JavacTask files(String... files) { 923 this.files = Arrays.asList(files); 924 return this; 925 } 926 927 /** 928 * Sets the files to be compiled or analyzed. 929 * @param files the files 930 * @return this task object 931 */ 932 public JavacTask files(Path... files) { 933 this.files = Stream.of(files) 934 .map(Path::toString) 935 .collect(Collectors.toList()); 936 return this; 937 } 938 939 /** 940 * Sets the sources to be compiled or analyzed. 941 * Each source string is converted into an in-memory object that 942 * can be passed directly to the compiler. 943 * @param sources the sources 944 * @return this task object 945 */ 946 public JavacTask sources(String... sources) { 947 fileObjects = Stream.of(sources) 948 .map(s -> new JavaSource(s)) 949 .collect(Collectors.toList()); 950 return this; 951 } 952 953 /** 954 * Sets the file manager to be used by this task. 955 * @param fileManager the file manager 956 * @return this task object 957 */ 958 public JavacTask fileManager(JavaFileManager fileManager) { 959 this.fileManager = fileManager; 960 return this; 961 } 962 963 /** 964 * {@inheritDoc} 965 * @return the name "javac" 966 */ 967 @Override 968 public String name() { 969 return "javac"; 970 } 971 972 /** 973 * Calls the compiler with the arguments as currently configured. 974 * @return a Result object indicating the outcome of the compilation 975 * and the content of any output written to stdout, stderr, or the 976 * main stream by the compiler. 977 * @throws TaskError if the outcome of the task is not as expected. 978 */ 979 @Override 980 public Result run() { 981 if (mode == Mode.EXEC) 982 return runExec(); 983 984 WriterOutput direct = new WriterOutput(); 985 // The following are to catch output to System.out and System.err, 986 // in case these are used instead of the primary (main) stream 987 StreamOutput sysOut = new StreamOutput(System.out, System::setOut); 988 StreamOutput sysErr = new StreamOutput(System.err, System::setErr); 989 int rc; 990 Map<OutputKind, String> outputMap = new HashMap<>(); 991 try { 992 switch (mode == null ? Mode.API : mode) { 993 case API: 994 rc = runAPI(direct.pw); 995 break; 996 case CMDLINE: 997 rc = runCommand(direct.pw); 998 break; 999 default: 1000 throw new IllegalStateException(); 1001 } 1002 } catch (IOException e) { 1003 out.println("Exception occurred: " + e); 1004 rc = 99; 1005 } finally { 1006 outputMap.put(OutputKind.STDOUT, sysOut.close()); 1007 outputMap.put(OutputKind.STDERR, sysErr.close()); 1008 outputMap.put(OutputKind.DIRECT, direct.close()); 1009 } 1010 return checkExit(new Result(this, rc, outputMap)); 1011 } 1012 1013 private int runAPI(PrintWriter pw) throws IOException { 1014 // if (compiler == null) { 1015 // TODO: allow this to be set externally 1016 // compiler = ToolProvider.getSystemJavaCompiler(); 1017 compiler = JavacTool.create(); 1018 // } 1019 1020 if (fileManager == null) 1021 fileManager = compiler.getStandardFileManager(null, null, null); 1022 if (outdir != null) 1023 setLocation(StandardLocation.CLASS_OUTPUT, toFiles(outdir)); 1024 if (classpath != null) 1025 setLocation(StandardLocation.CLASS_PATH, toFiles(classpath)); 1026 if (sourcepath != null) 1027 setLocation(StandardLocation.SOURCE_PATH, toFiles(sourcepath)); 1028 List<String> allOpts = new ArrayList<>(); 1029 if (options != null) 1030 allOpts.addAll(options); 1031 1032 Iterable<? extends JavaFileObject> allFiles = joinFiles(files, fileObjects); 1033 JavaCompiler.CompilationTask task = compiler.getTask(pw, 1034 fileManager, 1035 null, // diagnostic listener; should optionally collect diags 1036 allOpts, 1037 classes, 1038 allFiles); 1039 return ((JavacTaskImpl) task).doCall().exitCode; 1040 } 1041 1042 private void setLocation(StandardLocation location, List<File> files) throws IOException { 1043 if (!(fileManager instanceof StandardJavaFileManager)) 1044 throw new IllegalStateException("not a StandardJavaFileManager"); 1045 ((StandardJavaFileManager) fileManager).setLocation(location, files); 1046 } 1047 1048 private int runCommand(PrintWriter pw) { 1049 List<String> args = getAllArgs(); 1050 String[] argsArray = args.toArray(new String[args.size()]); 1051 return com.sun.tools.javac.Main.compile(argsArray, pw); 1052 } 1053 1054 private Result runExec() { 1055 List<String> args = new ArrayList<>(); 1056 Path javac = getJDKTool("javac"); 1057 args.add(javac.toString()); 1058 if (includeStandardOptions) { 1059 args.addAll(split(System.getProperty("test.tool.vm.opts"), " +")); 1060 args.addAll(split(System.getProperty("test.compiler.opts"), " +")); 1061 } 1062 args.addAll(getAllArgs()); 1063 1064 String[] argsArray = args.toArray(new String[args.size()]); 1065 ProcessBuilder pb = getProcessBuilder(); 1066 pb.command(argsArray); 1067 try { 1068 return runProcess(ToolBox.this, this, pb.start()); 1069 } catch (IOException | InterruptedException e) { 1070 throw new Error(e); 1071 } 1072 } 1073 1074 private List<String> getAllArgs() { 1075 List<String> args = new ArrayList<>(); 1076 if (options != null) 1077 args.addAll(options); 1078 if (outdir != null) { 1079 args.add("-d"); 1080 args.add(outdir); 1081 } 1082 if (classpath != null) { 1083 args.add("-classpath"); 1084 args.add(classpath); 1085 } 1086 if (sourcepath != null) { 1087 args.add("-sourcepath"); 1088 args.add(sourcepath); 1089 } 1090 if (classes != null) 1091 args.addAll(classes); 1092 if (files != null) 1093 args.addAll(files); 1094 1095 return args; 1096 } 1097 1098 private List<File> toFiles(String path) { 1099 List<File> result = new ArrayList<>(); 1100 for (String s : path.split(File.pathSeparator)) { 1101 if (!s.isEmpty()) 1102 result.add(new File(s)); 1103 } 1104 return result; 1105 } 1106 1107 private Iterable<? extends JavaFileObject> joinFiles( 1108 List<String> files, List<JavaFileObject> fileObjects) { 1109 if (files == null) 1110 return fileObjects; 1111 if (standardJavaFileManager == null) 1112 standardJavaFileManager = compiler.getStandardFileManager(null, null, null); 1113 Iterable<? extends JavaFileObject> filesAsFileObjects = 1114 standardJavaFileManager.getJavaFileObjectsFromStrings(files); 1115 if (fileObjects == null) 1116 return filesAsFileObjects; 1117 List<JavaFileObject> combinedList = new ArrayList<>(); 1118 for (JavaFileObject o : filesAsFileObjects) 1119 combinedList.add(o); 1120 combinedList.addAll(fileObjects); 1121 return combinedList; 1122 } 1123 } 1124 1125 /** 1126 * A task to configure and run the native header tool, javah. 1127 */ 1128 public class JavahTask extends AbstractTask<JavahTask> { 1129 private String classpath; 1130 private List<String> options; 1131 private List<String> classes; 1132 1133 /** 1134 * Create a task to execute {@code javah} using {@code CMDLINE} mode. 1135 */ 1136 public JavahTask() { 1137 super(Mode.CMDLINE); 1138 } 1139 1140 /** 1141 * Sets the classpath. 1142 * @param classpath the classpath 1143 * @return this task object 1144 */ 1145 public JavahTask classpath(String classpath) { 1146 this.classpath = classpath; 1147 return this; 1148 } 1149 1150 /** 1151 * Sets the options. 1152 * @param options the options 1153 * @return this task object 1154 */ 1155 public JavahTask options(String... options) { 1156 this.options = Arrays.asList(options); 1157 return this; 1158 } 1159 1160 /** 1161 * Sets the classes to be analyzed. 1162 * @param classes the classes 1163 * @return this task object 1164 */ 1165 public JavahTask classes(String... classes) { 1166 this.classes = Arrays.asList(classes); 1167 return this; 1168 } 1169 1170 /** 1171 * {@inheritDoc} 1172 * @return the name "javah" 1173 */ 1174 @Override 1175 public String name() { 1176 return "javah"; 1177 } 1178 1179 /** 1180 * Calls the javah tool with the arguments as currently configured. 1181 * @return a Result object indicating the outcome of the task 1182 * and the content of any output written to stdout, stderr, or the 1183 * main stream provided to the task. 1184 * @throws TaskError if the outcome of the task is not as expected. 1185 */ 1186 @Override 1187 public Result run() { 1188 List<String> args = new ArrayList<>(); 1189 if (options != null) 1190 args.addAll(options); 1191 if (classpath != null) { 1192 args.add("-classpath"); 1193 args.add(classpath); 1194 } 1195 if (classes != null) 1196 args.addAll(classes); 1197 1198 WriterOutput direct = new WriterOutput(); 1199 // These are to catch output to System.out and System.err, 1200 // in case these are used instead of the primary streams 1201 StreamOutput sysOut = new StreamOutput(System.out, System::setOut); 1202 StreamOutput sysErr = new StreamOutput(System.err, System::setErr); 1203 int rc; 1204 Map<OutputKind, String> outputMap = new HashMap<>(); 1205 try { 1206 rc = com.sun.tools.javah.Main.run(args.toArray(new String[args.size()]), direct.pw); 1207 } finally { 1208 outputMap.put(OutputKind.STDOUT, sysOut.close()); 1209 outputMap.put(OutputKind.STDERR, sysErr.close()); 1210 outputMap.put(OutputKind.DIRECT, direct.close()); 1211 } 1212 return checkExit(new Result(this, rc, outputMap)); 1213 } 1214 } 1215 1216 /** 1217 * A task to configure and run the disassembler tool, javap. 1218 */ 1219 public class JavapTask extends AbstractTask<JavapTask> { 1220 private String classpath; 1221 private List<String> options; 1222 private List<String> classes; 1223 1224 /** 1225 * Create a task to execute {@code javap} using {@code CMDLINE} mode. 1226 */ 1227 public JavapTask() { 1228 super(Mode.CMDLINE); 1229 } 1230 1231 /** 1232 * Sets the classpath. 1233 * @param classpath the classpath 1234 * @return this task object 1235 */ 1236 public JavapTask classpath(String classpath) { 1237 this.classpath = classpath; 1238 return this; 1239 } 1240 1241 /** 1242 * Sets the options. 1243 * @param options the options 1244 * @return this task object 1245 */ 1246 public JavapTask options(String... options) { 1247 this.options = Arrays.asList(options); 1248 return this; 1249 } 1250 1251 /** 1252 * Sets the classes to be analyzed. 1253 * @param classes the classes 1254 * @return this task object 1255 */ 1256 public JavapTask classes(String... classes) { 1257 this.classes = Arrays.asList(classes); 1258 return this; 1259 } 1260 1261 /** 1262 * {@inheritDoc} 1263 * @return the name "javap" 1264 */ 1265 @Override 1266 public String name() { 1267 return "javap"; 1268 } 1269 1270 /** 1271 * Calls the javap tool with the arguments as currently configured. 1272 * @return a Result object indicating the outcome of the task 1273 * and the content of any output written to stdout, stderr, or the 1274 * main stream. 1275 * @throws TaskError if the outcome of the task is not as expected. 1276 */ 1277 @Override 1278 public Result run() { 1279 List<String> args = new ArrayList<>(); 1280 if (options != null) 1281 args.addAll(options); 1282 if (classpath != null) { 1283 args.add("-classpath"); 1284 args.add(classpath); 1285 } 1286 if (classes != null) 1287 args.addAll(classes); 1288 1289 WriterOutput direct = new WriterOutput(); 1290 // These are to catch output to System.out and System.err, 1291 // in case these are used instead of the primary streams 1292 StreamOutput sysOut = new StreamOutput(System.out, System::setOut); 1293 StreamOutput sysErr = new StreamOutput(System.err, System::setErr); 1294 1295 int rc; 1296 Map<OutputKind, String> outputMap = new HashMap<>(); 1297 try { 1298 rc = com.sun.tools.javap.Main.run(args.toArray(new String[args.size()]), direct.pw); 1299 } finally { 1300 outputMap.put(OutputKind.STDOUT, sysOut.close()); 1301 outputMap.put(OutputKind.STDERR, sysErr.close()); 1302 outputMap.put(OutputKind.DIRECT, direct.close()); 1303 } 1304 return checkExit(new Result(this, rc, outputMap)); 1305 } 1306 } 1307 1308 /** 1309 * A task to configure and run the jar file utility. 1310 */ 1311 public class JarTask extends AbstractTask<JarTask> { 1312 private Path jar; 1313 private Manifest manifest; 1314 private String classpath; 1315 private String mainClass; 1316 private Path baseDir; 1317 private List<Path> paths; 1318 private Set<FileObject> fileObjects; 1319 1320 /** 1321 * Creates a task to write jar files, using API mode. 1322 */ 1323 public JarTask() { 1324 super(Mode.API); 1325 paths = Collections.emptyList(); 1326 fileObjects = new LinkedHashSet<>(); 1327 } 1328 1329 /** 1330 * Creates a JarTask for use with a given jar file. 1331 * @param path the file 1332 */ 1333 public JarTask(String path) { 1334 this(); 1335 jar = Paths.get(path); 1336 } 1337 1338 /** 1339 * Sets a manifest for the jar file. 1340 * @param manifest the manifest 1341 * @return this task object 1342 */ 1343 public JarTask manifest(Manifest manifest) { 1344 this.manifest = manifest; 1345 return this; 1346 } 1347 1348 /** 1349 * Sets a manifest for the jar file. 1350 * @param manifest a string containing the contents of the manifest 1351 * @return this task object 1352 * @throws IOException if there is a problem creating the manifest 1353 */ 1354 public JarTask manifest(String manifest) throws IOException { 1355 this.manifest = new Manifest(new ByteArrayInputStream(manifest.getBytes())); 1356 return this; 1357 } 1358 1359 /** 1360 * Sets the classpath to be written to the {@code Class-Path} 1361 * entry in the manifest. 1362 * @param classpath the classpath 1363 * @return this task object 1364 */ 1365 public JarTask classpath(String classpath) { 1366 this.classpath = classpath; 1367 return this; 1368 } 1369 1370 /** 1371 * Sets the class to be written to the {@code Main-Class} 1372 * entry in the manifest.. 1373 * @param mainClass the name of the main class 1374 * @return this task object 1375 */ 1376 public JarTask mainClass(String mainClass) { 1377 this.mainClass = mainClass; 1378 return this; 1379 } 1380 1381 /** 1382 * Sets the base directory for files to be written into the jar file. 1383 * @param baseDir the base directory 1384 * @return this task object 1385 */ 1386 public JarTask baseDir(String baseDir) { 1387 this.baseDir = Paths.get(baseDir); 1388 return this; 1389 } 1390 1391 /** 1392 * Sets the files to be written into the jar file. 1393 * @param files the files 1394 * @return this task object 1395 */ 1396 public JarTask files(String... files) { 1397 this.paths = Stream.of(files) 1398 .map(file -> Paths.get(file)) 1399 .collect(Collectors.toList()); 1400 return this; 1401 } 1402 1403 /** 1404 * Adds a set of file objects to be written into the jar file, by copying them 1405 * from a Location in a JavaFileManager. 1406 * The file objects to be written are specified by a series of paths; 1407 * each path can be in one of the following forms: 1408 * <ul> 1409 * <li>The name of a class. For example, java.lang.Object. 1410 * In this case, the corresponding .class file will be written to the jar file. 1411 * <li>the name of a package followed by {@code .*}. For example, {@code java.lang.*}. 1412 * In this case, all the class files in the specified package will be written to 1413 * the jar file. 1414 * <li>the name of a package followed by {@code .**}. For example, {@code java.lang.**}. 1415 * In this case, all the class files in the specified package, and any subpackages 1416 * will be written to the jar file. 1417 * </ul> 1418 * 1419 * @param fm the file manager in which to find the file objects 1420 * @param l the location in which to find the file objects 1421 * @param paths the paths specifying the file objects to be copied 1422 * @return this task object 1423 * @throws IOException if errors occur while determining the set of file objects 1424 */ 1425 public JarTask files(JavaFileManager fm, Location l, String... paths) 1426 throws IOException { 1427 for (String p : paths) { 1428 if (p.endsWith(".**")) 1429 addPackage(fm, l, p.substring(0, p.length() - 3), true); 1430 else if (p.endsWith(".*")) 1431 addPackage(fm, l, p.substring(0, p.length() - 2), false); 1432 else 1433 addFile(fm, l, p); 1434 } 1435 return this; 1436 } 1437 1438 private void addPackage(JavaFileManager fm, Location l, String pkg, boolean recurse) 1439 throws IOException { 1440 for (JavaFileObject fo : fm.list(l, pkg, EnumSet.allOf(JavaFileObject.Kind.class), recurse)) { 1441 fileObjects.add(fo); 1442 } 1443 } 1444 1445 private void addFile(JavaFileManager fm, Location l, String path) throws IOException { 1446 JavaFileObject fo = fm.getJavaFileForInput(l, path, Kind.CLASS); 1447 fileObjects.add(fo); 1448 } 1449 1450 /** 1451 * Provides limited jar command-like functionality. 1452 * The supported commands are: 1453 * <ul> 1454 * <li> jar cf jarfile -C dir files... 1455 * <li> jar cfm jarfile manifestfile -C dir files... 1456 * </ul> 1457 * Any values specified by other configuration methods will be ignored. 1458 * @param args arguments in the style of those for the jar command 1459 * @return a Result object containing the results of running the task 1460 */ 1461 public Result run(String... args) { 1462 if (args.length < 2) 1463 throw new IllegalArgumentException(); 1464 1465 ListIterator<String> iter = Arrays.asList(args).listIterator(); 1466 String first = iter.next(); 1467 switch (first) { 1468 case "cf": 1469 jar = Paths.get(iter.next()); 1470 break; 1471 case "cfm": 1472 jar = Paths.get(iter.next()); 1473 try (InputStream in = Files.newInputStream(Paths.get(iter.next()))) { 1474 manifest = new Manifest(in); 1475 } catch (IOException e) { 1476 throw new IOError(e); 1477 } 1478 break; 1479 } 1480 1481 if (iter.hasNext()) { 1482 if (iter.next().equals("-C")) 1483 baseDir = Paths.get(iter.next()); 1484 else 1485 iter.previous(); 1486 } 1487 1488 paths = new ArrayList<>(); 1489 while (iter.hasNext()) 1490 paths.add(Paths.get(iter.next())); 1491 1492 return run(); 1493 } 1494 1495 /** 1496 * {@inheritDoc} 1497 * @return the name "jar" 1498 */ 1499 @Override 1500 public String name() { 1501 return "jar"; 1502 } 1503 1504 /** 1505 * Creates a jar file with the arguments as currently configured. 1506 * @return a Result object indicating the outcome of the compilation 1507 * and the content of any output written to stdout, stderr, or the 1508 * main stream by the compiler. 1509 * @throws TaskError if the outcome of the task is not as expected. 1510 */ 1511 @Override 1512 public Result run() { 1513 Manifest m = (manifest == null) ? new Manifest() : manifest; 1514 Attributes mainAttrs = m.getMainAttributes(); 1515 if (mainClass != null) 1516 mainAttrs.put(Attributes.Name.MAIN_CLASS, mainClass); 1517 if (classpath != null) 1518 mainAttrs.put(Attributes.Name.CLASS_PATH, classpath); 1519 1520 StreamOutput sysOut = new StreamOutput(System.out, System::setOut); 1521 StreamOutput sysErr = new StreamOutput(System.err, System::setErr); 1522 1523 Map<OutputKind, String> outputMap = new HashMap<>(); 1524 1525 try (OutputStream os = Files.newOutputStream(jar); 1526 JarOutputStream jos = openJar(os, m)) { 1527 writeFiles(jos); 1528 writeFileObjects(jos); 1529 } catch (IOException e) { 1530 error("Exception while opening " + jar, e); 1531 } finally { 1532 outputMap.put(OutputKind.STDOUT, sysOut.close()); 1533 outputMap.put(OutputKind.STDERR, sysErr.close()); 1534 } 1535 return checkExit(new Result(this, (errors == 0) ? 0 : 1, outputMap)); 1536 } 1537 1538 private JarOutputStream openJar(OutputStream os, Manifest m) throws IOException { 1539 if (m == null || m.getMainAttributes().isEmpty() && m.getEntries().isEmpty()) { 1540 return new JarOutputStream(os); 1541 } else { 1542 if (m.getMainAttributes().get(Attributes.Name.MANIFEST_VERSION) == null) 1543 m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); 1544 return new JarOutputStream(os, m); 1545 } 1546 } 1547 1548 private void writeFiles(JarOutputStream jos) throws IOException { 1549 Path base = (baseDir == null) ? currDir : baseDir; 1550 for (Path path : paths) { 1551 Files.walkFileTree(base.resolve(path), new SimpleFileVisitor<Path>() { 1552 @Override 1553 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 1554 try { 1555 String p = base.relativize(file) 1556 .normalize() 1557 .toString() 1558 .replace(File.separatorChar, '/'); 1559 JarEntry e = new JarEntry(p); 1560 jos.putNextEntry(e); 1561 try { 1562 jos.write(Files.readAllBytes(file)); 1563 } finally { 1564 jos.closeEntry(); 1565 } 1566 return FileVisitResult.CONTINUE; 1567 } catch (IOException e) { 1568 error("Exception while adding " + file + " to jar file", e); 1569 return FileVisitResult.TERMINATE; 1570 } 1571 } 1572 }); 1573 } 1574 } 1575 1576 private void writeFileObjects(JarOutputStream jos) throws IOException { 1577 for (FileObject fo : fileObjects) { 1578 String p = guessPath(fo); 1579 JarEntry e = new JarEntry(p); 1580 jos.putNextEntry(e); 1581 try { 1582 byte[] buf = new byte[1024]; 1583 try (BufferedInputStream in = new BufferedInputStream(fo.openInputStream())) { 1584 int n; 1585 while ((n = in.read(buf)) > 0) 1586 jos.write(buf, 0, n); 1587 } catch (IOException ex) { 1588 error("Exception while adding " + fo.getName() + " to jar file", ex); 1589 } 1590 } finally { 1591 jos.closeEntry(); 1592 } 1593 } 1594 } 1595 1596 /* 1597 * A jar: URL is of the form jar:URL!/entry where URL is a URL for the .jar file itself. 1598 * In Symbol files (i.e. ct.sym) the underlying entry is prefixed META-INF/sym/<base>. 1599 */ 1600 private final Pattern jarEntry = Pattern.compile(".*!/(?:META-INF/sym/[^/]+/)?(.*)"); 1601 1602 /* 1603 * A jrt: URL is of the form jrt:/module/package/file 1604 */ 1605 private final Pattern jrtEntry = Pattern.compile("/([^/]+)/(.*)"); 1606 1607 private String guessPath(FileObject fo) { 1608 URI u = fo.toUri(); 1609 switch (u.getScheme()) { 1610 case "jar": { 1611 Matcher m = jarEntry.matcher(u.getSchemeSpecificPart()); 1612 if (m.matches()) { 1613 return m.group(1); 1614 } 1615 break; 1616 } 1617 case "jrt": { 1618 Matcher m = jrtEntry.matcher(u.getSchemeSpecificPart()); 1619 if (m.matches()) { 1620 return m.group(2); 1621 } 1622 break; 1623 } 1624 } 1625 throw new IllegalArgumentException(fo.getName() + "--" + fo.toUri()); 1626 } 1627 1628 private void error(String message, Throwable t) { 1629 out.println("Error: " + message + ": " + t); 1630 errors++; 1631 } 1632 1633 private int errors; 1634 } 1635 1636 /** 1637 * A task to configure and run the Java launcher. 1638 */ 1639 public class JavaTask extends AbstractTask<JavaTask> { 1640 boolean includeStandardOptions = true; 1641 private String classpath; 1642 private List<String> vmOptions; 1643 private String className; 1644 private List<String> classArgs; 1645 1646 /** 1647 * Create a task to run the Java launcher, using {@code EXEC} mode. 1648 */ 1649 public JavaTask() { 1650 super(Mode.EXEC); 1651 } 1652 1653 /** 1654 * Sets the classpath. 1655 * @param classpath the classpath 1656 * @return this task object 1657 */ 1658 public JavaTask classpath(String classpath) { 1659 this.classpath = classpath; 1660 return this; 1661 } 1662 1663 /** 1664 * Sets the VM options. 1665 * @param vmOptions the options 1666 * @return this task object 1667 */ 1668 public JavaTask vmOptions(String... vmOptions) { 1669 this.vmOptions = Arrays.asList(vmOptions); 1670 return this; 1671 } 1672 1673 /** 1674 * Sets the name of the class to be executed. 1675 * @param className the name of the class 1676 * @return this task object 1677 */ 1678 public JavaTask className(String className) { 1679 this.className = className; 1680 return this; 1681 } 1682 1683 /** 1684 * Sets the arguments for the class to be executed. 1685 * @param classArgs the arguments 1686 * @return this task object 1687 */ 1688 public JavaTask classArgs(String... classArgs) { 1689 this.classArgs = Arrays.asList(classArgs); 1690 return this; 1691 } 1692 1693 /** 1694 * Sets whether or not the standard VM and java options for the test should be passed 1695 * to the new VM instance. If this method is not called, the default behavior is that 1696 * the options will be passed to the new VM instance. 1697 * 1698 * @param includeStandardOptions whether or not the standard VM and java options for 1699 * the test should be passed to the new VM instance. 1700 * @return this task object 1701 */ 1702 public JavaTask includeStandardOptions(boolean includeStandardOptions) { 1703 this.includeStandardOptions = includeStandardOptions; 1704 return this; 1705 } 1706 1707 /** 1708 * {@inheritDoc} 1709 * @return the name "java" 1710 */ 1711 @Override 1712 public String name() { 1713 return "java"; 1714 } 1715 1716 /** 1717 * Calls the Java launcher with the arguments as currently configured. 1718 * @return a Result object indicating the outcome of the task 1719 * and the content of any output written to stdout or stderr. 1720 * @throws TaskError if the outcome of the task is not as expected. 1721 */ 1722 @Override 1723 public Result run() { 1724 List<String> args = new ArrayList<>(); 1725 args.add(getJDKTool("java").toString()); 1726 if (includeStandardOptions) { 1727 args.addAll(split(System.getProperty("test.vm.opts"), " +")); 1728 args.addAll(split(System.getProperty("test.java.opts"), " +")); 1729 } 1730 if (classpath != null) { 1731 args.add("-classpath"); 1732 args.add(classpath); 1733 } 1734 if (vmOptions != null) 1735 args.addAll(vmOptions); 1736 if (className != null) 1737 args.add(className); 1738 if (classArgs != null) 1739 args.addAll(classArgs); 1740 ProcessBuilder pb = getProcessBuilder(); 1741 pb.command(args); 1742 try { 1743 return runProcess(ToolBox.this, this, pb.start()); 1744 } catch (IOException | InterruptedException e) { 1745 throw new Error(e); 1746 } 1747 } 1748 } 1749 1750 /** 1751 * A task to configure and run a general command. 1752 */ 1753 public class ExecTask extends AbstractTask<ExecTask> { 1754 private final String command; 1755 private List<String> args; 1756 1757 /** 1758 * Create a task to execute a given command, to be run using {@code EXEC} mode. 1759 * @param command the command to be executed 1760 */ 1761 public ExecTask(String command) { 1762 super(Mode.EXEC); 1763 this.command = command; 1764 } 1765 1766 /** 1767 * Create a task to execute a given command, to be run using {@code EXEC} mode. 1768 * @param command the command to be executed 1769 */ 1770 public ExecTask(Path command) { 1771 super(Mode.EXEC); 1772 this.command = command.toString(); 1773 } 1774 1775 /** 1776 * Sets the arguments for the command to be executed 1777 * @param args the arguments 1778 * @return this task object 1779 */ 1780 public ExecTask args(String... args) { 1781 this.args = Arrays.asList(args); 1782 return this; 1783 } 1784 1785 /** 1786 * {@inheritDoc} 1787 * @return the name "exec" 1788 */ 1789 @Override 1790 public String name() { 1791 return "exec"; 1792 } 1793 1794 /** 1795 * Calls the command with the arguments as currently configured. 1796 * @return a Result object indicating the outcome of the task 1797 * and the content of any output written to stdout or stderr. 1798 * @throws TaskError if the outcome of the task is not as expected. 1799 */ 1800 @Override 1801 public Result run() { 1802 List<String> cmdArgs = new ArrayList<>(); 1803 cmdArgs.add(command); 1804 if (args != null) 1805 cmdArgs.addAll(args); 1806 ProcessBuilder pb = getProcessBuilder(); 1807 pb.command(cmdArgs); 1808 try { 1809 return runProcess(ToolBox.this, this, pb.start()); 1810 } catch (IOException | InterruptedException e) { 1811 throw new Error(e); 1812 } 1813 } 1814 } 1815 1816 /** 1817 * An in-memory Java source file. 1818 * It is able to extract the file name from simple source text using 1819 * regular expressions. 1820 */ 1821 public static class JavaSource extends SimpleJavaFileObject { 1822 private final String source; 1823 1824 /** 1825 * Creates a in-memory file object for Java source code. 1826 * @param className the name of the class 1827 * @param source the source text 1828 */ 1829 public JavaSource(String className, String source) { 1830 super(URI.create(className), JavaFileObject.Kind.SOURCE); 1831 this.source = source; 1832 } 1833 1834 /** 1835 * Creates a in-memory file object for Java source code. 1836 * The name of the class will be inferred from the source code. 1837 * @param source the source text 1838 */ 1839 public JavaSource(String source) { 1840 super(URI.create(getJavaFileNameFromSource(source)), 1841 JavaFileObject.Kind.SOURCE); 1842 this.source = source; 1843 } 1844 1845 /** 1846 * Writes the source code to a file in the current directory. 1847 * @throws IOException if there is a problem writing the file 1848 */ 1849 public void write() throws IOException { 1850 write(currDir); 1851 } 1852 1853 /** 1854 * Writes the source code to a file in a specified directory. 1855 * @param dir the directory 1856 * @throws IOException if there is a problem writing the file 1857 */ 1858 public void write(Path dir) throws IOException { 1859 Path file = dir.resolve(getJavaFileNameFromSource(source)); 1860 Files.createDirectories(file.getParent()); 1861 try (BufferedWriter out = Files.newBufferedWriter(file)) { 1862 out.write(source.replace("\n", lineSeparator)); 1863 } 1864 } 1865 1866 @Override 1867 public CharSequence getCharContent(boolean ignoreEncodingErrors) { 1868 return source; 1869 } 1870 1871 private static Pattern packagePattern = 1872 Pattern.compile("package\\s+(((?:\\w+\\.)*)(?:\\w+))"); 1873 private static Pattern classPattern = 1874 Pattern.compile("(?:public\\s+)?(?:class|enum|interface)\\s+(\\w+)"); 1875 1876 /** 1877 * Extracts the Java file name from the class declaration. 1878 * This method is intended for simple files and uses regular expressions, 1879 * so comments matching the pattern can make the method fail. 1880 */ 1881 static String getJavaFileNameFromSource(String source) { 1882 String packageName = null; 1883 1884 Matcher matcher = packagePattern.matcher(source); 1885 if (matcher.find()) 1886 packageName = matcher.group(1).replace(".", "/"); 1887 1888 matcher = classPattern.matcher(source); 1889 if (matcher.find()) { 1890 String className = matcher.group(1) + ".java"; 1891 return (packageName == null) ? className : packageName + "/" + className; 1892 } else { 1893 throw new Error("Could not extract the java class " + 1894 "name from the provided source"); 1895 } 1896 } 1897 } 1898 1899 /** 1900 * Extracts the Java file name from the class declaration. 1901 * This method is intended for simple files and uses regular expressions, 1902 * so comments matching the pattern can make the method fail. 1903 * @deprecated This is a legacy method for compatibility with ToolBox v1. 1904 * Use {@link JavaSource#getName JavaSource.getName} instead. 1905 * @param source the source text 1906 * @return the Java file name inferred from the source 1907 */ 1908 @Deprecated 1909 public static String getJavaFileNameFromSource(String source) { 1910 return JavaSource.getJavaFileNameFromSource(source); 1911 } 1912 1913 /** 1914 * A memory file manager, for saving generated files in memory. 1915 * The file manager delegates to a separate file manager for listing and 1916 * reading input files. 1917 */ 1918 public static class MemoryFileManager extends ForwardingJavaFileManager { 1919 private interface Content { 1920 byte[] getBytes(); 1921 String getString(); 1922 } 1923 1924 /** 1925 * Maps binary class names to generated content. 1926 */ 1927 final Map<Location, Map<String, Content>> files; 1928 1929 /** 1930 * Construct a memory file manager which stores output files in memory, 1931 * and delegates to a default file manager for input files. 1932 */ 1933 public MemoryFileManager() { 1934 this(JavacTool.create().getStandardFileManager(null, null, null)); 1935 } 1936 1937 /** 1938 * Construct a memory file manager which stores output files in memory, 1939 * and delegates to a specified file manager for input files. 1940 * @param fileManager the file manager to be used for input files 1941 */ 1942 public MemoryFileManager(JavaFileManager fileManager) { 1943 super(fileManager); 1944 files = new HashMap<>(); 1945 } 1946 1947 @Override 1948 public JavaFileObject getJavaFileForOutput(Location location, 1949 String name, 1950 JavaFileObject.Kind kind, 1951 FileObject sibling) 1952 { 1953 return new MemoryFileObject(location, name, kind); 1954 } 1955 1956 /** 1957 * Returns the content written to a file in a given location, 1958 * or null if no such file has been written. 1959 * @param location the location 1960 * @param name the name of the file 1961 * @return the content as an array of bytes 1962 */ 1963 public byte[] getFileBytes(Location location, String name) { 1964 Content content = getFile(location, name); 1965 return (content == null) ? null : content.getBytes(); 1966 } 1967 1968 /** 1969 * Returns the content written to a file in a given location, 1970 * or null if no such file has been written. 1971 * @param location the location 1972 * @param name the name of the file 1973 * @return the content as a string 1974 */ 1975 public String getFileString(Location location, String name) { 1976 Content content = getFile(location, name); 1977 return (content == null) ? null : content.getString(); 1978 } 1979 1980 private Content getFile(Location location, String name) { 1981 Map<String, Content> filesForLocation = files.get(location); 1982 return (filesForLocation == null) ? null : filesForLocation.get(name); 1983 } 1984 1985 private void save(Location location, String name, Content content) { 1986 Map<String, Content> filesForLocation = files.get(location); 1987 if (filesForLocation == null) 1988 files.put(location, filesForLocation = new HashMap<>()); 1989 filesForLocation.put(name, content); 1990 } 1991 1992 /** 1993 * A writable file object stored in memory. 1994 */ 1995 private class MemoryFileObject extends SimpleJavaFileObject { 1996 private final Location location; 1997 private final String name; 1998 1999 /** 2000 * Constructs a memory file object. 2001 * @param name binary name of the class to be stored in this file object 2002 */ 2003 MemoryFileObject(Location location, String name, JavaFileObject.Kind kind) { 2004 super(URI.create("mfm:///" + name.replace('.','/') + kind.extension), 2005 Kind.CLASS); 2006 this.location = location; 2007 this.name = name; 2008 } 2009 2010 @Override 2011 public OutputStream openOutputStream() { 2012 return new FilterOutputStream(new ByteArrayOutputStream()) { 2013 @Override 2014 public void close() throws IOException { 2015 out.close(); 2016 byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); 2017 save(location, name, new Content() { 2018 @Override 2019 public byte[] getBytes() { 2020 return bytes; 2021 } 2022 @Override 2023 public String getString() { 2024 return new String(bytes); 2025 } 2026 2027 }); 2028 } 2029 }; 2030 } 2031 2032 @Override 2033 public Writer openWriter() { 2034 return new FilterWriter(new StringWriter()) { 2035 @Override 2036 public void close() throws IOException { 2037 out.close(); 2038 String text = ((StringWriter) out).toString(); 2039 save(location, name, new Content() { 2040 @Override 2041 public byte[] getBytes() { 2042 return text.getBytes(); 2043 } 2044 @Override 2045 public String getString() { 2046 return text; 2047 } 2048 2049 }); 2050 } 2051 }; 2052 } 2053 } 2054 2055 } 2056 2057 }