1 /*
   2  * Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package toolbox;
  25 
  26 import java.io.BufferedWriter;
  27 import java.io.ByteArrayOutputStream;
  28 import java.io.FilterOutputStream;
  29 import java.io.FilterWriter;
  30 import java.io.IOException;
  31 import java.io.OutputStream;
  32 import java.io.PrintStream;
  33 import java.io.StringWriter;
  34 import java.io.Writer;
  35 import java.net.URI;
  36 import java.nio.charset.Charset;
  37 import java.nio.file.FileVisitResult;
  38 import java.nio.file.Files;
  39 import java.nio.file.Path;
  40 import java.nio.file.Paths;
  41 import java.nio.file.SimpleFileVisitor;
  42 import java.nio.file.StandardCopyOption;
  43 import java.nio.file.attribute.BasicFileAttributes;
  44 import java.util.ArrayList;
  45 import java.util.Arrays;
  46 import java.util.Collection;
  47 import java.util.Collections;
  48 import java.util.Deque;
  49 import java.util.HashMap;
  50 import java.util.LinkedList;
  51 import java.util.List;
  52 import java.util.Locale;
  53 import java.util.Map;
  54 import java.util.Objects;
  55 import java.util.Set;
  56 import java.util.TreeSet;
  57 import java.util.regex.Matcher;
  58 import java.util.regex.Pattern;
  59 import java.util.stream.Collectors;
  60 import java.util.stream.StreamSupport;
  61 
  62 import javax.tools.FileObject;
  63 import javax.tools.ForwardingJavaFileManager;
  64 import javax.tools.JavaFileManager;
  65 import javax.tools.JavaFileObject;
  66 import javax.tools.SimpleJavaFileObject;
  67 import javax.tools.ToolProvider;
  68 
  69 /**
  70  * Utility methods and classes for writing jtreg tests for
  71  * javac, javah, and javap. (For javadoc support, see JavadocTester.)
  72  *
  73  * <p>There is support for common file operations similar to
  74  * shell commands like cat, cp, diff, mv, rm, grep.
  75  *
  76  * <p>There is also support for invoking various tools, like
  77  * javac, javah, javap, jar, java and other JDK tools.
  78  *
  79  * <p><em>File separators</em>: for convenience, many operations accept strings
  80  * to represent filenames. On all platforms on which JDK is supported,
  81  * "/" is a legal filename component separator. In particular, even
  82  * on Windows, where the official file separator is "\", "/" is a legal
  83  * alternative. It is therefore recommended that any client code using
  84  * strings to specify filenames should use "/".
  85  *
  86  * @author Vicente Romero (original)
  87  * @author Jonathan Gibbons (revised)
  88  */
  89 public class ToolBox {
  90     /** The platform line separator. */
  91     public static final String lineSeparator = System.getProperty("line.separator");
  92     /** The platform path separator. */
  93     public static final String pathSeparator = System.getProperty("path.separator");
  94     /** The platform file separator character. */
  95     public static char fileSeparatorChar = System.getProperty("file.separator").charAt(0);
  96     /** The platform OS name. */
  97     public static final String osName = System.getProperty("os.name");
  98 
  99     /** The location of the class files for this test, or null if not set. */
 100     public static final String testClasses = System.getProperty("test.classes");
 101     /** The location of the source files for this test, or null if not set. */
 102     public static final String testSrc = System.getProperty("test.src");
 103     /** The location of the test JDK for this test, or null if not set. */
 104     public static final String testJDK = System.getProperty("test.jdk");
 105 
 106     /** The current directory. */
 107     public static final Path currDir = Path.of(".");
 108 
 109     /** The stream used for logging output. */
 110     public PrintStream out = System.err;
 111 
 112     /**
 113      * Checks if the host OS is some version of Windows.
 114      * @return true if the host OS is some version of Windows
 115      */
 116     public static boolean isWindows() {
 117         return osName.toLowerCase(Locale.ENGLISH).startsWith("windows");
 118     }
 119 
 120     /**
 121      * Splits a string around matches of the given regular expression.
 122      * If the string is empty, an empty list will be returned.
 123      *
 124      * @param text the string to be split
 125      * @param sep  the delimiting regular expression
 126      * @return the strings between the separators
 127      */
 128     public List<String> split(String text, String sep) {
 129         if (text.isEmpty())
 130             return Collections.emptyList();
 131         return Arrays.asList(text.split(sep));
 132     }
 133 
 134     /**
 135      * Checks if two lists of strings are equal.
 136      *
 137      * @param l1 the first list of strings to be compared
 138      * @param l2 the second list of strings to be compared
 139      * @throws Error if the lists are not equal
 140      */
 141     public void checkEqual(List<String> l1, List<String> l2) throws Error {
 142         if (!Objects.equals(l1, l2)) {
 143             // l1 and l2 cannot both be null
 144             if (l1 == null)
 145                 throw new Error("comparison failed: l1 is null");
 146             if (l2 == null)
 147                 throw new Error("comparison failed: l2 is null");
 148             // report first difference
 149             for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) {
 150                 String s1 = l1.get(i);
 151                 String s2 = l2.get(i);
 152                 if (!Objects.equals(s1, s2)) {
 153                     throw new Error("comparison failed, index " + i +
 154                             ", (" + s1 + ":" + s2 + ")");
 155                 }
 156             }
 157             throw new Error("comparison failed: l1.size=" + l1.size() + ", l2.size=" + l2.size());
 158         }
 159     }
 160 
 161     /**
 162      * Filters a list of strings according to the given regular expression,
 163      * returning the strings that match the regular expression.
 164      *
 165      * @param regex the regular expression
 166      * @param lines the strings to be filtered
 167      * @return the strings matching the regular expression
 168      */
 169     public List<String> grep(String regex, List<String> lines) {
 170         return grep(Pattern.compile(regex), lines, true);
 171     }
 172 
 173     /**
 174      * Filters a list of strings according to the given regular expression,
 175      * returning the strings that match the regular expression.
 176      *
 177      * @param pattern the regular expression
 178      * @param lines   the strings to be filtered
 179      * @return the strings matching the regular expression
 180      */
 181     public List<String> grep(Pattern pattern, List<String> lines) {
 182         return grep(pattern, lines, true);
 183     }
 184 
 185     /**
 186      * Filters a list of strings according to the given regular expression,
 187      * returning either the strings that match or the strings that do not match.
 188      *
 189      * @param regex the regular expression
 190      * @param lines the strings to be filtered
 191      * @param match if true, return the lines that match; otherwise if false, return the lines that do not match.
 192      * @return the strings matching(or not matching) the regular expression
 193      */
 194     public List<String> grep(String regex, List<String> lines, boolean match) {
 195         return grep(Pattern.compile(regex), lines, match);
 196     }
 197 
 198     /**
 199      * Filters a list of strings according to the given regular expression,
 200      * returning either the strings that match or the strings that do not match.
 201      *
 202      * @param pattern the regular expression
 203      * @param lines   the strings to be filtered
 204      * @param match   if true, return the lines that match; otherwise if false, return the lines that do not match.
 205      * @return the strings matching(or not matching) the regular expression
 206      */
 207     public List<String> grep(Pattern pattern, List<String> lines, boolean match) {
 208         return lines.stream()
 209                 .filter(s -> pattern.matcher(s).find() == match)
 210                 .collect(Collectors.toList());
 211     }
 212 
 213     /**
 214      * Copies a file.
 215      * If the given destination exists and is a directory, the copy is created
 216      * in that directory.  Otherwise, the copy will be placed at the destination,
 217      * possibly overwriting any existing file.
 218      * <p>Similar to the shell "cp" command: {@code cp from to}.
 219      *
 220      * @param from the file to be copied
 221      * @param to   where to copy the file
 222      * @throws IOException if any error occurred while copying the file
 223      */
 224     public void copyFile(String from, String to) throws IOException {
 225         copyFile(Path.of(from), Path.of(to));
 226     }
 227 
 228     /**
 229      * Copies a file.
 230      * If the given destination exists and is a directory, the copy is created
 231      * in that directory.  Otherwise, the copy will be placed at the destination,
 232      * possibly overwriting any existing file.
 233      * <p>Similar to the shell "cp" command: {@code cp from to}.
 234      *
 235      * @param from the file to be copied
 236      * @param to   where to copy the file
 237      * @throws IOException if an error occurred while copying the file
 238      */
 239     public void copyFile(Path from, Path to) throws IOException {
 240         if (Files.isDirectory(to)) {
 241             to = to.resolve(from.getFileName());
 242         } else if (to.getParent() != null) {
 243             Files.createDirectories(to.getParent());
 244         }
 245         Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
 246     }
 247 
 248     /**
 249      * Copies the contents of a directory to another directory.
 250      * <p>Similar to the shell command: {@code rsync fromDir/ toDir/}.
 251      *
 252      * @param fromDir the directory containing the files to be copied
 253      * @param toDir   the destination to which to copy the files
 254      */
 255     public void copyDir(String fromDir, String toDir) {
 256         copyDir(Path.of(fromDir), Path.of(toDir));
 257     }
 258 
 259     /**
 260      * Copies the contents of a directory to another directory.
 261      * The destination direction should not already exist.
 262      * <p>Similar to the shell command: {@code rsync fromDir/ toDir/}.
 263      *
 264      * @param fromDir the directory containing the files to be copied
 265      * @param toDir   the destination to which to copy the files
 266      */
 267     public void copyDir(Path fromDir, Path toDir) {
 268         try {
 269             if (toDir.getParent() != null) {
 270                 Files.createDirectories(toDir.getParent());
 271             }
 272             Files.walkFileTree(fromDir, new SimpleFileVisitor<Path>() {
 273                 @Override
 274                 public FileVisitResult preVisitDirectory(Path fromSubdir, BasicFileAttributes attrs)
 275                         throws IOException {
 276                     Files.copy(fromSubdir, toDir.resolve(fromDir.relativize(fromSubdir)));
 277                     return FileVisitResult.CONTINUE;
 278                 }
 279 
 280                 @Override
 281                 public FileVisitResult visitFile(Path fromFile, BasicFileAttributes attrs)
 282                         throws IOException {
 283                     Files.copy(fromFile, toDir.resolve(fromDir.relativize(fromFile)));
 284                     return FileVisitResult.CONTINUE;
 285                 }
 286             });
 287         } catch (IOException e) {
 288             throw new Error("Could not copy " + fromDir + " to " + toDir + ": " + e, e);
 289         }
 290     }
 291 
 292     /**
 293      * Creates one or more directories.
 294      * For each of the series of paths, a directory will be created,
 295      * including any necessary parent directories.
 296      * <p>Similar to the shell command: {@code mkdir -p paths}.
 297      *
 298      * @param paths the directories to be created
 299      * @throws IOException if an error occurred while creating the directories
 300      */
 301     public void createDirectories(String... paths) throws IOException {
 302         if (paths.length == 0)
 303             throw new IllegalArgumentException("no directories specified");
 304         for (String p : paths)
 305             Files.createDirectories(Path.of(p));
 306     }
 307 
 308     /**
 309      * Creates one or more directories.
 310      * For each of the series of paths, a directory will be created,
 311      * including any necessary parent directories.
 312      * <p>Similar to the shell command: {@code mkdir -p paths}.
 313      *
 314      * @param paths the directories to be created
 315      * @throws IOException if an error occurred while creating the directories
 316      */
 317     public void createDirectories(Path... paths) throws IOException {
 318         if (paths.length == 0)
 319             throw new IllegalArgumentException("no directories specified");
 320         for (Path p : paths)
 321             Files.createDirectories(p);
 322     }
 323 
 324     /**
 325      * Deletes one or more files, awaiting confirmation that the files
 326      * no longer exist. Any directories to be deleted must be empty.
 327      * <p>Similar to the shell command: {@code rm files}.
 328      *
 329      * @param files the names of the files to be deleted
 330      * @throws IOException if an error occurred while deleting the files
 331      */
 332     public void deleteFiles(String... files) throws IOException {
 333         deleteFiles(List.of(files).stream().map(Paths::get).collect(Collectors.toList()));
 334     }
 335 
 336     /**
 337      * Deletes one or more files, awaiting confirmation that the files
 338      * no longer exist. Any directories to be deleted must be empty.
 339      * <p>Similar to the shell command: {@code rm files}.
 340      *
 341      * @param paths the paths for the files to be deleted
 342      * @throws IOException if an error occurred while deleting the files
 343      */
 344     public void deleteFiles(Path... paths) throws IOException {
 345         deleteFiles(List.of(paths));
 346     }
 347 
 348     /**
 349      * Deletes one or more files, awaiting confirmation that the files
 350      * no longer exist. Any directories to be deleted must be empty.
 351      * <p>Similar to the shell command: {@code rm files}.
 352      *
 353      * @param paths the paths for the files to be deleted
 354      * @throws IOException if an error occurred while deleting the files
 355      */
 356     public void deleteFiles(List<Path> paths) throws IOException {
 357         if (paths.isEmpty())
 358             throw new IllegalArgumentException("no files specified");
 359         IOException ioe = null;
 360         for (Path path : paths) {
 361             ioe = deleteFile(path, ioe);
 362         }
 363         if (ioe != null) {
 364             throw ioe;
 365         }
 366         ensureDeleted(paths);
 367     }
 368 
 369     /**
 370      * Deletes all content of a directory (but not the directory itself),
 371      * awaiting confirmation that the content has been deleted.
 372      *
 373      * @param root the directory to be cleaned
 374      * @throws IOException if an error occurs while cleaning the directory
 375      */
 376     public void cleanDirectory(Path root) throws IOException {
 377         if (!Files.isDirectory(root)) {
 378             throw new IOException(root + " is not a directory");
 379         }
 380         Files.walkFileTree(root, new SimpleFileVisitor<>() {
 381             private IOException ioe = null;
 382             // for each directory we visit, maintain a list of the files that we try to delete
 383             private final Deque<List<Path>> dirFiles = new LinkedList<>();
 384 
 385             @Override
 386             public FileVisitResult visitFile(Path file, BasicFileAttributes a) {
 387                 ioe = deleteFile(file, ioe);
 388                 dirFiles.peekFirst().add(file);
 389                 return FileVisitResult.CONTINUE;
 390             }
 391 
 392             @Override
 393             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes a) {
 394                 if (!dir.equals(root)) {
 395                     dirFiles.peekFirst().add(dir);
 396                 }
 397                 dirFiles.addFirst(new ArrayList<>());
 398                 return FileVisitResult.CONTINUE;
 399             }
 400 
 401             @Override
 402             public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
 403                 if (e != null) {
 404                     throw e;
 405                 }
 406                 if (ioe != null) {
 407                     throw ioe;
 408                 }
 409                 ensureDeleted(dirFiles.removeFirst());
 410                 if (!dir.equals(root)) {
 411                     ioe = deleteFile(dir, ioe);
 412                 }
 413                 return FileVisitResult.CONTINUE;
 414             }
 415         });
 416     }
 417 
 418     /**
 419      * Internal method to delete a file, using {@code Files.delete}.
 420      * It does not wait to confirm deletion, nor does it retry.
 421      * If an exception occurs it is either returned or added to the set of
 422      * suppressed exceptions for an earlier exception.
 423      *
 424      * @param path the path for the file to be deleted
 425      * @param ioe  the earlier exception, or null
 426      * @return the earlier exception or an exception that occurred while
 427      * trying to delete the file
 428      */
 429     private IOException deleteFile(Path path, IOException ioe) {
 430         try {
 431             Files.delete(path);
 432         } catch (IOException e) {
 433             if (ioe == null) {
 434                 ioe = e;
 435             } else {
 436                 ioe.addSuppressed(e);
 437             }
 438         }
 439         return ioe;
 440     }
 441 
 442     /**
 443      * Wait until it is confirmed that a set of files have been deleted.
 444      *
 445      * @param paths the paths for the files to be deleted
 446      * @throws IOException if a file has not been deleted
 447      */
 448     private void ensureDeleted(Collection<Path> paths)
 449             throws IOException {
 450         for (Path path : paths) {
 451             ensureDeleted(path);
 452         }
 453     }
 454 
 455     /**
 456      * Wait until it is confirmed that a file has been deleted.
 457      *
 458      * @param path the path for the file to be deleted
 459      * @throws IOException if problems occur while deleting the file
 460      */
 461     private void ensureDeleted(Path path) throws IOException {
 462         long startTime = System.currentTimeMillis();
 463         do {
 464             // Note: Files.notExists is not the same as !Files.exists
 465             if (Files.notExists(path)) {
 466                 return;
 467             }
 468             System.gc(); // allow finalizers and cleaners to run
 469             try {
 470                 Thread.sleep(RETRY_DELETE_MILLIS);
 471             } catch (InterruptedException e) {
 472                 throw new IOException("Interrupted while waiting for file to be deleted: " + path, e);
 473             }
 474         } while ((System.currentTimeMillis() - startTime) <= MAX_RETRY_DELETE_MILLIS);
 475 
 476         throw new IOException("File not deleted: " + path);
 477     }
 478 
 479     private static final int RETRY_DELETE_MILLIS = isWindows() ? 500 : 0;
 480     private static final int MAX_RETRY_DELETE_MILLIS = isWindows() ? 60 * 1000 : 0;
 481 
 482     /**
 483      * Moves a file.
 484      * If the given destination exists and is a directory, the file will be moved
 485      * to that directory.  Otherwise, the file will be moved to the destination,
 486      * possibly overwriting any existing file.
 487      * <p>Similar to the shell "mv" command: {@code mv from to}.
 488      *
 489      * @param from the file to be moved
 490      * @param to   where to move the file
 491      * @throws IOException if an error occurred while moving the file
 492      */
 493     public void moveFile(String from, String to) throws IOException {
 494         moveFile(Path.of(from), Path.of(to));
 495     }
 496 
 497     /**
 498      * Moves a file.
 499      * If the given destination exists and is a directory, the file will be moved
 500      * to that directory.  Otherwise, the file will be moved to the destination,
 501      * possibly overwriting any existing file.
 502      * <p>Similar to the shell "mv" command: {@code mv from to}.
 503      *
 504      * @param from the file to be moved
 505      * @param to   where to move the file
 506      * @throws IOException if an error occurred while moving the file
 507      */
 508     public void moveFile(Path from, Path to) throws IOException {
 509         if (Files.isDirectory(to)) {
 510             to = to.resolve(from.getFileName());
 511         } else {
 512             Files.createDirectories(to.getParent());
 513         }
 514         Files.move(from, to, StandardCopyOption.REPLACE_EXISTING);
 515     }
 516 
 517     /**
 518      * Reads the lines of a file.
 519      * The file is read using the default character encoding.
 520      *
 521      * @param path the file to be read
 522      * @return the lines of the file
 523      * @throws IOException if an error occurred while reading the file
 524      */
 525     public List<String> readAllLines(String path) throws IOException {
 526         return readAllLines(path, null);
 527     }
 528 
 529     /**
 530      * Reads the lines of a file.
 531      * The file is read using the default character encoding.
 532      *
 533      * @param path the file to be read
 534      * @return the lines of the file
 535      * @throws IOException if an error occurred while reading the file
 536      */
 537     public List<String> readAllLines(Path path) throws IOException {
 538         return readAllLines(path, null);
 539     }
 540 
 541     /**
 542      * Reads the lines of a file using the given encoding.
 543      *
 544      * @param path     the file to be read
 545      * @param encoding the encoding to be used to read the file
 546      * @return the lines of the file.
 547      * @throws IOException if an error occurred while reading the file
 548      */
 549     public List<String> readAllLines(String path, String encoding) throws IOException {
 550         return readAllLines(Path.of(path), encoding);
 551     }
 552 
 553     /**
 554      * Reads the lines of a file using the given encoding.
 555      *
 556      * @param path     the file to be read
 557      * @param encoding the encoding to be used to read the file
 558      * @return the lines of the file
 559      * @throws IOException if an error occurred while reading the file
 560      */
 561     public List<String> readAllLines(Path path, String encoding) throws IOException {
 562         return Files.readAllLines(path, getCharset(encoding));
 563     }
 564 
 565     private Charset getCharset(String encoding) {
 566         return (encoding == null) ? Charset.defaultCharset() : Charset.forName(encoding);
 567     }
 568 
 569     /**
 570      * Find .java files in one or more directories.
 571      * <p>Similar to the shell "find" command: {@code find paths -name \*.java}.
 572      *
 573      * @param paths the directories in which to search for .java files
 574      * @return the .java files found
 575      * @throws IOException if an error occurred while searching for files
 576      */
 577     public Path[] findJavaFiles(Path... paths) throws IOException {
 578         return findFiles(".java", paths);
 579     }
 580 
 581     /**
 582      * Find files matching the file extension, in one or more directories.
 583      * <p>Similar to the shell "find" command: {@code find paths -name \*.ext}.
 584      *
 585      * @param fileExtension the extension to search for
 586      * @param paths         the directories in which to search for files
 587      * @return the files matching the file extension
 588      * @throws IOException if an error occurred while searching for files
 589      */
 590     public Path[] findFiles(String fileExtension, Path... paths) throws IOException {
 591         Set<Path> files = new TreeSet<>();  // use TreeSet to force a consistent order
 592         for (Path p : paths) {
 593             Files.walkFileTree(p, new SimpleFileVisitor<>() {
 594                 @Override
 595                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
 596                     if (file.getFileName().toString().endsWith(fileExtension)) {
 597                         files.add(file);
 598                     }
 599                     return FileVisitResult.CONTINUE;
 600                 }
 601             });
 602         }
 603         return files.toArray(new Path[0]);
 604     }
 605 
 606     /**
 607      * Writes a file containing the given content.
 608      * Any necessary directories for the file will be created.
 609      *
 610      * @param path    where to write the file
 611      * @param content the content for the file
 612      * @throws IOException if an error occurred while writing the file
 613      */
 614     public void writeFile(String path, String content) throws IOException {
 615         writeFile(Path.of(path), content);
 616     }
 617 
 618     /**
 619      * Writes a file containing the given content.
 620      * Any necessary directories for the file will be created.
 621      *
 622      * @param path    where to write the file
 623      * @param content the content for the file
 624      * @throws IOException if an error occurred while writing the file
 625      */
 626     public void writeFile(Path path, String content) throws IOException {
 627         Path dir = path.getParent();
 628         if (dir != null)
 629             Files.createDirectories(dir);
 630         try (BufferedWriter w = Files.newBufferedWriter(path)) {
 631             w.write(content);
 632         }
 633     }
 634 
 635     /**
 636      * Writes one or more files containing Java source code.
 637      * For each file to be written, the filename will be inferred from the
 638      * given base directory, the package declaration (if present) and from the
 639      * the name of the first class, interface or enum declared in the file.
 640      * <p>For example, if the base directory is /my/dir/ and the content
 641      * contains "package p; class C { }", the file will be written to
 642      * /my/dir/p/C.java.
 643      * <p>Note: the content is analyzed using regular expressions;
 644      * errors can occur if any contents have initial comments that might trip
 645      * up the analysis.
 646      *
 647      * @param dir      the base directory
 648      * @param contents the contents of the files to be written
 649      * @throws IOException if an error occurred while writing any of the files.
 650      */
 651     public void writeJavaFiles(Path dir, String... contents) throws IOException {
 652         if (contents.length == 0)
 653             throw new IllegalArgumentException("no content specified for any files");
 654         for (String c : contents) {
 655             new JavaSource(c).write(dir);
 656         }
 657     }
 658 
 659     /**
 660      * Returns the path for the binary of a JDK tool within {@link #testJDK}.
 661      *
 662      * @param tool the name of the tool
 663      * @return the path of the tool
 664      */
 665     public Path getJDKTool(String tool) {
 666         return Path.of(testJDK, "bin", tool);
 667     }
 668 
 669     /**
 670      * Returns a string representing the contents of an {@code Iterable} as a list.
 671      *
 672      * @param <T>   the type parameter of the {@code Iterable}
 673      * @param items the iterable
 674      * @return the string
 675      */
 676     <T> String toString(Iterable<T> items) {
 677         return StreamSupport.stream(items.spliterator(), false)
 678                 .map(Objects::toString)
 679                 .collect(Collectors.joining(",", "[", "]"));
 680     }
 681 
 682 
 683     /**
 684      * An in-memory Java source file.
 685      * It is able to extract the file name from simple source text using
 686      * regular expressions.
 687      */
 688     public static class JavaSource extends SimpleJavaFileObject {
 689         private final String source;
 690 
 691         /**
 692          * Creates a in-memory file object for Java source code.
 693          *
 694          * @param className the name of the class
 695          * @param source    the source text
 696          */
 697         public JavaSource(String className, String source) {
 698             super(URI.create(className), JavaFileObject.Kind.SOURCE);
 699             this.source = source;
 700         }
 701 
 702         /**
 703          * Creates a in-memory file object for Java source code.
 704          * The name of the class will be inferred from the source code.
 705          *
 706          * @param source the source text
 707          */
 708         public JavaSource(String source) {
 709             super(URI.create(getJavaFileNameFromSource(source)),
 710                     JavaFileObject.Kind.SOURCE);
 711             this.source = source;
 712         }
 713 
 714         /**
 715          * Writes the source code to a file in the current directory.
 716          *
 717          * @throws IOException if there is a problem writing the file
 718          */
 719         public void write() throws IOException {
 720             write(currDir);
 721         }
 722 
 723         /**
 724          * Writes the source code to a file in a specified directory.
 725          *
 726          * @param dir the directory
 727          * @throws IOException if there is a problem writing the file
 728          */
 729         public void write(Path dir) throws IOException {
 730             Path file = dir.resolve(getJavaFileNameFromSource(source));
 731             Files.createDirectories(file.getParent());
 732             try (BufferedWriter out = Files.newBufferedWriter(file)) {
 733                 out.write(source.replace("\n", lineSeparator));
 734             }
 735         }
 736 
 737         @Override
 738         public CharSequence getCharContent(boolean ignoreEncodingErrors) {
 739             return source;
 740         }
 741 
 742         private final static Pattern commentPattern =
 743                 Pattern.compile("(?s)(\\s+//.*?\n|/\\*.*?\\*/)");
 744         private final static Pattern importModulePattern =
 745                 Pattern.compile("import\\s+module\\s+(((?:\\w+\\.)*)\\w+);");
 746         private final static Pattern modulePattern =
 747                 Pattern.compile("module\\s+((?:\\w+\\.)*)");
 748         private final static Pattern packagePattern =
 749                 Pattern.compile("package\\s+(((?:\\w+\\.)*)\\w+)");
 750         private final static Pattern classPattern =
 751                 Pattern.compile("(?:public\\s+)?(?:class|enum|interface|record)\\s+((\\w|\\$)+)");
 752 
 753         /**
 754          * Extracts the Java file name from the class declaration.
 755          * This method is intended for simple files and uses regular expressions.
 756          * Comments in the source are stripped before looking for the
 757          * declarations from which the name is derived.
 758          */
 759         static String getJavaFileNameFromSource(String source) {
 760             source = removeMatchingSpans(source, commentPattern);
 761             source = removeMatchingSpans(source, importModulePattern);
 762 
 763             Matcher matcher;
 764 
 765             String packageName = null;
 766 
 767             matcher = modulePattern.matcher(source);
 768             if (matcher.find())
 769                 return "module-info.java";
 770 
 771             matcher = packagePattern.matcher(source);
 772             if (matcher.find()) {
 773                 packageName = matcher.group(1).replace(".", "/");
 774                 validateName(packageName);
 775             }
 776 
 777             matcher = classPattern.matcher(source);
 778             if (matcher.find()) {
 779                 String className = matcher.group(1) + ".java";
 780                 validateName(className);
 781                 return (packageName == null) ? className : packageName + "/" + className;
 782             } else if (packageName != null) {
 783                 return packageName + "/package-info.java";
 784             } else {
 785                 throw new Error("Could not extract the java class " +
 786                         "name from the provided source");
 787             }
 788         }
 789 
 790         static String removeMatchingSpans(String source, Pattern toRemove) {
 791             StringBuilder sb = new StringBuilder();
 792             Matcher matcher = toRemove.matcher(source);
 793             int start = 0;
 794 
 795             while (matcher.find()) {
 796                 sb.append(source, start, matcher.start());
 797                 start = matcher.end();
 798             }
 799 
 800             sb.append(source.substring(start));
 801             return sb.toString();
 802         }
 803     }
 804 
 805     /**
 806      * Extracts the Java file name from the class declaration.
 807      * This method is intended for simple files and uses regular expressions,
 808      * so comments matching the pattern can make the method fail.
 809      *
 810      * @param source the source text
 811      * @return the Java file name inferred from the source
 812      * @deprecated This is a legacy method for compatibility with ToolBox v1.
 813      * Use {@link JavaSource#getName JavaSource.getName} instead.
 814      */
 815     @Deprecated
 816     public static String getJavaFileNameFromSource(String source) {
 817         return JavaSource.getJavaFileNameFromSource(source);
 818     }
 819 
 820     private static final Set<String> RESERVED_NAMES = Set.of(
 821         "con", "prn", "aux", "nul",
 822         "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8",  "com9",
 823         "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8",  "lpt9"
 824     );
 825 
 826     /**
 827      * Validates if a given name is a valid file name
 828      * or path name on known platforms.
 829      *
 830      * @param name the name
 831      * @throws IllegalArgumentException if the name is a reserved name
 832      */
 833     public static void validateName(String name) {
 834         for (String part : name.split("[./\\\\]")) {
 835             if (RESERVED_NAMES.contains(part.toLowerCase(Locale.US))) {
 836                 throw new IllegalArgumentException("Name: " + name + " is" +
 837                                                    "a reserved name on Windows, " +
 838                                                    "and will not work!");
 839             }
 840         }
 841     }
 842 
 843     public static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
 844         private interface Content {
 845             byte[] getBytes();
 846             String getString();
 847         }
 848 
 849         /**
 850          * Maps binary class names to generated content.
 851          */
 852         private final Map<Location, Map<String, Content>> files;
 853 
 854         /**
 855          * Whether the delegate is owned by this instance and should be closed when
 856          * this instance is closed.
 857          */
 858         private final boolean shouldClose;
 859 
 860         /**
 861          * Constructs a memory file manager which stores output files in memory,
 862          * and delegates to a default file manager for input files.
 863          */
 864         public MemoryFileManager() {
 865             this(ToolProvider.getSystemJavaCompiler().getStandardFileManager(null, null, null), true);
 866         }
 867 
 868         /**
 869          * Constructs a memory file manager which stores output files in memory,
 870          * and delegates to a specified file manager for input files.
 871          *
 872          * @param fileManager the file manager to be used for input files
 873          * @param shouldClose whether the delegate file manager should be closed
 874          *     when this instance is closed
 875          */
 876         public MemoryFileManager(JavaFileManager fileManager, boolean shouldClose) {
 877             super(fileManager);
 878             this.files = new HashMap<>();
 879             this.shouldClose = shouldClose;
 880         }
 881 
 882         @Override
 883         public void close() throws IOException {
 884             if (shouldClose) {
 885                 super.close();
 886             }
 887         }
 888 
 889         @Override
 890         public JavaFileObject getJavaFileForOutput(Location location,
 891                                                    String name,
 892                                                    JavaFileObject.Kind kind,
 893                                                    FileObject sibling)
 894         {
 895             return new MemoryFileObject(location, name, kind);
 896         }
 897 
 898         /**
 899          * Returns the set of names of files that have been written to a given
 900          * location.
 901          *
 902          * @param location the location
 903          * @return the set of file names
 904          */
 905         public Set<String> getFileNames(Location location) {
 906             Map<String, Content> filesForLocation = files.get(location);
 907             return (filesForLocation == null)
 908                 ? Collections.emptySet() : filesForLocation.keySet();
 909         }
 910 
 911         /**
 912          * Returns the content written to a file in a given location,
 913          * or null if no such file has been written.
 914          *
 915          * @param location the location
 916          * @param name     the name of the file
 917          * @return the content as an array of bytes
 918          */
 919         public byte[] getFileBytes(Location location, String name) {
 920             Content content = getFile(location, name);
 921             return (content == null) ? null : content.getBytes();
 922         }
 923 
 924         /**
 925          * Returns the content written to a file in a given location,
 926          * or null if no such file has been written.
 927          *
 928          * @param location the location
 929          * @param name     the name of the file
 930          * @return the content as a string
 931          */
 932         public String getFileString(Location location, String name) {
 933             Content content = getFile(location, name);
 934             return (content == null) ? null : content.getString();
 935         }
 936 
 937         private Content getFile(Location location, String name) {
 938             Map<String, Content> filesForLocation = files.get(location);
 939             return (filesForLocation == null) ? null : filesForLocation.get(name);
 940         }
 941 
 942         private void save(Location location, String name, Content content) {
 943             files.computeIfAbsent(location, k -> new HashMap<>())
 944                     .put(name, content);
 945         }
 946 
 947         /**
 948          * A writable file object stored in memory.
 949          */
 950         private class MemoryFileObject extends SimpleJavaFileObject {
 951             private final Location location;
 952             private final String name;
 953 
 954             /**
 955              * Constructs a memory file object.
 956              *
 957              * @param location the location in which to save the file object
 958              * @param name     binary name of the class to be stored in this file object
 959              * @param kind     the kind of file object
 960              */
 961             MemoryFileObject(Location location, String name, JavaFileObject.Kind kind) {
 962                 super(URI.create("mfm:///" + name.replace('.','/') + kind.extension),
 963                       Kind.CLASS);
 964                 this.location = location;
 965                 this.name = name;
 966             }
 967 
 968             @Override
 969             public OutputStream openOutputStream() {
 970                 return new FilterOutputStream(new ByteArrayOutputStream()) {
 971                     @Override
 972                     public void close() throws IOException {
 973                         out.close();
 974                         byte[] bytes = ((ByteArrayOutputStream) out).toByteArray();
 975                         save(location, name, new Content() {
 976                             @Override
 977                             public byte[] getBytes() {
 978                                 return bytes;
 979                             }
 980                             @Override
 981                             public String getString() {
 982                                 return new String(bytes);
 983                             }
 984 
 985                         });
 986                     }
 987                 };
 988             }
 989 
 990             @Override
 991             public Writer openWriter() {
 992                 return new FilterWriter(new StringWriter()) {
 993                     @Override
 994                     public void close() throws IOException {
 995                         out.close();
 996                         String text = out.toString();
 997                         save(location, name, new Content() {
 998                             @Override
 999                             public byte[] getBytes() {
1000                                 return text.getBytes();
1001                             }
1002                             @Override
1003                             public String getString() {
1004                                 return text;
1005                             }
1006 
1007                         });
1008                     }
1009                 };
1010             }
1011         }
1012     }
1013 }