| /* |
| * Copyright (c) 2013, 2016, 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. |
| */ |
| |
| package toolbox; |
| |
| import java.io.BufferedWriter; |
| import java.io.ByteArrayOutputStream; |
| import java.io.FilterOutputStream; |
| import java.io.FilterWriter; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| 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.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.StreamSupport; |
| |
| import javax.tools.FileObject; |
| import javax.tools.ForwardingJavaFileManager; |
| 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.ToolProvider; |
| |
| /** |
| * Utility methods and classes for writing jtreg tests for |
| * javac, javah, javap, and sjavac. (For javadoc support, |
| * see JavadocTester.) |
| * |
| * <p>There is support for common file operations similar to |
| * shell commands like cat, cp, diff, mv, rm, grep. |
| * |
| * <p>There is also support for invoking various tools, like |
| * javac, javah, javap, jar, java and other JDK tools. |
| * |
| * <p><em>File separators</em>: 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; |
| |
| /** |
| * 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<String> 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<String> l1, List<String> 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<String> grep(String regex, List<String> 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<String> grep(Pattern pattern, List<String> 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. |
| * <p>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. |
| * <p>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. |
| * <p>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. |
| * <p>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. |
| * <p>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)); |
| } |
| |
| /** |
| * Deletes all content of a directory (but not the directory itself). |
| * @param root the directory to be cleaned |
| * @throws IOException if an error occurs while cleaning the directory |
| */ |
| public void cleanDirectory(Path root) throws IOException { |
| if (!Files.isDirectory(root)) { |
| throw new IOException(root + " is not a directory"); |
| } |
| Files.walkFileTree(root, new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes a) throws IOException { |
| Files.delete(file); |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { |
| if (e != null) { |
| throw e; |
| } |
| if (!dir.equals(root)) { |
| Files.delete(dir); |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } |
| |
| /** |
| * 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. |
| * <p>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. |
| * <p>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<String> 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<String> 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<String> 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<String> 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); |
| } |
| |
| /** |
| * Find .java files in one or more directories. |
| * <p>Similar to the shell "find" command: {@code find paths -name \*.java}. |
| * @param paths the directories in which to search for .java files |
| * @return the .java files found |
| * @throws IOException if an error occurred while searching for files |
| */ |
| public Path[] findJavaFiles(Path... paths) throws IOException { |
| Set<Path> files = new TreeSet<>(); // use TreeSet to force a consistent order |
| for (Path p : paths) { |
| Files.walkFileTree(p, new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) |
| throws IOException { |
| if (file.getFileName().toString().endsWith(".java")) { |
| files.add(file); |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } |
| return files.toArray(new Path[files.size()]); |
| } |
| |
| /** |
| * 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. |
| * <p>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. |
| * <p>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 <T> the type parameter of the {@code Iterable} |
| * @param items the iterable |
| * @return the string |
| */ |
| <T> String toString(Iterable<T> items) { |
| return StreamSupport.stream(items.spliterator(), false) |
| .map(Objects::toString) |
| .collect(Collectors.joining(",", "[", "]")); |
| } |
| |
| |
| /** |
| * 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 modulePattern = |
| Pattern.compile("module\\s+((?:\\w+\\.)*)"); |
| 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 = modulePattern.matcher(source); |
| if (matcher.find()) |
| return "module-info.java"; |
| |
| 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 if (packageName != null) { |
| return packageName + "/package-info.java"; |
| } 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. |
| */ |
| private final Map<Location, Map<String, Content>> 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(ToolProvider.getSystemJavaCompiler().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 set of names of files that have been written to a given |
| * location. |
| * @param location the location |
| * @return the set of file names |
| */ |
| public Set<String> getFileNames(Location location) { |
| Map<String, Content> filesForLocation = files.get(location); |
| return (filesForLocation == null) |
| ? Collections.emptySet() : filesForLocation.keySet(); |
| } |
| |
| /** |
| * 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<String, Content> filesForLocation = files.get(location); |
| return (filesForLocation == null) ? null : filesForLocation.get(name); |
| } |
| |
| private void save(Location location, String name, Content content) { |
| Map<String, Content> 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; |
| } |
| |
| }); |
| } |
| }; |
| } |
| } |
| } |
| } |
| |