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