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