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 }