Path: blob/master/test/langtools/tools/lib/toolbox/ToolBox.java
64474 views
/*1* Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223package toolbox;2425import java.io.BufferedWriter;26import java.io.ByteArrayOutputStream;27import java.io.FilterOutputStream;28import java.io.FilterWriter;29import java.io.IOException;30import java.io.OutputStream;31import java.io.PrintStream;32import java.io.StringWriter;33import java.io.Writer;34import java.net.URI;35import java.nio.charset.Charset;36import java.nio.file.FileAlreadyExistsException;37import java.nio.file.FileVisitOption;38import java.nio.file.FileVisitResult;39import java.nio.file.Files;40import java.nio.file.Path;41import java.nio.file.Paths;42import java.nio.file.SimpleFileVisitor;43import java.nio.file.StandardCopyOption;44import java.nio.file.attribute.BasicFileAttributes;45import java.util.ArrayList;46import java.util.Arrays;47import java.util.Collection;48import java.util.Collections;49import java.util.Deque;50import java.util.EnumSet;51import java.util.HashMap;52import java.util.LinkedList;53import java.util.List;54import java.util.Locale;55import java.util.Map;56import java.util.Objects;57import java.util.Set;58import java.util.TreeSet;59import java.util.regex.Matcher;60import java.util.regex.Pattern;61import java.util.stream.Collectors;62import java.util.stream.StreamSupport;6364import javax.tools.FileObject;65import javax.tools.ForwardingJavaFileManager;66import javax.tools.JavaFileManager;67import javax.tools.JavaFileObject;68import javax.tools.SimpleJavaFileObject;69import javax.tools.ToolProvider;7071/**72* Utility methods and classes for writing jtreg tests for73* javac, javah, javap, and sjavac. (For javadoc support,74* see JavadocTester.)75*76* <p>There is support for common file operations similar to77* shell commands like cat, cp, diff, mv, rm, grep.78*79* <p>There is also support for invoking various tools, like80* javac, javah, javap, jar, java and other JDK tools.81*82* <p><em>File separators</em>: for convenience, many operations accept strings83* to represent filenames. On all platforms on which JDK is supported,84* "/" is a legal filename component separator. In particular, even85* on Windows, where the official file separator is "\", "/" is a legal86* alternative. It is therefore recommended that any client code using87* strings to specify filenames should use "/".88*89* @author Vicente Romero (original)90* @author Jonathan Gibbons (revised)91*/92public class ToolBox {93/** The platform line separator. */94public static final String lineSeparator = System.getProperty("line.separator");95/** The platform OS name. */96public static final String osName = System.getProperty("os.name");9798/** The location of the class files for this test, or null if not set. */99public static final String testClasses = System.getProperty("test.classes");100/** The location of the source files for this test, or null if not set. */101public static final String testSrc = System.getProperty("test.src");102/** The location of the test JDK for this test, or null if not set. */103public static final String testJDK = System.getProperty("test.jdk");104/** The timeout factor for slow systems. */105public static final float timeoutFactor;106static {107String ttf = System.getProperty("test.timeout.factor");108timeoutFactor = (ttf == null) ? 1.0f : Float.parseFloat(ttf);109}110111/** The current directory. */112public static final Path currDir = Path.of(".");113114/** The stream used for logging output. */115public PrintStream out = System.err;116117/**118* Checks if the host OS is some version of Windows.119* @return true if the host OS is some version of Windows120*/121public static boolean isWindows() {122return osName.toLowerCase(Locale.ENGLISH).startsWith("windows");123}124125/**126* Splits a string around matches of the given regular expression.127* If the string is empty, an empty list will be returned.128*129* @param text the string to be split130* @param sep the delimiting regular expression131* @return the strings between the separators132*/133public List<String> split(String text, String sep) {134if (text.isEmpty())135return Collections.emptyList();136return Arrays.asList(text.split(sep));137}138139/**140* Checks if two lists of strings are equal.141*142* @param l1 the first list of strings to be compared143* @param l2 the second list of strings to be compared144* @throws Error if the lists are not equal145*/146public void checkEqual(List<String> l1, List<String> l2) throws Error {147if (!Objects.equals(l1, l2)) {148// l1 and l2 cannot both be null149if (l1 == null)150throw new Error("comparison failed: l1 is null");151if (l2 == null)152throw new Error("comparison failed: l2 is null");153// report first difference154for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) {155String s1 = l1.get(i);156String s2 = l2.get(i);157if (!Objects.equals(s1, s2)) {158throw new Error("comparison failed, index " + i +159", (" + s1 + ":" + s2 + ")");160}161}162throw new Error("comparison failed: l1.size=" + l1.size() + ", l2.size=" + l2.size());163}164}165166/**167* Filters a list of strings according to the given regular expression,168* returning the strings that match the regular expression.169*170* @param regex the regular expression171* @param lines the strings to be filtered172* @return the strings matching the regular expression173*/174public List<String> grep(String regex, List<String> lines) {175return grep(Pattern.compile(regex), lines, true);176}177178/**179* Filters a list of strings according to the given regular expression,180* returning the strings that match the regular expression.181*182* @param pattern the regular expression183* @param lines the strings to be filtered184* @return the strings matching the regular expression185*/186public List<String> grep(Pattern pattern, List<String> lines) {187return grep(pattern, lines, true);188}189190/**191* Filters a list of strings according to the given regular expression,192* returning either the strings that match or the strings that do not match.193*194* @param regex the regular expression195* @param lines the strings to be filtered196* @param match if true, return the lines that match; otherwise if false, return the lines that do not match.197* @return the strings matching(or not matching) the regular expression198*/199public List<String> grep(String regex, List<String> lines, boolean match) {200return grep(Pattern.compile(regex), lines, match);201}202203/**204* Filters a list of strings according to the given regular expression,205* returning either the strings that match or the strings that do not match.206*207* @param pattern the regular expression208* @param lines the strings to be filtered209* @param match if true, return the lines that match; otherwise if false, return the lines that do not match.210* @return the strings matching(or not matching) the regular expression211*/212public List<String> grep(Pattern pattern, List<String> lines, boolean match) {213return lines.stream()214.filter(s -> pattern.matcher(s).find() == match)215.collect(Collectors.toList());216}217218/**219* Copies a file.220* If the given destination exists and is a directory, the copy is created221* in that directory. Otherwise, the copy will be placed at the destination,222* possibly overwriting any existing file.223* <p>Similar to the shell "cp" command: {@code cp from to}.224*225* @param from the file to be copied226* @param to where to copy the file227* @throws IOException if any error occurred while copying the file228*/229public void copyFile(String from, String to) throws IOException {230copyFile(Path.of(from), Path.of(to));231}232233/**234* Copies a file.235* If the given destination exists and is a directory, the copy is created236* in that directory. Otherwise, the copy will be placed at the destination,237* possibly overwriting any existing file.238* <p>Similar to the shell "cp" command: {@code cp from to}.239*240* @param from the file to be copied241* @param to where to copy the file242* @throws IOException if an error occurred while copying the file243*/244public void copyFile(Path from, Path to) throws IOException {245if (Files.isDirectory(to)) {246to = to.resolve(from.getFileName());247} else {248Files.createDirectories(to.getParent());249}250Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);251}252253/**254* Copies the contents of a directory to another directory.255* <p>Similar to the shell command: {@code rsync fromDir/ toDir/}.256*257* @param fromDir the directory containing the files to be copied258* @param toDir the destination to which to copy the files259*/260public void copyDir(String fromDir, String toDir) {261copyDir(Path.of(fromDir), Path.of(toDir));262}263264/**265* Copies the contents of a directory to another directory.266* The destination direction should not already exist.267* <p>Similar to the shell command: {@code rsync fromDir/ toDir/}.268*269* @param fromDir the directory containing the files to be copied270* @param toDir the destination to which to copy the files271*/272public void copyDir(Path fromDir, Path toDir) {273try {274if (toDir.getParent() != null) {275Files.createDirectories(toDir.getParent());276}277Files.walkFileTree(fromDir, new SimpleFileVisitor<Path>() {278@Override279public FileVisitResult preVisitDirectory(Path fromSubdir, BasicFileAttributes attrs)280throws IOException {281Files.copy(fromSubdir, toDir.resolve(fromDir.relativize(fromSubdir)));282return FileVisitResult.CONTINUE;283}284285@Override286public FileVisitResult visitFile(Path fromFile, BasicFileAttributes attrs)287throws IOException {288Files.copy(fromFile, toDir.resolve(fromDir.relativize(fromFile)));289return FileVisitResult.CONTINUE;290}291});292} catch (IOException e) {293throw new Error("Could not copy " + fromDir + " to " + toDir + ": " + e, e);294}295}296297/**298* Creates one or more directories.299* For each of the series of paths, a directory will be created,300* including any necessary parent directories.301* <p>Similar to the shell command: {@code mkdir -p paths}.302*303* @param paths the directories to be created304* @throws IOException if an error occurred while creating the directories305*/306public void createDirectories(String... paths) throws IOException {307if (paths.length == 0)308throw new IllegalArgumentException("no directories specified");309for (String p : paths)310Files.createDirectories(Path.of(p));311}312313/**314* Creates one or more directories.315* For each of the series of paths, a directory will be created,316* including any necessary parent directories.317* <p>Similar to the shell command: {@code mkdir -p paths}.318*319* @param paths the directories to be created320* @throws IOException if an error occurred while creating the directories321*/322public void createDirectories(Path... paths) throws IOException {323if (paths.length == 0)324throw new IllegalArgumentException("no directories specified");325for (Path p : paths)326Files.createDirectories(p);327}328329/**330* Deletes one or more files, awaiting confirmation that the files331* no longer exist. Any directories to be deleted must be empty.332* <p>Similar to the shell command: {@code rm files}.333*334* @param files the names of the files to be deleted335* @throws IOException if an error occurred while deleting the files336*/337public void deleteFiles(String... files) throws IOException {338deleteFiles(List.of(files).stream().map(Paths::get).collect(Collectors.toList()));339}340341/**342* Deletes one or more files, awaiting confirmation that the files343* no longer exist. Any directories to be deleted must be empty.344* <p>Similar to the shell command: {@code rm files}.345*346* @param paths the paths for the files to be deleted347* @throws IOException if an error occurred while deleting the files348*/349public void deleteFiles(Path... paths) throws IOException {350deleteFiles(List.of(paths));351}352353/**354* Deletes one or more files, awaiting confirmation that the files355* no longer exist. Any directories to be deleted must be empty.356* <p>Similar to the shell command: {@code rm files}.357*358* @param paths the paths for the files to be deleted359* @throws IOException if an error occurred while deleting the files360*/361public void deleteFiles(List<Path> paths) throws IOException {362if (paths.isEmpty())363throw new IllegalArgumentException("no files specified");364IOException ioe = null;365for (Path path : paths) {366ioe = deleteFile(path, ioe);367}368if (ioe != null) {369throw ioe;370}371ensureDeleted(paths);372}373374/**375* Deletes all content of a directory (but not the directory itself),376* awaiting confirmation that the content has been deleted.377*378* @param root the directory to be cleaned379* @throws IOException if an error occurs while cleaning the directory380*/381public void cleanDirectory(Path root) throws IOException {382if (!Files.isDirectory(root)) {383throw new IOException(root + " is not a directory");384}385Files.walkFileTree(root, new SimpleFileVisitor<>() {386private IOException ioe = null;387// for each directory we visit, maintain a list of the files that we try to delete388private final Deque<List<Path>> dirFiles = new LinkedList<>();389390@Override391public FileVisitResult visitFile(Path file, BasicFileAttributes a) {392ioe = deleteFile(file, ioe);393dirFiles.peekFirst().add(file);394return FileVisitResult.CONTINUE;395}396397@Override398public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes a) {399if (!dir.equals(root)) {400dirFiles.peekFirst().add(dir);401}402dirFiles.addFirst(new ArrayList<>());403return FileVisitResult.CONTINUE;404}405406@Override407public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {408if (e != null) {409throw e;410}411if (ioe != null) {412throw ioe;413}414ensureDeleted(dirFiles.removeFirst());415if (!dir.equals(root)) {416ioe = deleteFile(dir, ioe);417}418return FileVisitResult.CONTINUE;419}420});421}422423/**424* Internal method to delete a file, using {@code Files.delete}.425* It does not wait to confirm deletion, nor does it retry.426* If an exception occurs it is either returned or added to the set of427* suppressed exceptions for an earlier exception.428*429* @param path the path for the file to be deleted430* @param ioe the earlier exception, or null431* @return the earlier exception or an exception that occurred while432* trying to delete the file433*/434private IOException deleteFile(Path path, IOException ioe) {435try {436Files.delete(path);437} catch (IOException e) {438if (ioe == null) {439ioe = e;440} else {441ioe.addSuppressed(e);442}443}444return ioe;445}446447/**448* Wait until it is confirmed that a set of files have been deleted.449*450* @param paths the paths for the files to be deleted451* @throws IOException if a file has not been deleted452*/453private void ensureDeleted(Collection<Path> paths)454throws IOException {455for (Path path : paths) {456ensureDeleted(path);457}458}459460/**461* Wait until it is confirmed that a file has been deleted.462*463* @param path the path for the file to be deleted464* @throws IOException if problems occur while deleting the file465*/466private void ensureDeleted(Path path) throws IOException {467long startTime = System.currentTimeMillis();468do {469// Note: Files.notExists is not the same as !Files.exists470if (Files.notExists(path)) {471return;472}473System.gc(); // allow finalizers and cleaners to run474try {475Thread.sleep(RETRY_DELETE_MILLIS);476} catch (InterruptedException e) {477throw new IOException("Interrupted while waiting for file to be deleted: " + path, e);478}479} while ((System.currentTimeMillis() - startTime) <= MAX_RETRY_DELETE_MILLIS);480481throw new IOException("File not deleted: " + path);482}483484private static final int RETRY_DELETE_MILLIS = isWindows() ? (int)(500 * timeoutFactor): 0;485private static final int MAX_RETRY_DELETE_MILLIS = isWindows() ? (int)(15 * 1000 * timeoutFactor) : 0;486487/**488* Moves a file.489* If the given destination exists and is a directory, the file will be moved490* to that directory. Otherwise, the file will be moved to the destination,491* possibly overwriting any existing file.492* <p>Similar to the shell "mv" command: {@code mv from to}.493*494* @param from the file to be moved495* @param to where to move the file496* @throws IOException if an error occurred while moving the file497*/498public void moveFile(String from, String to) throws IOException {499moveFile(Path.of(from), Path.of(to));500}501502/**503* Moves a file.504* If the given destination exists and is a directory, the file will be moved505* to that directory. Otherwise, the file will be moved to the destination,506* possibly overwriting any existing file.507* <p>Similar to the shell "mv" command: {@code mv from to}.508*509* @param from the file to be moved510* @param to where to move the file511* @throws IOException if an error occurred while moving the file512*/513public void moveFile(Path from, Path to) throws IOException {514if (Files.isDirectory(to)) {515to = to.resolve(from.getFileName());516} else {517Files.createDirectories(to.getParent());518}519Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);520}521522/**523* Reads the lines of a file.524* The file is read using the default character encoding.525*526* @param path the file to be read527* @return the lines of the file528* @throws IOException if an error occurred while reading the file529*/530public List<String> readAllLines(String path) throws IOException {531return readAllLines(path, null);532}533534/**535* Reads the lines of a file.536* The file is read using the default character encoding.537*538* @param path the file to be read539* @return the lines of the file540* @throws IOException if an error occurred while reading the file541*/542public List<String> readAllLines(Path path) throws IOException {543return readAllLines(path, null);544}545546/**547* Reads the lines of a file using the given encoding.548*549* @param path the file to be read550* @param encoding the encoding to be used to read the file551* @return the lines of the file.552* @throws IOException if an error occurred while reading the file553*/554public List<String> readAllLines(String path, String encoding) throws IOException {555return readAllLines(Path.of(path), encoding);556}557558/**559* Reads the lines of a file using the given encoding.560*561* @param path the file to be read562* @param encoding the encoding to be used to read the file563* @return the lines of the file564* @throws IOException if an error occurred while reading the file565*/566public List<String> readAllLines(Path path, String encoding) throws IOException {567return Files.readAllLines(path, getCharset(encoding));568}569570private Charset getCharset(String encoding) {571return (encoding == null) ? Charset.defaultCharset() : Charset.forName(encoding);572}573574/**575* Find .java files in one or more directories.576* <p>Similar to the shell "find" command: {@code find paths -name \*.java}.577*578* @param paths the directories in which to search for .java files579* @return the .java files found580* @throws IOException if an error occurred while searching for files581*/582public Path[] findJavaFiles(Path... paths) throws IOException {583return findFiles(".java", paths);584}585586/**587* Find files matching the file extension, in one or more directories.588* <p>Similar to the shell "find" command: {@code find paths -name \*.ext}.589*590* @param fileExtension the extension to search for591* @param paths the directories in which to search for files592* @return the files matching the file extension593* @throws IOException if an error occurred while searching for files594*/595public Path[] findFiles(String fileExtension, Path... paths) throws IOException {596Set<Path> files = new TreeSet<>(); // use TreeSet to force a consistent order597for (Path p : paths) {598Files.walkFileTree(p, new SimpleFileVisitor<>() {599@Override600public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {601if (file.getFileName().toString().endsWith(fileExtension)) {602files.add(file);603}604return FileVisitResult.CONTINUE;605}606});607}608return files.toArray(new Path[0]);609}610611/**612* Writes a file containing the given content.613* Any necessary directories for the file will be created.614*615* @param path where to write the file616* @param content the content for the file617* @throws IOException if an error occurred while writing the file618*/619public void writeFile(String path, String content) throws IOException {620writeFile(Path.of(path), content);621}622623/**624* Writes a file containing the given content.625* Any necessary directories for the file will be created.626*627* @param path where to write the file628* @param content the content for the file629* @throws IOException if an error occurred while writing the file630*/631public void writeFile(Path path, String content) throws IOException {632Path dir = path.getParent();633if (dir != null)634Files.createDirectories(dir);635try (BufferedWriter w = Files.newBufferedWriter(path)) {636w.write(content);637}638}639640/**641* Writes one or more files containing Java source code.642* For each file to be written, the filename will be inferred from the643* given base directory, the package declaration (if present) and from the644* the name of the first class, interface or enum declared in the file.645* <p>For example, if the base directory is /my/dir/ and the content646* contains "package p; class C { }", the file will be written to647* /my/dir/p/C.java.648* <p>Note: the content is analyzed using regular expressions;649* errors can occur if any contents have initial comments that might trip650* up the analysis.651*652* @param dir the base directory653* @param contents the contents of the files to be written654* @throws IOException if an error occurred while writing any of the files.655*/656public void writeJavaFiles(Path dir, String... contents) throws IOException {657if (contents.length == 0)658throw new IllegalArgumentException("no content specified for any files");659for (String c : contents) {660new JavaSource(c).write(dir);661}662}663664/**665* Returns the path for the binary of a JDK tool within {@link #testJDK}.666*667* @param tool the name of the tool668* @return the path of the tool669*/670public Path getJDKTool(String tool) {671return Path.of(testJDK, "bin", tool);672}673674/**675* Returns a string representing the contents of an {@code Iterable} as a list.676*677* @param <T> the type parameter of the {@code Iterable}678* @param items the iterable679* @return the string680*/681<T> String toString(Iterable<T> items) {682return StreamSupport.stream(items.spliterator(), false)683.map(Objects::toString)684.collect(Collectors.joining(",", "[", "]"));685}686687688/**689* An in-memory Java source file.690* It is able to extract the file name from simple source text using691* regular expressions.692*/693public static class JavaSource extends SimpleJavaFileObject {694private final String source;695696/**697* Creates a in-memory file object for Java source code.698*699* @param className the name of the class700* @param source the source text701*/702public JavaSource(String className, String source) {703super(URI.create(className), JavaFileObject.Kind.SOURCE);704this.source = source;705}706707/**708* Creates a in-memory file object for Java source code.709* The name of the class will be inferred from the source code.710*711* @param source the source text712*/713public JavaSource(String source) {714super(URI.create(getJavaFileNameFromSource(source)),715JavaFileObject.Kind.SOURCE);716this.source = source;717}718719/**720* Writes the source code to a file in the current directory.721*722* @throws IOException if there is a problem writing the file723*/724public void write() throws IOException {725write(currDir);726}727728/**729* Writes the source code to a file in a specified directory.730*731* @param dir the directory732* @throws IOException if there is a problem writing the file733*/734public void write(Path dir) throws IOException {735Path file = dir.resolve(getJavaFileNameFromSource(source));736Files.createDirectories(file.getParent());737try (BufferedWriter out = Files.newBufferedWriter(file)) {738out.write(source.replace("\n", lineSeparator));739}740}741742@Override743public CharSequence getCharContent(boolean ignoreEncodingErrors) {744return source;745}746747private final static Pattern commentPattern =748Pattern.compile("(?s)(\\s+//.*?\n|/\\*.*?\\*/)");749private final static Pattern modulePattern =750Pattern.compile("module\\s+((?:\\w+\\.)*)");751private final static Pattern packagePattern =752Pattern.compile("package\\s+(((?:\\w+\\.)*)\\w+)");753private final static Pattern classPattern =754Pattern.compile("(?:public\\s+)?(?:class|enum|interface|record)\\s+(\\w+)");755756/**757* Extracts the Java file name from the class declaration.758* This method is intended for simple files and uses regular expressions.759* Comments in the source are stripped before looking for the760* declarations from which the name is derived.761*/762static String getJavaFileNameFromSource(String source) {763StringBuilder sb = new StringBuilder();764Matcher matcher = commentPattern.matcher(source);765int start = 0;766while (matcher.find()) {767sb.append(source, start, matcher.start());768start = matcher.end();769}770sb.append(source.substring(start));771source = sb.toString();772773String packageName = null;774775matcher = modulePattern.matcher(source);776if (matcher.find())777return "module-info.java";778779matcher = packagePattern.matcher(source);780if (matcher.find()) {781packageName = matcher.group(1).replace(".", "/");782validateName(packageName);783}784785matcher = classPattern.matcher(source);786if (matcher.find()) {787String className = matcher.group(1) + ".java";788validateName(className);789return (packageName == null) ? className : packageName + "/" + className;790} else if (packageName != null) {791return packageName + "/package-info.java";792} else {793throw new Error("Could not extract the java class " +794"name from the provided source");795}796}797}798799/**800* Extracts the Java file name from the class declaration.801* This method is intended for simple files and uses regular expressions,802* so comments matching the pattern can make the method fail.803*804* @param source the source text805* @return the Java file name inferred from the source806* @deprecated This is a legacy method for compatibility with ToolBox v1.807* Use {@link JavaSource#getName JavaSource.getName} instead.808*/809@Deprecated810public static String getJavaFileNameFromSource(String source) {811return JavaSource.getJavaFileNameFromSource(source);812}813814private static final Set<String> RESERVED_NAMES = Set.of(815"con", "prn", "aux", "nul",816"com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9",817"lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9"818);819820/**821* Validates if a given name is a valid file name822* or path name on known platforms.823*824* @param name the name825* @throws IllegalArgumentException if the name is a reserved name826*/827public static void validateName(String name) {828for (String part : name.split("[./\\\\]")) {829if (RESERVED_NAMES.contains(part.toLowerCase(Locale.US))) {830throw new IllegalArgumentException("Name: " + name + " is" +831"a reserved name on Windows, " +832"and will not work!");833}834}835}836837public static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {838private interface Content {839byte[] getBytes();840String getString();841}842843/**844* Maps binary class names to generated content.845*/846private final Map<Location, Map<String, Content>> files;847848/**849* Constructs a memory file manager which stores output files in memory,850* and delegates to a default file manager for input files.851*/852public MemoryFileManager() {853this(ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null));854}855856/**857* Constructs a memory file manager which stores output files in memory,858* and delegates to a specified file manager for input files.859*860* @param fileManager the file manager to be used for input files861*/862public MemoryFileManager(JavaFileManager fileManager) {863super(fileManager);864files = new HashMap<>();865}866867@Override868public JavaFileObject getJavaFileForOutput(Location location,869String name,870JavaFileObject.Kind kind,871FileObject sibling)872{873return new MemoryFileObject(location, name, kind);874}875876/**877* Returns the set of names of files that have been written to a given878* location.879*880* @param location the location881* @return the set of file names882*/883public Set<String> getFileNames(Location location) {884Map<String, Content> filesForLocation = files.get(location);885return (filesForLocation == null)886? Collections.emptySet() : filesForLocation.keySet();887}888889/**890* Returns the content written to a file in a given location,891* or null if no such file has been written.892*893* @param location the location894* @param name the name of the file895* @return the content as an array of bytes896*/897public byte[] getFileBytes(Location location, String name) {898Content content = getFile(location, name);899return (content == null) ? null : content.getBytes();900}901902/**903* Returns the content written to a file in a given location,904* or null if no such file has been written.905*906* @param location the location907* @param name the name of the file908* @return the content as a string909*/910public String getFileString(Location location, String name) {911Content content = getFile(location, name);912return (content == null) ? null : content.getString();913}914915private Content getFile(Location location, String name) {916Map<String, Content> filesForLocation = files.get(location);917return (filesForLocation == null) ? null : filesForLocation.get(name);918}919920private void save(Location location, String name, Content content) {921files.computeIfAbsent(location, k -> new HashMap<>())922.put(name, content);923}924925/**926* A writable file object stored in memory.927*/928private class MemoryFileObject extends SimpleJavaFileObject {929private final Location location;930private final String name;931932/**933* Constructs a memory file object.934*935* @param location the location in which to save the file object936* @param name binary name of the class to be stored in this file object937* @param kind the kind of file object938*/939MemoryFileObject(Location location, String name, JavaFileObject.Kind kind) {940super(URI.create("mfm:///" + name.replace('.','/') + kind.extension),941Kind.CLASS);942this.location = location;943this.name = name;944}945946@Override947public OutputStream openOutputStream() {948return new FilterOutputStream(new ByteArrayOutputStream()) {949@Override950public void close() throws IOException {951out.close();952byte[] bytes = ((ByteArrayOutputStream) out).toByteArray();953save(location, name, new Content() {954@Override955public byte[] getBytes() {956return bytes;957}958@Override959public String getString() {960return new String(bytes);961}962963});964}965};966}967968@Override969public Writer openWriter() {970return new FilterWriter(new StringWriter()) {971@Override972public void close() throws IOException {973out.close();974String text = out.toString();975save(location, name, new Content() {976@Override977public byte[] getBytes() {978return text.getBytes();979}980@Override981public String getString() {982return text;983}984985});986}987};988}989}990}991}992993994995