f2c4a41304
Co-authored-by: Jim Laskey <jlaskey@openjdk.org> Reviewed-by: mcimadamore, vromero
1004 lines
38 KiB
Java
1004 lines
38 KiB
Java
/*
|
|
* Copyright (c) 2013, 2022, 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;
|
|
}
|
|
|
|
});
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|