/* * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FilterOutputStream; import java.io.FilterWriter; import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.net.URI; import java.nio.charset.Charset; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.JavaFileManager.Location; import javax.tools.SimpleJavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.StandardLocation; import com.sun.tools.javac.api.JavacTaskImpl; import com.sun.tools.javac.api.JavacTool; /** * Utility methods and classes for writing jtreg tests for * javac, javah, javap, and sjavac. (For javadoc support, * see JavadocTester.) * *

There is support for common file operations similar to * shell commands like cat, cp, diff, mv, rm, grep. * *

There is also support for invoking various tools, like * javac, javah, javap, jar, java and other JDK tools. * *

File separators: for convenience, many operations accept strings * to represent filenames. On all platforms on which JDK is supported, * "/" is a legal filename component separator. In particular, even * on Windows, where the official file separator is "\", "/" is a legal * alternative. It is therefore recommended that any client code using * strings to specify filenames should use "/". * * @author Vicente Romero (original) * @author Jonathan Gibbons (revised) */ public class ToolBox { /** The platform line separator. */ public static final String lineSeparator = System.getProperty("line.separator"); /** The platform OS name. */ public static final String osName = System.getProperty("os.name"); /** The location of the class files for this test, or null if not set. */ public static final String testClasses = System.getProperty("test.classes"); /** The location of the source files for this test, or null if not set. */ public static final String testSrc = System.getProperty("test.src"); /** The location of the test JDK for this test, or null if not set. */ public static final String testJDK = System.getProperty("test.jdk"); /** The current directory. */ public static final Path currDir = Paths.get("."); /** The stream used for logging output. */ public PrintStream out = System.err; JavaCompiler compiler; StandardJavaFileManager standardJavaFileManager; /** * Checks if the host OS is some version of Windows. * @return true if the host OS is some version of Windows */ public boolean isWindows() { return osName.toLowerCase(Locale.ENGLISH).startsWith("windows"); } /** * Splits a string around matches of the given regular expression. * If the string is empty, an empty list will be returned. * @param text the string to be split * @param sep the delimiting regular expression * @return the strings between the separators */ public List split(String text, String sep) { if (text.isEmpty()) return Collections.emptyList(); return Arrays.asList(text.split(sep)); } /** * Checks if two lists of strings are equal. * @param l1 the first list of strings to be compared * @param l2 the second list of strings to be compared * @throws Error if the lists are not equal */ public void checkEqual(List l1, List l2) throws Error { if (!Objects.equals(l1, l2)) { // l1 and l2 cannot both be null if (l1 == null) throw new Error("comparison failed: l1 is null"); if (l2 == null) throw new Error("comparison failed: l2 is null"); // report first difference for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) { String s1 = l1.get(i); String s2 = l1.get(i); if (!Objects.equals(s1, s2)) { throw new Error("comparison failed, index " + i + ", (" + s1 + ":" + s2 + ")"); } } throw new Error("comparison failed: l1.size=" + l1.size() + ", l2.size=" + l2.size()); } } /** * Filters a list of strings according to the given regular expression. * @param regex the regular expression * @param lines the strings to be filtered * @return the strings matching the regular expression */ public List grep(String regex, List lines) { return grep(Pattern.compile(regex), lines); } /** * Filters a list of strings according to the given regular expression. * @param pattern the regular expression * @param lines the strings to be filtered * @return the strings matching the regular expression */ public List grep(Pattern pattern, List lines) { return lines.stream() .filter(s -> pattern.matcher(s).find()) .collect(Collectors.toList()); } /** * Copies a file. * If the given destination exists and is a directory, the copy is created * in that directory. Otherwise, the copy will be placed at the destination, * possibly overwriting any existing file. *

Similar to the shell "cp" command: {@code cp from to}. * @param from the file to be copied * @param to where to copy the file * @throws IOException if any error occurred while copying the file */ public void copyFile(String from, String to) throws IOException { copyFile(Paths.get(from), Paths.get(to)); } /** * Copies a file. * If the given destination exists and is a directory, the copy is created * in that directory. Otherwise, the copy will be placed at the destination, * possibly overwriting any existing file. *

Similar to the shell "cp" command: {@code cp from to}. * @param from the file to be copied * @param to where to copy the file * @throws IOException if an error occurred while copying the file */ public void copyFile(Path from, Path to) throws IOException { if (Files.isDirectory(to)) { to = to.resolve(from.getFileName()); } else { Files.createDirectories(to.getParent()); } Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); } /** * Creates one of more directories. * For each of the series of paths, a directory will be created, * including any necessary parent directories. *

Similar to the shell command: {@code mkdir -p paths}. * @param paths the directories to be created * @throws IOException if an error occurred while creating the directories */ public void createDirectories(String... paths) throws IOException { if (paths.length == 0) throw new IllegalArgumentException("no directories specified"); for (String p : paths) Files.createDirectories(Paths.get(p)); } /** * Creates one or more directories. * For each of the series of paths, a directory will be created, * including any necessary parent directories. *

Similar to the shell command: {@code mkdir -p paths}. * @param paths the directories to be created * @throws IOException if an error occurred while creating the directories */ public void createDirectories(Path... paths) throws IOException { if (paths.length == 0) throw new IllegalArgumentException("no directories specified"); for (Path p : paths) Files.createDirectories(p); } /** * Deletes one or more files. * Any directories to be deleted must be empty. *

Similar to the shell command: {@code rm files}. * @param files the files to be deleted * @throws IOException if an error occurred while deleting the files */ public void deleteFiles(String... files) throws IOException { if (files.length == 0) throw new IllegalArgumentException("no files specified"); for (String file : files) Files.delete(Paths.get(file)); } /** * Moves a file. * If the given destination exists and is a directory, the file will be moved * to that directory. Otherwise, the file will be moved to the destination, * possibly overwriting any existing file. *

Similar to the shell "mv" command: {@code mv from to}. * @param from the file to be moved * @param to where to move the file * @throws IOException if an error occurred while moving the file */ public void moveFile(String from, String to) throws IOException { moveFile(Paths.get(from), Paths.get(to)); } /** * Moves a file. * If the given destination exists and is a directory, the file will be moved * to that directory. Otherwise, the file will be moved to the destination, * possibly overwriting any existing file. *

Similar to the shell "mv" command: {@code mv from to}. * @param from the file to be moved * @param to where to move the file * @throws IOException if an error occurred while moving the file */ public void moveFile(Path from, Path to) throws IOException { if (Files.isDirectory(to)) { to = to.resolve(from.getFileName()); } else { Files.createDirectories(to.getParent()); } Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); } /** * Reads the lines of a file. * The file is read using the default character encoding. * @param path the file to be read * @return the lines of the file. * @throws IOException if an error occurred while reading the file */ public List readAllLines(String path) throws IOException { return readAllLines(path, null); } /** * Reads the lines of a file. * The file is read using the default character encoding. * @param path the file to be read * @return the lines of the file. * @throws IOException if an error occurred while reading the file */ public List readAllLines(Path path) throws IOException { return readAllLines(path, null); } /** * Reads the lines of a file using the given encoding. * @param path the file to be read * @param encoding the encoding to be used to read the file * @return the lines of the file. * @throws IOException if an error occurred while reading the file */ public List readAllLines(String path, String encoding) throws IOException { return readAllLines(Paths.get(path), encoding); } /** * Reads the lines of a file using the given encoding. * @param path the file to be read * @param encoding the encoding to be used to read the file * @return the lines of the file. * @throws IOException if an error occurred while reading the file */ public List readAllLines(Path path, String encoding) throws IOException { return Files.readAllLines(path, getCharset(encoding)); } private Charset getCharset(String encoding) { return (encoding == null) ? Charset.defaultCharset() : Charset.forName(encoding); } /** * Writes a file containing the given content. * Any necessary directories for the file will be created. * @param path where to write the file * @param content the content for the file * @throws IOException if an error occurred while writing the file */ public void writeFile(String path, String content) throws IOException { writeFile(Paths.get(path), content); } /** * Writes a file containing the given content. * Any necessary directories for the file will be created. * @param path where to write the file * @param content the content for the file * @throws IOException if an error occurred while writing the file */ public void writeFile(Path path, String content) throws IOException { Path dir = path.getParent(); if (dir != null) Files.createDirectories(dir); try (BufferedWriter w = Files.newBufferedWriter(path)) { w.write(content); } } /** * Writes one or more files containing Java source code. * For each file to be written, the filename will be inferred from the * given base directory, the package declaration (if present) and from the * the name of the first class, interface or enum declared in the file. *

For example, if the base directory is /my/dir/ and the content * contains "package p; class C { }", the file will be written to * /my/dir/p/C.java. *

Note: the content is analyzed using regular expressions; * errors can occur if any contents have initial comments that might trip * up the analysis. * @param dir the base directory * @param contents the contents of the files to be written * @throws IOException if an error occurred while writing any of the files. */ public void writeJavaFiles(Path dir, String... contents) throws IOException { if (contents.length == 0) throw new IllegalArgumentException("no content specified for any files"); for (String c : contents) { new JavaSource(c).write(dir); } } /** * Returns the path for the binary of a JDK tool within {@link testJDK}. * @param tool the name of the tool * @return the path of the tool */ public Path getJDKTool(String tool) { return Paths.get(testJDK, "bin", tool); } /** * Returns a string representing the contents of an {@code Iterable} as a list. * @param the type parameter of the {@code Iterable} * @param items the iterable * @return the string */ String toString(Iterable items) { return StreamSupport.stream(items.spliterator(), false) .map(Objects::toString) .collect(Collectors.joining(",", "[", "]")); } /** * The supertype for tasks. * Complex operations are modelled by building and running a "Task" object. * Tasks are typically configured in a fluent series of calls. */ public interface Task { /** * Returns the name of the task. * @return the name of the task */ String name(); /** * Executes the task as currently configured. * @return a Result object containing the results of running the task * @throws TaskError if the outcome of the task was not as expected */ Result run() throws TaskError; } /** * Exception thrown by {@code Task.run} when the outcome is not as * expected. */ public static class TaskError extends Error { /** * Creates a TaskError object with the given message. * @param message the message */ public TaskError(String message) { super(message); } } /** * An enum to indicate the mode a task should use it is when executed. */ public enum Mode { /** * The task should use the interface used by the command * line launcher for the task. * For example, for javac: com.sun.tools.javac.Main.compile */ CMDLINE, /** * The task should use a publicly defined API for the task. * For example, for javac: javax.tools.JavaCompiler */ API, /** * The task should use the standard launcher for the task. * For example, $JAVA_HOME/bin/javac */ EXEC } /** * An enum to indicate the expected success or failure of executing a task. */ public enum Expect { /** It is expected that the task will complete successfully. */ SUCCESS, /** It is expected that the task will not complete successfully. */ FAIL } /** * An enum to identify the streams that may be written by a {@code Task}. */ public enum OutputKind { /** Identifies output written to {@code System.out} or {@code stdout}. */ STDOUT, /** Identifies output written to {@code System.err} or {@code stderr}. */ STDERR, /** Identifies output written to a stream provided directly to the task. */ DIRECT }; /** * The results from running a {@link Task}. * The results contain the exit code returned when the tool was invoked, * and a map containing the output written to any streams during the * execution of the tool. * All tools support "stdout" and "stderr". * Tools that take an explicit PrintWriter save output written to that * stream as "main". */ public class Result { final Task task; final int exitCode; final Map outputMap; Result(Task task, int exitCode, Map outputMap) { this.task = task; this.exitCode = exitCode; this.outputMap = outputMap; } /** * Returns the content of a specified stream. * @param outputKind the kind of the selected stream * @return the content that was written to that stream when the tool * was executed. */ public String getOutput(OutputKind outputKind) { return outputMap.get(outputKind); } /** * Returns the content of a named stream as a list of lines. * @param outputKind the kind of the selected stream * @return the content that was written to that stream when the tool * was executed. */ public List getOutputLines(OutputKind outputKind) { return Arrays.asList(outputMap.get(outputKind).split(lineSeparator)); } /** * Writes the content of the specified stream to the log. * @param kind the kind of the selected stream * @return this Result object */ public Result write(OutputKind kind) { String text = getOutput(kind); if (text == null || text.isEmpty()) out.println("[" + task.name() + ":" + kind + "]: empty"); else { out.println("[" + task.name() + ":" + kind + "]:"); out.print(text); } return this; } /** * Writes the content of all streams with any content to the log. * @return this Result object */ public Result writeAll() { outputMap.forEach((name, text) -> { if (!text.isEmpty()) { out.println("[" + name + "]:"); out.print(text); } }); return this; } } /** * A utility base class to simplify the implementation of tasks. * Provides support for running the task in a process and for * capturing output written by the task to stdout, stderr and * other writers where applicable. * @param the implementing subclass */ protected static abstract class AbstractTask> implements Task { protected final Mode mode; private final Map redirects = new EnumMap<>(OutputKind.class); private final Map envVars = new HashMap<>(); private Expect expect = Expect.SUCCESS; int expectedExitCode = 0; /** * Create a task that will execute in the specified mode. * @param mode the mode */ protected AbstractTask(Mode mode) { this.mode = mode; } /** * Sets the expected outcome of the task and calls {@code run()}. * @param expect the expected outcome * @return the result of calling {@code run()} */ public Result run(Expect expect) { expect(expect, Integer.MIN_VALUE); return run(); } /** * Sets the expected outcome of the task and calls {@code run()}. * @param expect the expected outcome * @param exitCode the expected exit code if the expected outcome * is {@code FAIL} * @return the result of calling {@code run()} */ public Result run(Expect expect, int exitCode) { expect(expect, exitCode); return run(); } /** * Sets the expected outcome and expected exit code of the task. * The exit code will not be checked if the outcome is * {@code Expect.SUCCESS} or if the exit code is set to * {@code Integer.MIN_VALUE}. * @param expect the expected outcome * @param exitCode the expected exit code */ protected void expect(Expect expect, int exitCode) { this.expect = expect; this.expectedExitCode = exitCode; } /** * Checks the exit code contained in a {@code Result} against the * expected outcome and exit value * @param result the result object * @return the result object * @throws TaskError if the exit code stored in the result object * does not match the expected outcome and exit code. */ protected Result checkExit(Result result) throws TaskError { switch (expect) { case SUCCESS: if (result.exitCode != 0) { result.writeAll(); throw new TaskError("Task " + name() + " failed: rc=" + result.exitCode); } break; case FAIL: if (result.exitCode == 0) { result.writeAll(); throw new TaskError("Task " + name() + " succeeded unexpectedly"); } if (expectedExitCode != Integer.MIN_VALUE && result.exitCode != expectedExitCode) { result.writeAll(); throw new TaskError("Task " + name() + "failed with unexpected exit code " + result.exitCode + ", expected " + expectedExitCode); } break; } return result; } /** * Sets an environment variable to be used by this task. * @param name the name of the environment variable * @param value the value for the environment variable * @return this task object * @throws IllegalStateException if the task mode is not {@code EXEC} */ protected T envVar(String name, String value) { if (mode != Mode.EXEC) throw new IllegalStateException(); envVars.put(name, value); return (T) this; } /** * Redirects output from an output stream to a file. * @param outputKind the name of the stream to be redirected. * @param path the file * @return this task object * @throws IllegalStateException if the task mode is not {@code EXEC} */ protected T redirect(OutputKind outputKind, String path) { if (mode != Mode.EXEC) throw new IllegalStateException(); redirects.put(outputKind, path); return (T) this; } /** * Returns a {@code ProcessBuilder} initialized with any * redirects and environment variables that have been set. * @return a {@code ProcessBuilder} */ protected ProcessBuilder getProcessBuilder() { if (mode != Mode.EXEC) throw new IllegalStateException(); ProcessBuilder pb = new ProcessBuilder(); if (redirects.get(OutputKind.STDOUT) != null) pb.redirectOutput(new File(redirects.get(OutputKind.STDOUT))); if (redirects.get(OutputKind.STDERR) != null) pb.redirectError(new File(redirects.get(OutputKind.STDERR))); pb.environment().putAll(envVars); return pb; } /** * Collects the output from a process and saves it in a {@code Result}. * @param tb the {@code ToolBox} containing the task {@code t} * @param t the task initiating the process * @param p the process * @return a Result object containing the output from the process and its * exit value. * @throws InterruptedException if the thread is interrupted */ protected Result runProcess(ToolBox tb, Task t, Process p) throws InterruptedException { if (mode != Mode.EXEC) throw new IllegalStateException(); ProcessOutput sysOut = new ProcessOutput(p.getInputStream()).start(); ProcessOutput sysErr = new ProcessOutput(p.getErrorStream()).start(); sysOut.waitUntilDone(); sysErr.waitUntilDone(); int rc = p.waitFor(); Map outputMap = new EnumMap<>(OutputKind.class); outputMap.put(OutputKind.STDOUT, sysOut.getOutput()); outputMap.put(OutputKind.STDERR, sysErr.getOutput()); return checkExit(tb.new Result(t, rc, outputMap)); } /** * Thread-friendly class to read the output from a process until the stream * is exhausted. */ static class ProcessOutput implements Runnable { ProcessOutput(InputStream from) { in = new BufferedReader(new InputStreamReader(from)); out = new StringBuilder(); } ProcessOutput start() { new Thread(this).start(); return this; } @Override public void run() { try { String line; while ((line = in.readLine()) != null) { out.append(line).append(lineSeparator); } } catch (IOException e) { } synchronized (this) { done = true; notifyAll(); } } synchronized void waitUntilDone() throws InterruptedException { boolean interrupted = false; // poll interrupted flag, while waiting for copy to complete while (!(interrupted = Thread.interrupted()) && !done) wait(1000); if (interrupted) throw new InterruptedException(); } String getOutput() { return out.toString(); } private BufferedReader in; private final StringBuilder out; private boolean done; } /** * Utility class to simplify the handling of temporarily setting a * new stream for System.out or System.err. */ static class StreamOutput { // Functional interface to set a stream. // Expected use: System::setOut, System::setErr private interface Initializer { void set(PrintStream s); } private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); private final PrintStream ps = new PrintStream(baos); private final PrintStream prev; private final Initializer init; StreamOutput(PrintStream s, Initializer init) { prev = s; init.set(ps); this.init = init; } /** * Closes the stream and returns the contents that were written to it. * @return the contents that were written to it. */ String close() { init.set(prev); ps.close(); return baos.toString(); } } /** * Utility class to simplify the handling of creating an in-memory PrintWriter. */ static class WriterOutput { private final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw); /** * Closes the stream and returns the contents that were written to it. * @return the contents that were written to it. */ String close() { pw.close(); return sw.toString(); } } } /** * A task to configure and run the Java compiler, javac. */ public class JavacTask extends AbstractTask { private boolean includeStandardOptions; private String classpath; private String sourcepath; private String outdir; private List options; private List classes; private List files; private List fileObjects; private JavaFileManager fileManager; /** * Creates a task to execute {@code javac} using API mode. */ public JavacTask() { super(Mode.API); } /** * Creates a task to execute {@code javac} in a specified mode. * @param mode the mode to be used */ public JavacTask(Mode mode) { super(mode); } /** * Sets the classpath. * @param classpath the classpath * @return this task object */ public JavacTask classpath(String classpath) { this.classpath = classpath; return this; } /** * Sets the sourcepath. * @param sourcepath the sourcepath * @return this task object */ public JavacTask sourcepath(String sourcepath) { this.sourcepath = sourcepath; return this; } /** * Sets the output directory. * @param outdir the output directory * @return this task object */ public JavacTask outdir(String outdir) { this.outdir = outdir; return this; } /** * Sets the options. * @param options the options * @return this task object */ public JavacTask options(String... options) { this.options = Arrays.asList(options); return this; } /** * Sets the classes to be analyzed. * @param classes the classes * @return this task object */ public JavacTask classes(String... classes) { this.classes = Arrays.asList(classes); return this; } /** * Sets the files to be compiled or analyzed. * @param files the files * @return this task object */ public JavacTask files(String... files) { this.files = Arrays.asList(files); return this; } /** * Sets the files to be compiled or analyzed. * @param files the files * @return this task object */ public JavacTask files(Path... files) { this.files = Stream.of(files) .map(Path::toString) .collect(Collectors.toList()); return this; } /** * Sets the sources to be compiled or analyzed. * Each source string is converted into an in-memory object that * can be passed directly to the compiler. * @param sources the sources * @return this task object */ public JavacTask sources(String... sources) { fileObjects = Stream.of(sources) .map(s -> new JavaSource(s)) .collect(Collectors.toList()); return this; } /** * Sets the file manager to be used by this task. * @param fileManager the file manager * @return this task object */ public JavacTask fileManager(JavaFileManager fileManager) { this.fileManager = fileManager; return this; } /** * {@inheritDoc} * @return the name "javac" */ @Override public String name() { return "javac"; } /** * Calls the compiler with the arguments as currently configured. * @return a Result object indicating the outcome of the compilation * and the content of any output written to stdout, stderr, or the * main stream by the compiler. * @throws TaskError if the outcome of the task is not as expected. */ @Override public Result run() { if (mode == Mode.EXEC) return runExec(); WriterOutput direct = new WriterOutput(); // The following are to catch output to System.out and System.err, // in case these are used instead of the primary (main) stream StreamOutput sysOut = new StreamOutput(System.out, System::setOut); StreamOutput sysErr = new StreamOutput(System.err, System::setErr); int rc; Map outputMap = new HashMap<>(); try { switch (mode == null ? Mode.API : mode) { case API: rc = runAPI(direct.pw); break; case CMDLINE: rc = runCommand(direct.pw); break; default: throw new IllegalStateException(); } } catch (IOException e) { out.println("Exception occurred: " + e); rc = 99; } finally { outputMap.put(OutputKind.STDOUT, sysOut.close()); outputMap.put(OutputKind.STDERR, sysErr.close()); outputMap.put(OutputKind.DIRECT, direct.close()); } return checkExit(new Result(this, rc, outputMap)); } private int runAPI(PrintWriter pw) throws IOException { // if (compiler == null) { // TODO: allow this to be set externally // compiler = ToolProvider.getSystemJavaCompiler(); compiler = JavacTool.create(); // } if (fileManager == null) fileManager = compiler.getStandardFileManager(null, null, null); if (outdir != null) setLocation(StandardLocation.CLASS_OUTPUT, toFiles(outdir)); if (classpath != null) setLocation(StandardLocation.CLASS_PATH, toFiles(classpath)); if (sourcepath != null) setLocation(StandardLocation.SOURCE_PATH, toFiles(sourcepath)); List allOpts = new ArrayList<>(); if (options != null) allOpts.addAll(options); Iterable allFiles = joinFiles(files, fileObjects); JavaCompiler.CompilationTask task = compiler.getTask(pw, fileManager, null, // diagnostic listener; should optionally collect diags allOpts, classes, allFiles); return ((JavacTaskImpl) task).doCall().exitCode; } private void setLocation(StandardLocation location, List files) throws IOException { if (!(fileManager instanceof StandardJavaFileManager)) throw new IllegalStateException("not a StandardJavaFileManager"); ((StandardJavaFileManager) fileManager).setLocation(location, files); } private int runCommand(PrintWriter pw) { List args = getAllArgs(); String[] argsArray = args.toArray(new String[args.size()]); return com.sun.tools.javac.Main.compile(argsArray, pw); } private Result runExec() { List args = new ArrayList<>(); Path javac = getJDKTool("javac"); args.add(javac.toString()); if (includeStandardOptions) { args.addAll(split(System.getProperty("test.tool.vm.opts"), " +")); args.addAll(split(System.getProperty("test.compiler.opts"), " +")); } args.addAll(getAllArgs()); String[] argsArray = args.toArray(new String[args.size()]); ProcessBuilder pb = getProcessBuilder(); pb.command(argsArray); try { return runProcess(ToolBox.this, this, pb.start()); } catch (IOException | InterruptedException e) { throw new Error(e); } } private List getAllArgs() { List args = new ArrayList<>(); if (options != null) args.addAll(options); if (outdir != null) { args.add("-d"); args.add(outdir); } if (classpath != null) { args.add("-classpath"); args.add(classpath); } if (sourcepath != null) { args.add("-sourcepath"); args.add(sourcepath); } if (classes != null) args.addAll(classes); if (files != null) args.addAll(files); return args; } private List toFiles(String path) { List result = new ArrayList<>(); for (String s : path.split(File.pathSeparator)) { if (!s.isEmpty()) result.add(new File(s)); } return result; } private Iterable joinFiles( List files, List fileObjects) { if (files == null) return fileObjects; if (standardJavaFileManager == null) standardJavaFileManager = compiler.getStandardFileManager(null, null, null); Iterable filesAsFileObjects = standardJavaFileManager.getJavaFileObjectsFromStrings(files); if (fileObjects == null) return filesAsFileObjects; List combinedList = new ArrayList<>(); for (JavaFileObject o : filesAsFileObjects) combinedList.add(o); combinedList.addAll(fileObjects); return combinedList; } } /** * A task to configure and run the native header tool, javah. */ public class JavahTask extends AbstractTask { private String classpath; private List options; private List classes; /** * Create a task to execute {@code javah} using {@code CMDLINE} mode. */ public JavahTask() { super(Mode.CMDLINE); } /** * Sets the classpath. * @param classpath the classpath * @return this task object */ public JavahTask classpath(String classpath) { this.classpath = classpath; return this; } /** * Sets the options. * @param options the options * @return this task object */ public JavahTask options(String... options) { this.options = Arrays.asList(options); return this; } /** * Sets the classes to be analyzed. * @param classes the classes * @return this task object */ public JavahTask classes(String... classes) { this.classes = Arrays.asList(classes); return this; } /** * {@inheritDoc} * @return the name "javah" */ @Override public String name() { return "javah"; } /** * Calls the javah tool with the arguments as currently configured. * @return a Result object indicating the outcome of the task * and the content of any output written to stdout, stderr, or the * main stream provided to the task. * @throws TaskError if the outcome of the task is not as expected. */ @Override public Result run() { List args = new ArrayList<>(); if (options != null) args.addAll(options); if (classpath != null) { args.add("-classpath"); args.add(classpath); } if (classes != null) args.addAll(classes); WriterOutput direct = new WriterOutput(); // These are to catch output to System.out and System.err, // in case these are used instead of the primary streams StreamOutput sysOut = new StreamOutput(System.out, System::setOut); StreamOutput sysErr = new StreamOutput(System.err, System::setErr); int rc; Map outputMap = new HashMap<>(); try { rc = com.sun.tools.javah.Main.run(args.toArray(new String[args.size()]), direct.pw); } finally { outputMap.put(OutputKind.STDOUT, sysOut.close()); outputMap.put(OutputKind.STDERR, sysErr.close()); outputMap.put(OutputKind.DIRECT, direct.close()); } return checkExit(new Result(this, rc, outputMap)); } } /** * A task to configure and run the disassembler tool, javap. */ public class JavapTask extends AbstractTask { private String classpath; private List options; private List classes; /** * Create a task to execute {@code javap} using {@code CMDLINE} mode. */ public JavapTask() { super(Mode.CMDLINE); } /** * Sets the classpath. * @param classpath the classpath * @return this task object */ public JavapTask classpath(String classpath) { this.classpath = classpath; return this; } /** * Sets the options. * @param options the options * @return this task object */ public JavapTask options(String... options) { this.options = Arrays.asList(options); return this; } /** * Sets the classes to be analyzed. * @param classes the classes * @return this task object */ public JavapTask classes(String... classes) { this.classes = Arrays.asList(classes); return this; } /** * {@inheritDoc} * @return the name "javap" */ @Override public String name() { return "javap"; } /** * Calls the javap tool with the arguments as currently configured. * @return a Result object indicating the outcome of the task * and the content of any output written to stdout, stderr, or the * main stream. * @throws TaskError if the outcome of the task is not as expected. */ @Override public Result run() { List args = new ArrayList<>(); if (options != null) args.addAll(options); if (classpath != null) { args.add("-classpath"); args.add(classpath); } if (classes != null) args.addAll(classes); WriterOutput direct = new WriterOutput(); // These are to catch output to System.out and System.err, // in case these are used instead of the primary streams StreamOutput sysOut = new StreamOutput(System.out, System::setOut); StreamOutput sysErr = new StreamOutput(System.err, System::setErr); int rc; Map outputMap = new HashMap<>(); try { rc = com.sun.tools.javap.Main.run(args.toArray(new String[args.size()]), direct.pw); } finally { outputMap.put(OutputKind.STDOUT, sysOut.close()); outputMap.put(OutputKind.STDERR, sysErr.close()); outputMap.put(OutputKind.DIRECT, direct.close()); } return checkExit(new Result(this, rc, outputMap)); } } /** * A task to configure and run the jar file utility. */ public class JarTask extends AbstractTask { private Path jar; private Manifest manifest; private String classpath; private String mainClass; private Path baseDir; private List paths; private Set fileObjects; /** * Creates a task to write jar files, using API mode. */ public JarTask() { super(Mode.API); paths = Collections.emptyList(); fileObjects = new LinkedHashSet<>(); } /** * Creates a JarTask for use with a given jar file. * @param path the file */ public JarTask(String path) { this(); jar = Paths.get(path); } /** * Sets a manifest for the jar file. * @param manifest the manifest * @return this task object */ public JarTask manifest(Manifest manifest) { this.manifest = manifest; return this; } /** * Sets a manifest for the jar file. * @param manifest a string containing the contents of the manifest * @return this task object * @throws IOException if there is a problem creating the manifest */ public JarTask manifest(String manifest) throws IOException { this.manifest = new Manifest(new ByteArrayInputStream(manifest.getBytes())); return this; } /** * Sets the classpath to be written to the {@code Class-Path} * entry in the manifest. * @param classpath the classpath * @return this task object */ public JarTask classpath(String classpath) { this.classpath = classpath; return this; } /** * Sets the class to be written to the {@code Main-Class} * entry in the manifest.. * @param mainClass the name of the main class * @return this task object */ public JarTask mainClass(String mainClass) { this.mainClass = mainClass; return this; } /** * Sets the base directory for files to be written into the jar file. * @param baseDir the base directory * @return this task object */ public JarTask baseDir(String baseDir) { this.baseDir = Paths.get(baseDir); return this; } /** * Sets the files to be written into the jar file. * @param files the files * @return this task object */ public JarTask files(String... files) { this.paths = Stream.of(files) .map(file -> Paths.get(file)) .collect(Collectors.toList()); return this; } /** * Adds a set of file objects to be written into the jar file, by copying them * from a Location in a JavaFileManager. * The file objects to be written are specified by a series of paths; * each path can be in one of the following forms: *

* * @param fm the file manager in which to find the file objects * @param l the location in which to find the file objects * @param paths the paths specifying the file objects to be copied * @return this task object * @throws IOException if errors occur while determining the set of file objects */ public JarTask files(JavaFileManager fm, Location l, String... paths) throws IOException { for (String p : paths) { if (p.endsWith(".**")) addPackage(fm, l, p.substring(0, p.length() - 3), true); else if (p.endsWith(".*")) addPackage(fm, l, p.substring(0, p.length() - 2), false); else addFile(fm, l, p); } return this; } private void addPackage(JavaFileManager fm, Location l, String pkg, boolean recurse) throws IOException { for (JavaFileObject fo : fm.list(l, pkg, EnumSet.allOf(JavaFileObject.Kind.class), recurse)) { fileObjects.add(fo); } } private void addFile(JavaFileManager fm, Location l, String path) throws IOException { JavaFileObject fo = fm.getJavaFileForInput(l, path, Kind.CLASS); fileObjects.add(fo); } /** * Provides limited jar command-like functionality. * The supported commands are: * * Any values specified by other configuration methods will be ignored. * @param args arguments in the style of those for the jar command * @return a Result object containing the results of running the task */ public Result run(String... args) { if (args.length < 2) throw new IllegalArgumentException(); ListIterator iter = Arrays.asList(args).listIterator(); String first = iter.next(); switch (first) { case "cf": jar = Paths.get(iter.next()); break; case "cfm": jar = Paths.get(iter.next()); try (InputStream in = Files.newInputStream(Paths.get(iter.next()))) { manifest = new Manifest(in); } catch (IOException e) { throw new IOError(e); } break; } if (iter.hasNext()) { if (iter.next().equals("-C")) baseDir = Paths.get(iter.next()); else iter.previous(); } paths = new ArrayList<>(); while (iter.hasNext()) paths.add(Paths.get(iter.next())); return run(); } /** * {@inheritDoc} * @return the name "jar" */ @Override public String name() { return "jar"; } /** * Creates a jar file with the arguments as currently configured. * @return a Result object indicating the outcome of the compilation * and the content of any output written to stdout, stderr, or the * main stream by the compiler. * @throws TaskError if the outcome of the task is not as expected. */ @Override public Result run() { Manifest m = (manifest == null) ? new Manifest() : manifest; Attributes mainAttrs = m.getMainAttributes(); if (mainClass != null) mainAttrs.put(Attributes.Name.MAIN_CLASS, mainClass); if (classpath != null) mainAttrs.put(Attributes.Name.CLASS_PATH, classpath); StreamOutput sysOut = new StreamOutput(System.out, System::setOut); StreamOutput sysErr = new StreamOutput(System.err, System::setErr); Map outputMap = new HashMap<>(); try (OutputStream os = Files.newOutputStream(jar); JarOutputStream jos = openJar(os, m)) { writeFiles(jos); writeFileObjects(jos); } catch (IOException e) { error("Exception while opening " + jar, e); } finally { outputMap.put(OutputKind.STDOUT, sysOut.close()); outputMap.put(OutputKind.STDERR, sysErr.close()); } return checkExit(new Result(this, (errors == 0) ? 0 : 1, outputMap)); } private JarOutputStream openJar(OutputStream os, Manifest m) throws IOException { if (m == null || m.getMainAttributes().isEmpty() && m.getEntries().isEmpty()) { return new JarOutputStream(os); } else { if (m.getMainAttributes().get(Attributes.Name.MANIFEST_VERSION) == null) m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); return new JarOutputStream(os, m); } } private void writeFiles(JarOutputStream jos) throws IOException { Path base = (baseDir == null) ? currDir : baseDir; for (Path path : paths) { Files.walkFileTree(base.resolve(path), new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { try { String p = base.relativize(file) .normalize() .toString() .replace(File.separatorChar, '/'); JarEntry e = new JarEntry(p); jos.putNextEntry(e); try { jos.write(Files.readAllBytes(file)); } finally { jos.closeEntry(); } return FileVisitResult.CONTINUE; } catch (IOException e) { error("Exception while adding " + file + " to jar file", e); return FileVisitResult.TERMINATE; } } }); } } private void writeFileObjects(JarOutputStream jos) throws IOException { for (FileObject fo : fileObjects) { String p = guessPath(fo); JarEntry e = new JarEntry(p); jos.putNextEntry(e); try { byte[] buf = new byte[1024]; try (BufferedInputStream in = new BufferedInputStream(fo.openInputStream())) { int n; while ((n = in.read(buf)) > 0) jos.write(buf, 0, n); } catch (IOException ex) { error("Exception while adding " + fo.getName() + " to jar file", ex); } } finally { jos.closeEntry(); } } } /* * A jar: URL is of the form jar:URL!/entry where URL is a URL for the .jar file itself. * In Symbol files (i.e. ct.sym) the underlying entry is prefixed META-INF/sym/. */ private final Pattern jarEntry = Pattern.compile(".*!/(?:META-INF/sym/[^/]+/)?(.*)"); /* * A jrt: URL is of the form jrt:/module/package/file */ private final Pattern jrtEntry = Pattern.compile("/([^/]+)/(.*)"); private String guessPath(FileObject fo) { URI u = fo.toUri(); switch (u.getScheme()) { case "jar": { Matcher m = jarEntry.matcher(u.getSchemeSpecificPart()); if (m.matches()) { return m.group(1); } break; } case "jrt": { Matcher m = jrtEntry.matcher(u.getSchemeSpecificPart()); if (m.matches()) { return m.group(2); } break; } } throw new IllegalArgumentException(fo.getName() + "--" + fo.toUri()); } private void error(String message, Throwable t) { out.println("Error: " + message + ": " + t); errors++; } private int errors; } /** * A task to configure and run the Java launcher. */ public class JavaTask extends AbstractTask { boolean includeStandardOptions = true; private String classpath; private List vmOptions; private String className; private List classArgs; /** * Create a task to run the Java launcher, using {@code EXEC} mode. */ public JavaTask() { super(Mode.EXEC); } /** * Sets the classpath. * @param classpath the classpath * @return this task object */ public JavaTask classpath(String classpath) { this.classpath = classpath; return this; } /** * Sets the VM options. * @param vmOptions the options * @return this task object */ public JavaTask vmOptions(String... vmOptions) { this.vmOptions = Arrays.asList(vmOptions); return this; } /** * Sets the name of the class to be executed. * @param className the name of the class * @return this task object */ public JavaTask className(String className) { this.className = className; return this; } /** * Sets the arguments for the class to be executed. * @param classArgs the arguments * @return this task object */ public JavaTask classArgs(String... classArgs) { this.classArgs = Arrays.asList(classArgs); return this; } /** * Sets whether or not the standard VM and java options for the test should be passed * to the new VM instance. If this method is not called, the default behavior is that * the options will be passed to the new VM instance. * * @param includeStandardOptions whether or not the standard VM and java options for * the test should be passed to the new VM instance. * @return this task object */ public JavaTask includeStandardOptions(boolean includeStandardOptions) { this.includeStandardOptions = includeStandardOptions; return this; } /** * {@inheritDoc} * @return the name "java" */ @Override public String name() { return "java"; } /** * Calls the Java launcher with the arguments as currently configured. * @return a Result object indicating the outcome of the task * and the content of any output written to stdout or stderr. * @throws TaskError if the outcome of the task is not as expected. */ @Override public Result run() { List args = new ArrayList<>(); args.add(getJDKTool("java").toString()); if (includeStandardOptions) { args.addAll(split(System.getProperty("test.vm.opts"), " +")); args.addAll(split(System.getProperty("test.java.opts"), " +")); } if (classpath != null) { args.add("-classpath"); args.add(classpath); } if (vmOptions != null) args.addAll(vmOptions); if (className != null) args.add(className); if (classArgs != null) args.addAll(classArgs); ProcessBuilder pb = getProcessBuilder(); pb.command(args); try { return runProcess(ToolBox.this, this, pb.start()); } catch (IOException | InterruptedException e) { throw new Error(e); } } } /** * A task to configure and run a general command. */ public class ExecTask extends AbstractTask { private final String command; private List args; /** * Create a task to execute a given command, to be run using {@code EXEC} mode. * @param command the command to be executed */ public ExecTask(String command) { super(Mode.EXEC); this.command = command; } /** * Create a task to execute a given command, to be run using {@code EXEC} mode. * @param command the command to be executed */ public ExecTask(Path command) { super(Mode.EXEC); this.command = command.toString(); } /** * Sets the arguments for the command to be executed * @param args the arguments * @return this task object */ public ExecTask args(String... args) { this.args = Arrays.asList(args); return this; } /** * {@inheritDoc} * @return the name "exec" */ @Override public String name() { return "exec"; } /** * Calls the command with the arguments as currently configured. * @return a Result object indicating the outcome of the task * and the content of any output written to stdout or stderr. * @throws TaskError if the outcome of the task is not as expected. */ @Override public Result run() { List cmdArgs = new ArrayList<>(); cmdArgs.add(command); if (args != null) cmdArgs.addAll(args); ProcessBuilder pb = getProcessBuilder(); pb.command(cmdArgs); try { return runProcess(ToolBox.this, this, pb.start()); } catch (IOException | InterruptedException e) { throw new Error(e); } } } /** * An in-memory Java source file. * It is able to extract the file name from simple source text using * regular expressions. */ public static class JavaSource extends SimpleJavaFileObject { private final String source; /** * Creates a in-memory file object for Java source code. * @param className the name of the class * @param source the source text */ public JavaSource(String className, String source) { super(URI.create(className), JavaFileObject.Kind.SOURCE); this.source = source; } /** * Creates a in-memory file object for Java source code. * The name of the class will be inferred from the source code. * @param source the source text */ public JavaSource(String source) { super(URI.create(getJavaFileNameFromSource(source)), JavaFileObject.Kind.SOURCE); this.source = source; } /** * Writes the source code to a file in the current directory. * @throws IOException if there is a problem writing the file */ public void write() throws IOException { write(currDir); } /** * Writes the source code to a file in a specified directory. * @param dir the directory * @throws IOException if there is a problem writing the file */ public void write(Path dir) throws IOException { Path file = dir.resolve(getJavaFileNameFromSource(source)); Files.createDirectories(file.getParent()); try (BufferedWriter out = Files.newBufferedWriter(file)) { out.write(source.replace("\n", lineSeparator)); } } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return source; } private static Pattern packagePattern = Pattern.compile("package\\s+(((?:\\w+\\.)*)(?:\\w+))"); private static Pattern classPattern = Pattern.compile("(?:public\\s+)?(?:class|enum|interface)\\s+(\\w+)"); /** * Extracts the Java file name from the class declaration. * This method is intended for simple files and uses regular expressions, * so comments matching the pattern can make the method fail. */ static String getJavaFileNameFromSource(String source) { String packageName = null; Matcher matcher = packagePattern.matcher(source); if (matcher.find()) packageName = matcher.group(1).replace(".", "/"); matcher = classPattern.matcher(source); if (matcher.find()) { String className = matcher.group(1) + ".java"; return (packageName == null) ? className : packageName + "/" + className; } else { throw new Error("Could not extract the java class " + "name from the provided source"); } } } /** * Extracts the Java file name from the class declaration. * This method is intended for simple files and uses regular expressions, * so comments matching the pattern can make the method fail. * @deprecated This is a legacy method for compatibility with ToolBox v1. * Use {@link JavaSource#getName JavaSource.getName} instead. * @param source the source text * @return the Java file name inferred from the source */ @Deprecated public static String getJavaFileNameFromSource(String source) { return JavaSource.getJavaFileNameFromSource(source); } /** * A memory file manager, for saving generated files in memory. * The file manager delegates to a separate file manager for listing and * reading input files. */ public static class MemoryFileManager extends ForwardingJavaFileManager { private interface Content { byte[] getBytes(); String getString(); } /** * Maps binary class names to generated content. */ final Map> files; /** * Construct a memory file manager which stores output files in memory, * and delegates to a default file manager for input files. */ public MemoryFileManager() { this(JavacTool.create().getStandardFileManager(null, null, null)); } /** * Construct a memory file manager which stores output files in memory, * and delegates to a specified file manager for input files. * @param fileManager the file manager to be used for input files */ public MemoryFileManager(JavaFileManager fileManager) { super(fileManager); files = new HashMap<>(); } @Override public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) { return new MemoryFileObject(location, name, kind); } /** * Returns the content written to a file in a given location, * or null if no such file has been written. * @param location the location * @param name the name of the file * @return the content as an array of bytes */ public byte[] getFileBytes(Location location, String name) { Content content = getFile(location, name); return (content == null) ? null : content.getBytes(); } /** * Returns the content written to a file in a given location, * or null if no such file has been written. * @param location the location * @param name the name of the file * @return the content as a string */ public String getFileString(Location location, String name) { Content content = getFile(location, name); return (content == null) ? null : content.getString(); } private Content getFile(Location location, String name) { Map filesForLocation = files.get(location); return (filesForLocation == null) ? null : filesForLocation.get(name); } private void save(Location location, String name, Content content) { Map filesForLocation = files.get(location); if (filesForLocation == null) files.put(location, filesForLocation = new HashMap<>()); filesForLocation.put(name, content); } /** * A writable file object stored in memory. */ private class MemoryFileObject extends SimpleJavaFileObject { private final Location location; private final String name; /** * Constructs a memory file object. * @param name binary name of the class to be stored in this file object */ MemoryFileObject(Location location, String name, JavaFileObject.Kind kind) { super(URI.create("mfm:///" + name.replace('.','/') + kind.extension), Kind.CLASS); this.location = location; this.name = name; } @Override public OutputStream openOutputStream() { return new FilterOutputStream(new ByteArrayOutputStream()) { @Override public void close() throws IOException { out.close(); byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); save(location, name, new Content() { @Override public byte[] getBytes() { return bytes; } @Override public String getString() { return new String(bytes); } }); } }; } @Override public Writer openWriter() { return new FilterWriter(new StringWriter()) { @Override public void close() throws IOException { out.close(); String text = ((StringWriter) out).toString(); save(location, name, new Content() { @Override public byte[] getBytes() { return text.getBytes(); } @Override public String getString() { return text; } }); } }; } } } }