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