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