| /* | |
| * Copyright (c) 2013, 2024, 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.Collection; | |
| import java.util.Collections; | |
| import java.util.Deque; | |
| import java.util.HashMap; | |
| import java.util.LinkedList; | |
| 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.SimpleJavaFileObject; | |
| import javax.tools.ToolProvider; | |
| /** | |
| * Utility methods and classes for writing jtreg tests for | |
| * javac, javah, and javap. (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 path separator. */ | |
| public static final String pathSeparator = System.getProperty("path.separator"); | |
| /** The platform file separator character. */ | |
| public static char fileSeparatorChar = System.getProperty("file.separator").charAt(0); | |
| /** 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 timeout factor for slow systems. */ | |
| public static final float timeoutFactor; | |
| static { | |
| String ttf = System.getProperty("test.timeout.factor"); | |
| timeoutFactor = (ttf == null) ? 1.0f : Float.parseFloat(ttf); | |
| } | |
| /** The current directory. */ | |
| public static final Path currDir = Path.of("."); | |
| /** 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 static 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 = l2.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, | |
| * returning the strings that match the 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, true); | |
| } | |
| /** | |
| * Filters a list of strings according to the given regular expression, | |
| * returning the strings that match the 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 grep(pattern, lines, true); | |
| } | |
| /** | |
| * Filters a list of strings according to the given regular expression, | |
| * returning either the strings that match or the strings that do not match. | |
| * | |
| * @param regex the regular expression | |
| * @param lines the strings to be filtered | |
| * @param match if true, return the lines that match; otherwise if false, return the lines that do not match. | |
| * @return the strings matching(or not matching) the regular expression | |
| */ | |
| public List<String> grep(String regex, List<String> lines, boolean match) { | |
| return grep(Pattern.compile(regex), lines, match); | |
| } | |
| /** | |
| * Filters a list of strings according to the given regular expression, | |
| * returning either the strings that match or the strings that do not match. | |
| * | |
| * @param pattern the regular expression | |
| * @param lines the strings to be filtered | |
| * @param match if true, return the lines that match; otherwise if false, return the lines that do not match. | |
| * @return the strings matching(or not matching) the regular expression | |
| */ | |
| public List<String> grep(Pattern pattern, List<String> lines, boolean match) { | |
| return lines.stream() | |
| .filter(s -> pattern.matcher(s).find() == match) | |
| .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(Path.of(from), Path.of(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 if (to.getParent() != null) { | |
| Files.createDirectories(to.getParent()); | |
| } | |
| Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); | |
| } | |
| /** | |
| * Copies the contents of a directory to another directory. | |
| * <p>Similar to the shell command: {@code rsync fromDir/ toDir/}. | |
| * | |
| * @param fromDir the directory containing the files to be copied | |
| * @param toDir the destination to which to copy the files | |
| */ | |
| public void copyDir(String fromDir, String toDir) { | |
| copyDir(Path.of(fromDir), Path.of(toDir)); | |
| } | |
| /** | |
| * Copies the contents of a directory to another directory. | |
| * The destination direction should not already exist. | |
| * <p>Similar to the shell command: {@code rsync fromDir/ toDir/}. | |
| * | |
| * @param fromDir the directory containing the files to be copied | |
| * @param toDir the destination to which to copy the files | |
| */ | |
| public void copyDir(Path fromDir, Path toDir) { | |
| try { | |
| if (toDir.getParent() != null) { | |
| Files.createDirectories(toDir.getParent()); | |
| } | |
| Files.walkFileTree(fromDir, new SimpleFileVisitor<Path>() { | |
| @Override | |
| public FileVisitResult preVisitDirectory(Path fromSubdir, BasicFileAttributes attrs) | |
| throws IOException { | |
| Files.copy(fromSubdir, toDir.resolve(fromDir.relativize(fromSubdir))); | |
| return FileVisitResult.CONTINUE; | |
| } | |
| @Override | |
| public FileVisitResult visitFile(Path fromFile, BasicFileAttributes attrs) | |
| throws IOException { | |
| Files.copy(fromFile, toDir.resolve(fromDir.relativize(fromFile))); | |
| return FileVisitResult.CONTINUE; | |
| } | |
| }); | |
| } catch (IOException e) { | |
| throw new Error("Could not copy " + fromDir + " to " + toDir + ": " + e, e); | |
| } | |
| } | |
| /** | |
| * 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(String... paths) throws IOException { | |
| if (paths.length == 0) | |
| throw new IllegalArgumentException("no directories specified"); | |
| for (String p : paths) | |
| Files.createDirectories(Path.of(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, awaiting confirmation that the files | |
| * no longer exist. Any directories to be deleted must be empty. | |
| * <p>Similar to the shell command: {@code rm files}. | |
| * | |
| * @param files the names of the files to be deleted | |
| * @throws IOException if an error occurred while deleting the files | |
| */ | |
| public void deleteFiles(String... files) throws IOException { | |
| deleteFiles(List.of(files).stream().map(Paths::get).collect(Collectors.toList())); | |
| } | |
| /** | |
| * Deletes one or more files, awaiting confirmation that the files | |
| * no longer exist. Any directories to be deleted must be empty. | |
| * <p>Similar to the shell command: {@code rm files}. | |
| * | |
| * @param paths the paths for the files to be deleted | |
| * @throws IOException if an error occurred while deleting the files | |
| */ | |
| public void deleteFiles(Path... paths) throws IOException { | |
| deleteFiles(List.of(paths)); | |
| } | |
| /** | |
| * Deletes one or more files, awaiting confirmation that the files | |
| * no longer exist. Any directories to be deleted must be empty. | |
| * <p>Similar to the shell command: {@code rm files}. | |
| * | |
| * @param paths the paths for the files to be deleted | |
| * @throws IOException if an error occurred while deleting the files | |
| */ | |
| public void deleteFiles(List<Path> paths) throws IOException { | |
| if (paths.isEmpty()) | |
| throw new IllegalArgumentException("no files specified"); | |
| IOException ioe = null; | |
| for (Path path : paths) { | |
| ioe = deleteFile(path, ioe); | |
| } | |
| if (ioe != null) { | |
| throw ioe; | |
| } | |
| ensureDeleted(paths); | |
| } | |
| /** | |
| * Deletes all content of a directory (but not the directory itself), | |
| * awaiting confirmation that the content has been deleted. | |
| * | |
| * @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<>() { | |
| private IOException ioe = null; | |
| // for each directory we visit, maintain a list of the files that we try to delete | |
| private final Deque<List<Path>> dirFiles = new LinkedList<>(); | |
| @Override | |
| public FileVisitResult visitFile(Path file, BasicFileAttributes a) { | |
| ioe = deleteFile(file, ioe); | |
| dirFiles.peekFirst().add(file); | |
| return FileVisitResult.CONTINUE; | |
| } | |
| @Override | |
| public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes a) { | |
| if (!dir.equals(root)) { | |
| dirFiles.peekFirst().add(dir); | |
| } | |
| dirFiles.addFirst(new ArrayList<>()); | |
| return FileVisitResult.CONTINUE; | |
| } | |
| @Override | |
| public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { | |
| if (e != null) { | |
| throw e; | |
| } | |
| if (ioe != null) { | |
| throw ioe; | |
| } | |
| ensureDeleted(dirFiles.removeFirst()); | |
| if (!dir.equals(root)) { | |
| ioe = deleteFile(dir, ioe); | |
| } | |
| return FileVisitResult.CONTINUE; | |
| } | |
| }); | |
| } | |
| /** | |
| * Internal method to delete a file, using {@code Files.delete}. | |
| * It does not wait to confirm deletion, nor does it retry. | |
| * If an exception occurs it is either returned or added to the set of | |
| * suppressed exceptions for an earlier exception. | |
| * | |
| * @param path the path for the file to be deleted | |
| * @param ioe the earlier exception, or null | |
| * @return the earlier exception or an exception that occurred while | |
| * trying to delete the file | |
| */ | |
| private IOException deleteFile(Path path, IOException ioe) { | |
| try { | |
| Files.delete(path); | |
| } catch (IOException e) { | |
| if (ioe == null) { | |
| ioe = e; | |
| } else { | |
| ioe.addSuppressed(e); | |
| } | |
| } | |
| return ioe; | |
| } | |
| /** | |
| * Wait until it is confirmed that a set of files have been deleted. | |
| * | |
| * @param paths the paths for the files to be deleted | |
| * @throws IOException if a file has not been deleted | |
| */ | |
| private void ensureDeleted(Collection<Path> paths) | |
| throws IOException { | |
| for (Path path : paths) { | |
| ensureDeleted(path); | |
| } | |
| } | |
| /** | |
| * Wait until it is confirmed that a file has been deleted. | |
| * | |
| * @param path the path for the file to be deleted | |
| * @throws IOException if problems occur while deleting the file | |
| */ | |
| private void ensureDeleted(Path path) throws IOException { | |
| long startTime = System.currentTimeMillis(); | |
| do { | |
| // Note: Files.notExists is not the same as !Files.exists | |
| if (Files.notExists(path)) { | |
| return; | |
| } | |
| System.gc(); // allow finalizers and cleaners to run | |
| try { | |
| Thread.sleep(RETRY_DELETE_MILLIS); | |
| } catch (InterruptedException e) { | |
| throw new IOException("Interrupted while waiting for file to be deleted: " + path, e); | |
| } | |
| } while ((System.currentTimeMillis() - startTime) <= MAX_RETRY_DELETE_MILLIS); | |
| throw new IOException("File not deleted: " + path); | |
| } | |
| private static final int RETRY_DELETE_MILLIS = isWindows() ? (int)(500 * timeoutFactor): 0; | |
| private static final int MAX_RETRY_DELETE_MILLIS = isWindows() ? (int)(15 * 1000 * timeoutFactor) : 0; | |
| /** | |
| * 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(Path.of(from), Path.of(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(Path.of(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 { | |
| return findFiles(".java", paths); | |
| } | |
| /** | |
| * Find files matching the file extension, in one or more directories. | |
| * <p>Similar to the shell "find" command: {@code find paths -name \*.ext}. | |
| * | |
| * @param fileExtension the extension to search for | |
| * @param paths the directories in which to search for files | |
| * @return the files matching the file extension | |
| * @throws IOException if an error occurred while searching for files | |
| */ | |
| public Path[] findFiles(String fileExtension, 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<>() { | |
| @Override | |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { | |
| if (file.getFileName().toString().endsWith(fileExtension)) { | |
| files.add(file); | |
| } | |
| return FileVisitResult.CONTINUE; | |
| } | |
| }); | |
| } | |
| return files.toArray(new Path[0]); | |
| } | |
| /** | |
| * 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(Path.of(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 Path.of(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 final static Pattern commentPattern = | |
| Pattern.compile("(?s)(\\s+//.*?\n|/\\*.*?\\*/)"); | |
| private final static Pattern importModulePattern = | |
| Pattern.compile("import\\s+module\\s+(((?:\\w+\\.)*)\\w+);"); | |
| private final static Pattern modulePattern = | |
| Pattern.compile("module\\s+((?:\\w+\\.)*)"); | |
| private final static Pattern packagePattern = | |
| Pattern.compile("package\\s+(((?:\\w+\\.)*)\\w+)"); | |
| private final static Pattern classPattern = | |
| Pattern.compile("(?:public\\s+)?(?:class|enum|interface|record)\\s+((\\w|\\$)+)"); | |
| /** | |
| * Extracts the Java file name from the class declaration. | |
| * This method is intended for simple files and uses regular expressions. | |
| * Comments in the source are stripped before looking for the | |
| * declarations from which the name is derived. | |
| */ | |
| static String getJavaFileNameFromSource(String source) { | |
| source = removeMatchingSpans(source, commentPattern); | |
| source = removeMatchingSpans(source, importModulePattern); | |
| Matcher matcher; | |
| String packageName = null; | |
| matcher = modulePattern.matcher(source); | |
| if (matcher.find()) | |
| return "module-info.java"; | |
| matcher = packagePattern.matcher(source); | |
| if (matcher.find()) { | |
| packageName = matcher.group(1).replace(".", "/"); | |
| validateName(packageName); | |
| } | |
| matcher = classPattern.matcher(source); | |
| if (matcher.find()) { | |
| String className = matcher.group(1) + ".java"; | |
| validateName(className); | |
| 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"); | |
| } | |
| } | |
| static String removeMatchingSpans(String source, Pattern toRemove) { | |
| StringBuilder sb = new StringBuilder(); | |
| Matcher matcher = toRemove.matcher(source); | |
| int start = 0; | |
| while (matcher.find()) { | |
| sb.append(source, start, matcher.start()); | |
| start = matcher.end(); | |
| } | |
| sb.append(source.substring(start)); | |
| return sb.toString(); | |
| } | |
| } | |
| /** | |
| * 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. | |
| * | |
| * @param source the source text | |
| * @return the Java file name inferred from the source | |
| * @deprecated This is a legacy method for compatibility with ToolBox v1. | |
| * Use {@link JavaSource#getName JavaSource.getName} instead. | |
| */ | |
| @Deprecated | |
| public static String getJavaFileNameFromSource(String source) { | |
| return JavaSource.getJavaFileNameFromSource(source); | |
| } | |
| private static final Set<String> RESERVED_NAMES = Set.of( | |
| "con", "prn", "aux", "nul", | |
| "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", | |
| "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9" | |
| ); | |
| /** | |
| * Validates if a given name is a valid file name | |
| * or path name on known platforms. | |
| * | |
| * @param name the name | |
| * @throws IllegalArgumentException if the name is a reserved name | |
| */ | |
| public static void validateName(String name) { | |
| for (String part : name.split("[./\\\\]")) { | |
| if (RESERVED_NAMES.contains(part.toLowerCase(Locale.US))) { | |
| throw new IllegalArgumentException("Name: " + name + " is" + | |
| "a reserved name on Windows, " + | |
| "and will not work!"); | |
| } | |
| } | |
| } | |
| public static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> { | |
| private interface Content { | |
| byte[] getBytes(); | |
| String getString(); | |
| } | |
| /** | |
| * Maps binary class names to generated content. | |
| */ | |
| private final Map<Location, Map<String, Content>> files; | |
| /** | |
| * Constructs 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)); | |
| } | |
| /** | |
| * Constructs 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) { | |
| files.computeIfAbsent(location, k -> new HashMap<>()) | |
| .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 location the location in which to save the file object | |
| * @param name binary name of the class to be stored in this file object | |
| * @param kind the kind of 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 = out.toString(); | |
| save(location, name, new Content() { | |
| @Override | |
| public byte[] getBytes() { | |
| return text.getBytes(); | |
| } | |
| @Override | |
| public String getString() { | |
| return text; | |
| } | |
| }); | |
| } | |
| }; | |
| } | |
| } | |
| } | |
| } |