1 /*
2  * Copyright (c) 2013, 2019, 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      */
isWindows()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      */
split(String text, String sep)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      */
checkEqual(List<String> l1, List<String> l2)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      */
grep(String regex, List<String> lines)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      */
grep(Pattern pattern, List<String> lines)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      */
copyFile(String from, String to)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      */
copyFile(Path from, Path to)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      */
createDirectories(String... paths)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      */
createDirectories(Path... paths)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      */
deleteFiles(String... files)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      */
deleteFiles(Path... paths)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      */
deleteFiles(List<Path> paths)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      */
cleanDirectory(Path root)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      */
deleteFile(Path path, IOException ioe)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      */
ensureDeleted(Collection<Path> paths)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      */
ensureDeleted(Path path)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      */
moveFile(String from, String to)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      */
moveFile(Path from, Path to)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      */
readAllLines(String path)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      */
readAllLines(Path path)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      */
readAllLines(String path, String encoding)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      */
readAllLines(Path path, String encoding)475     public List<String> readAllLines(Path path, String encoding) throws IOException {
476         return Files.readAllLines(path, getCharset(encoding));
477     }
478 
getCharset(String encoding)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      */
findJavaFiles(Path... paths)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      */
findFiles(String fileExtension, Path... paths)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      */
writeFile(String path, String content)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      */
writeFile(Path path, String content)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      */
writeJavaFiles(Path dir, String... contents)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      */
getJDKTool(String tool)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      */
toString(Iterable<T> items)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          */
JavaSource(String className, String source)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          */
JavaSource(String source)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          */
write()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          */
write(Path dir)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
getCharContent(boolean ignoreEncodingErrors)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|record)\\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          */
getJavaFileNameFromSource(String source)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
getJavaFileNameFromSource(String source)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 {
getBytes()716             byte[] getBytes();
getString()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          */
MemoryFileManager()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          */
MemoryFileManager(JavaFileManager fileManager)738         public MemoryFileManager(JavaFileManager fileManager) {
739             super(fileManager);
740             files = new HashMap<>();
741         }
742 
743         @Override
getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling)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          */
getFileNames(Location location)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          */
getFileBytes(Location location, String name)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          */
getFileString(Location location, String name)783         public String getFileString(Location location, String name) {
784             Content content = getFile(location, name);
785             return (content == null) ? null : content.getString();
786         }
787 
getFile(Location location, String name)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 
save(Location location, String name, Content content)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              */
MemoryFileObject(Location location, String name, JavaFileObject.Kind kind)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
openOutputStream()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
openWriter()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 
865