| /* |
| * Copyright (c) 2002, 2018, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileWriter; |
| import java.io.FilenameFilter; |
| import java.io.InputStreamReader; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.io.PrintWriter; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.lang.annotation.Annotation; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.ref.SoftReference; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.nio.charset.Charset; |
| import java.nio.charset.CharsetDecoder; |
| import java.nio.charset.CodingErrorAction; |
| import java.nio.charset.UnsupportedCharsetException; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.EnumMap; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.function.Function; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| |
| |
| /** |
| * Test framework for running javadoc and performing tests on the resulting output. |
| * |
| * <p> |
| * Tests are typically written as subtypes of JavadocTester, with a main |
| * method that creates an instance of the test class and calls the runTests() |
| * method. The runTests() methods calls all the test methods declared in the class, |
| * and then calls a method to print a summary, and throw an exception if |
| * any of the test methods reported a failure. |
| * |
| * <p> |
| * Test methods are identified with a @Test annotation. They have no parameters. |
| * The name of the method is not important, but if you have more than one, it is |
| * recommended that the names be meaningful and suggestive of the test case |
| * contained therein. |
| * |
| * <p> |
| * Typically, a test method will invoke javadoc, and then perform various |
| * checks on the results. The standard checks are: |
| * |
| * <dl> |
| * <dt>checkExitCode |
| * <dd>Check the exit code returned from javadoc. |
| * <dt>checkOutput |
| * <dd>Perform a series of checks on the contents on a file or output stream |
| * generated by javadoc. |
| * The checks can be either that a series of strings are found or are not found. |
| * <dt>checkFiles |
| * <dd>Perform a series of checks on the files generated by javadoc. |
| * The checks can be that a series of files are found or are not found. |
| * </dl> |
| * |
| * <pre><code> |
| * public class MyTester extends JavadocTester { |
| * public static void main(String... args) throws Exception { |
| * MyTester tester = new MyTester(); |
| * tester.runTests(); |
| * } |
| * |
| * // test methods... |
| * @Test |
| * void test() { |
| * javadoc(<i>args</i>); |
| * checkExit(Exit.OK); |
| * checkOutput(<i>file</i>, true, |
| * <i>strings-to-find</i>); |
| * checkOutput(<i>file</i>, false, |
| * <i>strings-to-not-find</i>); |
| * } |
| * } |
| * </code></pre> |
| * |
| * <p> |
| * If javadoc is run more than once in a test method, you can compare the |
| * results that are generated with the diff method. Since files written by |
| * javadoc typically contain a timestamp, you may want to use the -notimestamp |
| * option if you are going to compare the results from two runs of javadoc. |
| * |
| * <p> |
| * If you have many calls of checkOutput that are very similar, you can write |
| * your own check... method to reduce the amount of duplication. For example, |
| * if you want to check that many files contain the same string, you could |
| * write a method that takes a varargs list of files and calls checkOutput |
| * on each file in turn with the string to be checked. |
| * |
| * <p> |
| * You can also write you own custom check methods, which can use |
| * readFile to get the contents of a file generated by javadoc, |
| * and then use pass(...) or fail(...) to report whether the check |
| * succeeded or not. |
| * |
| * <p> |
| * You can have many separate test methods, each identified with a @Test |
| * annotation. However, you should <b>not</b> assume they will be called |
| * in the order declared in your source file. If the order of a series |
| * of javadoc invocations is important, do that within a single method. |
| * If the invocations are independent, for better clarity, use separate |
| * test methods, each with their own set of checks on the results. |
| * |
| * @author Doug Kramer |
| * @author Jamie Ho |
| * @author Jonathan Gibbons (rewrite) |
| */ |
| public abstract class JavadocTester { |
| |
| public static final String FS = System.getProperty("file.separator"); |
| public static final String PS = System.getProperty("path.separator"); |
| public static final String NL = System.getProperty("line.separator"); |
| public static final Path currDir = Paths.get(".").toAbsolutePath().normalize(); |
| |
| public enum Output { |
| /** The name of the output stream from javadoc. */ |
| OUT, |
| /** The name for any output written to System.out. */ |
| STDOUT, |
| /** The name for any output written to System.err. */ |
| STDERR |
| } |
| |
| /** The output directory used in the most recent call of javadoc. */ |
| protected File outputDir; |
| |
| /** The output charset used in the most recent call of javadoc. */ |
| protected Charset charset = Charset.defaultCharset(); |
| |
| /** The exit code of the most recent call of javadoc. */ |
| private int exitCode; |
| |
| /** The output generated by javadoc to the various writers and streams. */ |
| private final Map<Output, String> outputMap = new EnumMap<>(Output.class); |
| |
| /** A cache of file content, to avoid reading files unnecessarily. */ |
| private final Map<File,SoftReference<String>> fileContentCache = new HashMap<>(); |
| /** The charset used for files in the fileContentCache. */ |
| private Charset fileContentCacheCharset = null; |
| |
| /** Stream used for logging messages. */ |
| protected final PrintStream out = System.out; |
| |
| /** The directory containing the source code for the test. */ |
| public static final String testSrc = System.getProperty("test.src"); |
| |
| /** |
| * Get the path for a source file in the test source directory. |
| * @param path the path of a file or directory in the source directory |
| * @return the full path of the specified file |
| */ |
| public static String testSrc(String path) { |
| return new File(testSrc, path).getPath(); |
| } |
| |
| /** |
| * Alternatives for checking the contents of a directory. |
| */ |
| public enum DirectoryCheck { |
| /** |
| * Check that the directory is empty. |
| */ |
| EMPTY((file, name) -> true), |
| /** |
| * Check that the directory does not contain any HTML files, |
| * such as may have been generated by a prior run of javadoc |
| * using this directory. |
| * For now, the check is only performed on the top level directory. |
| */ |
| NO_HTML_FILES((file, name) -> name.endsWith(".html")), |
| /** |
| * No check is performed on the directory contents. |
| */ |
| NONE(null) { @Override void check(File dir) { } }; |
| |
| /** The filter used to detect that files should <i>not</i> be present. */ |
| FilenameFilter filter; |
| |
| DirectoryCheck(FilenameFilter f) { |
| filter = f; |
| } |
| |
| void check(File dir) { |
| if (dir.isDirectory()) { |
| String[] contents = dir.list(filter); |
| if (contents == null) |
| throw new Error("cannot list directory: " + dir); |
| if (contents.length > 0) { |
| System.err.println("Found extraneous files in dir:" + dir.getAbsolutePath()); |
| for (String x : contents) { |
| System.err.println(x); |
| } |
| throw new Error("directory has unexpected content: " + dir); |
| } |
| } |
| } |
| } |
| |
| private DirectoryCheck outputDirectoryCheck = DirectoryCheck.EMPTY; |
| |
| private boolean automaticCheckLinks = true; |
| |
| /** The current subtest number. Incremented when checking(...) is called. */ |
| private int numTestsRun = 0; |
| |
| /** The number of subtests passed. Incremented when passed(...) is called. */ |
| private int numTestsPassed = 0; |
| |
| /** The current run of javadoc. Incremented when javadoc is called. */ |
| private int javadocRunNum = 0; |
| |
| /** The current subtest number for this run of javadoc. Incremented when checking(...) is called. */ |
| private int javadocTestNum = 0; |
| |
| /** Marker annotation for test methods to be invoked by runTests. */ |
| @Retention(RetentionPolicy.RUNTIME) |
| @interface Test { } |
| |
| /** |
| * Run all methods annotated with @Test, followed by printSummary. |
| * Typically called on a tester object in main() |
| * @throws Exception if any errors occurred |
| */ |
| public void runTests() throws Exception { |
| runTests(m -> new Object[0]); |
| } |
| |
| /** |
| * Run all methods annotated with @Test, followed by printSummary. |
| * Typically called on a tester object in main() |
| * @param f a function which will be used to provide arguments to each |
| * invoked method |
| * @throws Exception if any errors occurred |
| */ |
| public void runTests(Function<Method, Object[]> f) throws Exception { |
| for (Method m: getClass().getDeclaredMethods()) { |
| Annotation a = m.getAnnotation(Test.class); |
| if (a != null) { |
| try { |
| out.println("Running test " + m.getName()); |
| m.invoke(this, f.apply(m)); |
| } catch (InvocationTargetException e) { |
| Throwable cause = e.getCause(); |
| throw (cause instanceof Exception) ? ((Exception) cause) : e; |
| } |
| out.println(); |
| } |
| } |
| printSummary(); |
| } |
| |
| /** |
| * Run javadoc. |
| * The output directory used by this call and the final exit code |
| * will be saved for later use. |
| * To aid the reader, it is recommended that calls to this method |
| * put each option and the arguments it takes on a separate line. |
| * |
| * Example: |
| * <pre><code> |
| * javadoc("-d", "out", |
| * "-sourcepath", testSrc, |
| * "-notimestamp", |
| * "pkg1", "pkg2", "pkg3/C.java"); |
| * </code></pre> |
| * |
| * @param args the arguments to pass to javadoc |
| */ |
| public void javadoc(String... args) { |
| outputMap.clear(); |
| fileContentCache.clear(); |
| |
| javadocRunNum++; |
| javadocTestNum = 0; // reset counter for this run of javadoc |
| if (javadocRunNum == 1) { |
| out.println("Running javadoc..."); |
| } else { |
| out.println("Running javadoc (run "+ javadocRunNum + ")..."); |
| } |
| |
| outputDir = new File("."); |
| String charsetArg = null; |
| String docencodingArg = null; |
| String encodingArg = null; |
| for (int i = 0; i < args.length - 2; i++) { |
| switch (args[i]) { |
| case "-d": |
| outputDir = new File(args[++i]); |
| break; |
| case "-charset": |
| charsetArg = args[++i]; |
| break; |
| case "-docencoding": |
| docencodingArg = args[++i]; |
| break; |
| case "-encoding": |
| encodingArg = args[++i]; |
| break; |
| } |
| } |
| |
| // The following replicates HtmlConfiguration.finishOptionSettings0 |
| // and sets up the charset used to read files. |
| String cs; |
| if (docencodingArg == null) { |
| if (charsetArg == null) { |
| cs = (encodingArg == null) ? "UTF-8" : encodingArg; |
| } else { |
| cs = charsetArg; |
| } |
| } else { |
| cs = docencodingArg; |
| } |
| try { |
| charset = Charset.forName(cs); |
| } catch (UnsupportedCharsetException e) { |
| charset = Charset.defaultCharset(); |
| } |
| |
| out.println("args: " + Arrays.toString(args)); |
| // log.setOutDir(outputDir); |
| |
| outputDirectoryCheck.check(outputDir); |
| |
| // This is the sole stream used by javadoc |
| WriterOutput outOut = new WriterOutput(); |
| |
| // These are to catch output to System.out and System.err, |
| // in case these are used instead of the primary streams |
| StreamOutput sysOut = new StreamOutput(System.out, System::setOut); |
| StreamOutput sysErr = new StreamOutput(System.err, System::setErr); |
| |
| try { |
| exitCode = jdk.javadoc.internal.tool.Main.execute(args, outOut.pw); |
| } finally { |
| outputMap.put(Output.STDOUT, sysOut.close()); |
| outputMap.put(Output.STDERR, sysErr.close()); |
| outputMap.put(Output.OUT, outOut.close()); |
| } |
| |
| outputMap.forEach((name, text) -> { |
| if (!text.isEmpty()) { |
| out.println("javadoc " + name + ":"); |
| out.println(text); |
| } |
| }); |
| |
| if (automaticCheckLinks && exitCode == Exit.OK.code && outputDir.exists()) { |
| checkLinks(); |
| } |
| } |
| |
| /** |
| * Set the kind of check for the initial contents of the output directory |
| * before javadoc is run. |
| * The filter should return true for files that should <b>not</b> appear. |
| * @param c the kind of check to perform |
| */ |
| public void setOutputDirectoryCheck(DirectoryCheck c) { |
| outputDirectoryCheck = c; |
| } |
| |
| /** |
| * Set whether or not to perform an automatic call of checkLinks. |
| */ |
| public void setAutomaticCheckLinks(boolean b) { |
| automaticCheckLinks = b; |
| } |
| |
| /** |
| * The exit codes returned by the javadoc tool. |
| * @see jdk.javadoc.internal.tool.Main.Result |
| */ |
| public enum Exit { |
| OK(0), // Javadoc completed with no errors. |
| ERROR(1), // Completed but reported errors. |
| CMDERR(2), // Bad command-line arguments |
| SYSERR(3), // System error or resource exhaustion. |
| ABNORMAL(4); // Javadoc terminated abnormally |
| |
| Exit(int code) { |
| this.code = code; |
| } |
| |
| final int code; |
| |
| @Override |
| public String toString() { |
| return name() + '(' + code + ')'; |
| } |
| } |
| |
| /** |
| * Check the exit code of the most recent call of javadoc. |
| * |
| * @param expected the exit code that is required for the test |
| * to pass. |
| */ |
| public void checkExit(Exit expected) { |
| checking("check exit code"); |
| if (exitCode == expected.code) { |
| passed("return code " + exitCode); |
| } else { |
| failed("return code " + exitCode +"; expected " + expected); |
| } |
| } |
| |
| /** |
| * Check for content in (or not in) the generated output. |
| * Within the search strings, the newline character \n |
| * will be translated to the platform newline character sequence. |
| * @param path a path within the most recent output directory |
| * or the name of one of the output buffers, identifying |
| * where to look for the search strings. |
| * @param expectedFound true if all of the search strings are expected |
| * to be found, or false if the file is not expected to be found |
| * @param strings the strings to be searched for |
| */ |
| public void checkFileAndOutput(String path, boolean expectedFound, String... strings) { |
| if (expectedFound) { |
| checkOutput(path, true, strings); |
| } else { |
| checkFiles(false, path); |
| } |
| } |
| |
| /** |
| * Check for content in (or not in) the generated output. |
| * Within the search strings, the newline character \n |
| * will be translated to the platform newline character sequence. |
| * @param path a path within the most recent output directory, identifying |
| * where to look for the search strings. |
| * @param expectedFound true if all of the search strings are expected |
| * to be found, or false if all of the strings are expected to be |
| * not found |
| * @param strings the strings to be searched for |
| */ |
| public void checkOutput(String path, boolean expectedFound, String... strings) { |
| // Read contents of file |
| try { |
| String fileString = readFile(outputDir, path); |
| checkOutput(new File(outputDir, path).getPath(), fileString, expectedFound, strings); |
| } catch (Error e) { |
| checking("Read file"); |
| failed("Error reading file: " + e); |
| } |
| } |
| |
| /** |
| * Check for content in (or not in) the one of the output streams written by |
| * javadoc. Within the search strings, the newline character \n |
| * will be translated to the platform newline character sequence. |
| * @param output the output stream to check |
| * @param expectedFound true if all of the search strings are expected |
| * to be found, or false if all of the strings are expected to be |
| * not found |
| * @param strings the strings to be searched for |
| */ |
| public void checkOutput(Output output, boolean expectedFound, String... strings) { |
| checkOutput(output.toString(), outputMap.get(output), expectedFound, strings); |
| } |
| |
| // NOTE: path may be the name of an Output stream as well as a file path |
| private void checkOutput(String path, String fileString, boolean expectedFound, String... strings) { |
| for (String stringToFind : strings) { |
| // log.logCheckOutput(path, expectedFound, stringToFind); |
| checking("checkOutput"); |
| // Find string in file's contents |
| boolean isFound = findString(fileString, stringToFind); |
| if (isFound == expectedFound) { |
| passed(path + ": following text " + (isFound ? "found:" : "not found:") + "\n" |
| + stringToFind); |
| } else { |
| failed(path + ": following text " + (isFound ? "found:" : "not found:") + "\n" |
| + stringToFind + '\n' + |
| "found \n" + |
| fileString); |
| } |
| } |
| } |
| |
| public void checkLinks() { |
| checking("Check links"); |
| LinkChecker c = new LinkChecker(out, this::readFile); |
| try { |
| c.checkDirectory(outputDir.toPath()); |
| c.report(); |
| int errors = c.getErrorCount(); |
| if (errors == 0) { |
| passed("Links are OK"); |
| } else { |
| failed(errors + " errors found when checking links"); |
| } |
| } catch (IOException e) { |
| failed("exception thrown when reading files: " + e); |
| } |
| } |
| |
| /** |
| * Get the content of the one of the output streams written by javadoc. |
| * @param output the name of the output stream |
| * @return the content of the output stream |
| */ |
| public String getOutput(Output output) { |
| return outputMap.get(output); |
| } |
| |
| /** |
| * Get the content of the one of the output streams written by javadoc. |
| * @param output the name of the output stream |
| * @return the content of the output stream, as a line of lines |
| */ |
| public List<String> getOutputLines(Output output) { |
| String text = outputMap.get(output); |
| return (text == null) ? Collections.emptyList() : Arrays.asList(text.split(NL)); |
| } |
| |
| /** |
| * Check for files in (or not in) the generated output. |
| * @param expectedFound true if all of the files are expected |
| * to be found, or false if all of the files are expected to be |
| * not found |
| * @param paths the files to check, within the most recent output directory. |
| * */ |
| public void checkFiles(boolean expectedFound, String... paths) { |
| checkFiles(expectedFound, Arrays.asList(paths)); |
| } |
| |
| /** |
| * Check for files in (or not in) the generated output. |
| * @param expectedFound true if all of the files are expected |
| * to be found, or false if all of the files are expected to be |
| * not found |
| * @param paths the files to check, within the most recent output directory. |
| * */ |
| public void checkFiles(boolean expectedFound, Collection<String> paths) { |
| for (String path: paths) { |
| // log.logCheckFile(path, expectedFound); |
| checking("checkFile"); |
| File file = new File(outputDir, path); |
| boolean isFound = file.exists(); |
| if (isFound == expectedFound) { |
| passed(file, "file " + (isFound ? "found:" : "not found:") + "\n"); |
| } else { |
| failed(file, "file " + (isFound ? "found:" : "not found:") + "\n"); |
| } |
| } |
| } |
| |
| /** |
| * Check that a series of strings are found in order in a file in |
| * the generated output. |
| * @param path the file to check |
| * @param strings the strings whose order to check |
| */ |
| public void checkOrder(String path, String... strings) { |
| File file = new File(outputDir, path); |
| String fileString = readOutputFile(path); |
| int prevIndex = -1; |
| for (String s : strings) { |
| s = s.replace("\n", NL); // normalize new lines |
| int currentIndex = fileString.indexOf(s, prevIndex + 1); |
| checking("file: " + file + ": " + s + " at index " + currentIndex); |
| if (currentIndex == -1) { |
| failed(file, s + " not found."); |
| continue; |
| } |
| if (currentIndex > prevIndex) { |
| passed(file, s + " is in the correct order"); |
| } else { |
| failed(file, s + " is in the wrong order."); |
| } |
| prevIndex = currentIndex; |
| } |
| } |
| |
| /** |
| * Ensures that a series of strings appear only once, in the generated output, |
| * noting that, this test does not exhaustively check for all other possible |
| * duplicates once one is found. |
| * @param path the file to check |
| * @param strings ensure each are unique |
| */ |
| public void checkUnique(String path, String... strings) { |
| File file = new File(outputDir, path); |
| String fileString = readOutputFile(path); |
| for (String s : strings) { |
| int currentIndex = fileString.indexOf(s); |
| checking(s + " at index " + currentIndex); |
| if (currentIndex == -1) { |
| failed(file, s + " not found."); |
| continue; |
| } |
| int nextindex = fileString.indexOf(s, currentIndex + s.length()); |
| if (nextindex == -1) { |
| passed(file, s + " is unique"); |
| } else { |
| failed(file, s + " is not unique, found at " + nextindex); |
| } |
| } |
| } |
| |
| /** |
| * Compare a set of files in each of two directories. |
| * |
| * @param baseDir1 the directory containing the first set of files |
| * @param baseDir2 the directory containing the second set of files |
| * @param files the set of files to be compared |
| */ |
| public void diff(String baseDir1, String baseDir2, String... files) { |
| File bd1 = new File(baseDir1); |
| File bd2 = new File(baseDir2); |
| for (String file : files) { |
| diff(bd1, bd2, file); |
| } |
| } |
| |
| /** |
| * A utility to copy a directory from one place to another. |
| * |
| * @param targetDir the directory to copy. |
| * @param destDir the destination to copy the directory to. |
| */ |
| // TODO: convert to using java.nio.Files.walkFileTree |
| public void copyDir(String targetDir, String destDir) { |
| try { |
| File targetDirObj = new File(targetDir); |
| File destDirParentObj = new File(destDir); |
| File destDirObj = new File(destDirParentObj, targetDirObj.getName()); |
| if (! destDirParentObj.exists()) { |
| destDirParentObj.mkdir(); |
| } |
| if (! destDirObj.exists()) { |
| destDirObj.mkdir(); |
| } |
| String[] files = targetDirObj.list(); |
| for (String file : files) { |
| File srcFile = new File(targetDirObj, file); |
| File destFile = new File(destDirObj, file); |
| if (srcFile.isFile()) { |
| out.println("Copying " + srcFile + " to " + destFile); |
| copyFile(destFile, srcFile); |
| } else if(srcFile.isDirectory()) { |
| copyDir(srcFile.getAbsolutePath(), destDirObj.getAbsolutePath()); |
| } |
| } |
| } catch (IOException exc) { |
| throw new Error("Could not copy " + targetDir + " to " + destDir); |
| } |
| } |
| |
| /** |
| * Copy source file to destination file. |
| * |
| * @param destfile the destination file |
| * @param srcfile the source file |
| * @throws IOException |
| */ |
| public void copyFile(File destfile, File srcfile) throws IOException { |
| Files.copy(srcfile.toPath(), destfile.toPath()); |
| } |
| |
| /** |
| * Read a file from the output directory. |
| * |
| * @param fileName the name of the file to read |
| * @return the file in string format |
| */ |
| public String readOutputFile(String fileName) throws Error { |
| return readFile(outputDir, fileName); |
| } |
| |
| protected String readFile(String fileName) throws Error { |
| return readFile(outputDir, fileName); |
| } |
| |
| protected String readFile(String baseDir, String fileName) throws Error { |
| return readFile(new File(baseDir), fileName); |
| } |
| |
| private String readFile(Path file) { |
| File baseDir; |
| if (file.startsWith(outputDir.toPath())) { |
| baseDir = outputDir; |
| } else if (file.startsWith(currDir)) { |
| baseDir = currDir.toFile(); |
| } else { |
| baseDir = file.getParent().toFile(); |
| } |
| String fileName = baseDir.toPath().relativize(file).toString(); |
| return readFile(baseDir, fileName); |
| } |
| |
| /** |
| * Read the file and return it as a string. |
| * |
| * @param baseDir the directory in which to locate the file |
| * @param fileName the name of the file to read |
| * @return the file in string format |
| */ |
| private String readFile(File baseDir, String fileName) throws Error { |
| if (!Objects.equals(fileContentCacheCharset, charset)) { |
| fileContentCache.clear(); |
| fileContentCacheCharset = charset; |
| } |
| try { |
| File file = new File(baseDir, fileName); |
| SoftReference<String> ref = fileContentCache.get(file); |
| String content = (ref == null) ? null : ref.get(); |
| if (content != null) |
| return content; |
| |
| // charset defaults to a value inferred from latest javadoc run |
| content = new String(Files.readAllBytes(file.toPath()), charset); |
| fileContentCache.put(file, new SoftReference<>(content)); |
| return content; |
| } catch (FileNotFoundException e) { |
| throw new Error("File not found: " + fileName + ": " + e); |
| } catch (IOException e) { |
| throw new Error("Error reading file: " + fileName + ": " + e); |
| } |
| } |
| |
| protected void checking(String message) { |
| numTestsRun++; |
| javadocTestNum++; |
| print("Starting subtest " + javadocRunNum + "." + javadocTestNum, message); |
| } |
| |
| protected void passed(File file, String message) { |
| passed(file + ": " + message); |
| } |
| |
| protected void passed(String message) { |
| numTestsPassed++; |
| print("Passed", message); |
| out.println(); |
| } |
| |
| protected void failed(File file, String message) { |
| failed(file + ": " + message); |
| } |
| |
| protected void failed(String message) { |
| print("FAILED", message); |
| StackWalker.getInstance().walk(s -> { |
| s.dropWhile(f -> f.getMethodName().equals("failed")) |
| .takeWhile(f -> !f.getMethodName().equals("runTests")) |
| .forEach(f -> out.println(" at " |
| + f.getClassName() + "." + f.getMethodName() |
| + "(" + f.getFileName() + ":" + f.getLineNumber() + ")")); |
| return null; |
| }); |
| out.println(); |
| } |
| |
| private void print(String prefix, String message) { |
| if (message.isEmpty()) |
| out.println(prefix); |
| else { |
| out.print(prefix); |
| out.print(": "); |
| out.print(message.replace("\n", NL)); |
| if (!(message.endsWith("\n") || message.endsWith(NL))) { |
| out.println(); |
| } |
| } |
| } |
| |
| /** |
| * Print a summary of the test results. |
| */ |
| protected void printSummary() { |
| String javadocRuns = (javadocRunNum <= 1) ? "" |
| : ", in " + javadocRunNum + " runs of javadoc"; |
| |
| if (numTestsRun != 0 && numTestsPassed == numTestsRun) { |
| // Test passed |
| out.println(); |
| out.println("All " + numTestsPassed + " subtests passed" + javadocRuns); |
| } else { |
| // Test failed |
| throw new Error((numTestsRun - numTestsPassed) |
| + " of " + (numTestsRun) |
| + " subtests failed" |
| + javadocRuns); |
| } |
| } |
| |
| /** |
| * Search for the string in the given file and return true |
| * if the string was found. |
| * |
| * @param fileString the contents of the file to search through |
| * @param stringToFind the string to search for |
| * @return true if the string was found |
| */ |
| private boolean findString(String fileString, String stringToFind) { |
| // javadoc (should) always use the platform newline sequence, |
| // but in the strings to find it is more convenient to use the Java |
| // newline character. So we translate \n to NL before we search. |
| stringToFind = stringToFind.replace("\n", NL); |
| return fileString.contains(stringToFind); |
| } |
| |
| /** |
| * Compare the two given files. |
| * |
| * @param baseDir1 the directory in which to locate the first file |
| * @param baseDir2 the directory in which to locate the second file |
| * @param file the file to compare in the two base directories |
| * @param throwErrorIFNoMatch flag to indicate whether or not to throw |
| * an error if the files do not match. |
| * @return true if the files are the same and false otherwise. |
| */ |
| private void diff(File baseDir1, File baseDir2, String file) { |
| String file1Contents = readFile(baseDir1, file); |
| String file2Contents = readFile(baseDir2, file); |
| checking("diff " + new File(baseDir1, file) + ", " + new File(baseDir2, file)); |
| if (file1Contents.trim().compareTo(file2Contents.trim()) == 0) { |
| passed("files are equal"); |
| } else { |
| failed("files differ"); |
| } |
| } |
| |
| /** |
| * Utility class to simplify the handling of temporarily setting a |
| * new stream for System.out or System.err. |
| */ |
| private static class StreamOutput { |
| // functional interface to set a stream. |
| private interface Initializer { |
| void set(PrintStream s); |
| } |
| |
| private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| private final PrintStream ps = new PrintStream(baos); |
| private final PrintStream prev; |
| private final Initializer init; |
| |
| StreamOutput(PrintStream s, Initializer init) { |
| prev = s; |
| init.set(ps); |
| this.init = init; |
| } |
| |
| String close() { |
| init.set(prev); |
| ps.close(); |
| return baos.toString(); |
| } |
| } |
| |
| /** |
| * Utility class to simplify the handling of creating an in-memory PrintWriter. |
| */ |
| private static class WriterOutput { |
| private final StringWriter sw = new StringWriter(); |
| final PrintWriter pw = new PrintWriter(sw); |
| String close() { |
| pw.close(); |
| return sw.toString(); |
| } |
| } |
| |
| |
| // private final Logger log = new Logger(); |
| |
| //--------- Logging -------------------------------------------------------- |
| // |
| // This class writes out the details of calls to checkOutput and checkFile |
| // in a canonical way, so that the resulting file can be checked against |
| // similar files from other versions of JavadocTester using the same logging |
| // facilities. |
| |
| static class Logger { |
| private static final int PREFIX = 40; |
| private static final int SUFFIX = 20; |
| private static final int MAX = PREFIX + SUFFIX; |
| List<String> tests = new ArrayList<>(); |
| String outDir; |
| String rootDir = rootDir(); |
| |
| static String rootDir() { |
| File f = new File(".").getAbsoluteFile(); |
| while (!new File(f, ".hg").exists()) |
| f = f.getParentFile(); |
| return f.getPath(); |
| } |
| |
| void setOutDir(File outDir) { |
| this.outDir = outDir.getPath(); |
| } |
| |
| void logCheckFile(String file, boolean positive) { |
| // Strip the outdir because that will typically not be the same |
| if (file.startsWith(outDir + "/")) |
| file = file.substring(outDir.length() + 1); |
| tests.add(file + " " + positive); |
| } |
| |
| void logCheckOutput(String file, boolean positive, String text) { |
| // Compress the string to be displayed in the log file |
| String simpleText = text.replaceAll("\\s+", " ").replace(rootDir, "[ROOT]"); |
| if (simpleText.length() > MAX) |
| simpleText = simpleText.substring(0, PREFIX) |
| + "..." + simpleText.substring(simpleText.length() - SUFFIX); |
| // Strip the outdir because that will typically not be the same |
| if (file.startsWith(outDir + "/")) |
| file = file.substring(outDir.length() + 1); |
| // The use of text.hashCode ensure that all of "text" is taken into account |
| tests.add(file + " " + positive + " " + text.hashCode() + " " + simpleText); |
| } |
| |
| void write() { |
| // sort the log entries because the subtests may not be executed in the same order |
| tests.sort((a, b) -> a.compareTo(b)); |
| try (BufferedWriter bw = new BufferedWriter(new FileWriter("tester.log"))) { |
| for (String t: tests) { |
| bw.write(t); |
| bw.newLine(); |
| } |
| } catch (IOException e) { |
| throw new Error("problem writing log: " + e); |
| } |
| } |
| } |
| |
| // Support classes for checkLinks |
| |
| /** |
| * A basic HTML parser. Override the protected methods as needed to get notified |
| * of significant items in any file that is read. |
| */ |
| static abstract class HtmlParser { |
| |
| protected final PrintStream out; |
| protected final Function<Path,String> fileReader; |
| |
| private Path file; |
| private StringReader in; |
| private int ch; |
| private int lineNumber; |
| private boolean inScript; |
| private boolean xml; |
| |
| HtmlParser(PrintStream out, Function<Path,String> fileReader) { |
| this.out = out; |
| this.fileReader = fileReader; |
| } |
| |
| /** |
| * Read a file. |
| * @param file the file to be read |
| * @throws IOException if an error occurs while reading the file |
| */ |
| void read(Path file) throws IOException { |
| try (StringReader r = new StringReader(fileReader.apply(file))) { |
| this.file = file; |
| this.in = r; |
| |
| startFile(file); |
| try { |
| lineNumber = 1; |
| xml = false; |
| nextChar(); |
| |
| while (ch != -1) { |
| switch (ch) { |
| |
| case '<': |
| html(); |
| break; |
| |
| default: |
| nextChar(); |
| } |
| } |
| } finally { |
| endFile(); |
| } |
| } catch (IOException e) { |
| error(file, lineNumber, e); |
| } catch (Throwable t) { |
| error(file, lineNumber, t); |
| t.printStackTrace(out); |
| } |
| } |
| |
| |
| int getLineNumber() { |
| return lineNumber; |
| } |
| |
| /** |
| * Called when a file has been opened, before parsing begins. |
| * This is always the first notification when reading a file. |
| * This implementation does nothing. |
| * |
| * @param file the file |
| */ |
| protected void startFile(Path file) { } |
| |
| /** |
| * Called when the parser has finished reading a file. |
| * This is always the last notification when reading a file, |
| * unless any errors occur while closing the file. |
| * This implementation does nothing. |
| */ |
| protected void endFile() { } |
| |
| /** |
| * Called when a doctype declaration is found, at the beginning of the file. |
| * This implementation does nothing. |
| * @param s the doctype declaration |
| */ |
| protected void docType(String s) { } |
| |
| /** |
| * Called when the opening tag of an HTML element is encountered. |
| * This implementation does nothing. |
| * @param name the name of the tag |
| * @param attrs the attribute |
| * @param selfClosing whether or not this is a self-closing tag |
| */ |
| protected void startElement(String name, Map<String,String> attrs, boolean selfClosing) { } |
| |
| /** |
| * Called when the closing tag of an HTML tag is encountered. |
| * This implementation does nothing. |
| * @param name the name of the tag |
| */ |
| protected void endElement(String name) { } |
| |
| /** |
| * Called when an error has been encountered. |
| * @param file the file being read |
| * @param lineNumber the line number of line containing the error |
| * @param message a description of the error |
| */ |
| protected void error(Path file, int lineNumber, String message) { |
| out.println(file + ":" + lineNumber + ": " + message); |
| } |
| |
| /** |
| * Called when an exception has been encountered. |
| * @param file the file being read |
| * @param lineNumber the line number of the line being read when the exception was found |
| * @param t the exception |
| */ |
| protected void error(Path file, int lineNumber, Throwable t) { |
| out.println(file + ":" + lineNumber + ": " + t); |
| } |
| |
| private void nextChar() throws IOException { |
| ch = in.read(); |
| if (ch == '\n') |
| lineNumber++; |
| } |
| |
| /** |
| * Read the start or end of an HTML tag, or an HTML comment |
| * {@literal <identifier attrs> } or {@literal </identifier> } |
| * @throws java.io.IOException if there is a problem reading the file |
| */ |
| private void html() throws IOException { |
| nextChar(); |
| if (isIdentifierStart((char) ch)) { |
| String name = readIdentifier().toLowerCase(Locale.US); |
| Map<String,String> attrs = htmlAttrs(); |
| if (attrs != null) { |
| boolean selfClosing = false; |
| if (ch == '/') { |
| nextChar(); |
| selfClosing = true; |
| } |
| if (ch == '>') { |
| nextChar(); |
| startElement(name, attrs, selfClosing); |
| if (name.equals("script")) { |
| inScript = true; |
| } |
| return; |
| } |
| } |
| } else if (ch == '/') { |
| nextChar(); |
| if (isIdentifierStart((char) ch)) { |
| String name = readIdentifier().toLowerCase(Locale.US); |
| skipWhitespace(); |
| if (ch == '>') { |
| nextChar(); |
| endElement(name); |
| if (name.equals("script")) { |
| inScript = false; |
| } |
| return; |
| } |
| } |
| } else if (ch == '!') { |
| nextChar(); |
| if (ch == '-') { |
| nextChar(); |
| if (ch == '-') { |
| nextChar(); |
| while (ch != -1) { |
| int dash = 0; |
| while (ch == '-') { |
| dash++; |
| nextChar(); |
| } |
| // Strictly speaking, a comment should not contain "--" |
| // so dash > 2 is an error, dash == 2 implies ch == '>' |
| // See http://www.w3.org/TR/html-markup/syntax.html#syntax-comments |
| // for more details. |
| if (dash >= 2 && ch == '>') { |
| nextChar(); |
| return; |
| } |
| |
| nextChar(); |
| } |
| } |
| } else if (ch == '[') { |
| nextChar(); |
| if (ch == 'C') { |
| nextChar(); |
| if (ch == 'D') { |
| nextChar(); |
| if (ch == 'A') { |
| nextChar(); |
| if (ch == 'T') { |
| nextChar(); |
| if (ch == 'A') { |
| nextChar(); |
| if (ch == '[') { |
| while (true) { |
| nextChar(); |
| if (ch == ']') { |
| nextChar(); |
| if (ch == ']') { |
| nextChar(); |
| if (ch == '>') { |
| nextChar(); |
| return; |
| } |
| } |
| } |
| } |
| |
| } |
| } |
| } |
| } |
| } |
| } |
| } else { |
| StringBuilder sb = new StringBuilder(); |
| while (ch != -1 && ch != '>') { |
| sb.append((char) ch); |
| nextChar(); |
| } |
| Pattern p = Pattern.compile("(?is)doctype\\s+html\\s?.*"); |
| String s = sb.toString(); |
| if (p.matcher(s).matches()) { |
| docType(s); |
| return; |
| } |
| } |
| } else if (ch == '?') { |
| nextChar(); |
| if (ch == 'x') { |
| nextChar(); |
| if (ch == 'm') { |
| nextChar(); |
| if (ch == 'l') { |
| Map<String,String> attrs = htmlAttrs(); |
| if (ch == '?') { |
| nextChar(); |
| if (ch == '>') { |
| nextChar(); |
| xml = true; |
| return; |
| } |
| } |
| } |
| } |
| |
| } |
| } |
| |
| if (!inScript) { |
| error(file, lineNumber, "bad html"); |
| } |
| } |
| |
| /** |
| * Read a series of HTML attributes, terminated by {@literal > }. |
| * Each attribute is of the form {@literal identifier[=value] }. |
| * "value" may be unquoted, single-quoted, or double-quoted. |
| */ |
| private Map<String,String> htmlAttrs() throws IOException { |
| Map<String, String> map = new LinkedHashMap<>(); |
| skipWhitespace(); |
| |
| loop: |
| while (isIdentifierStart((char) ch)) { |
| String name = readAttributeName().toLowerCase(Locale.US); |
| skipWhitespace(); |
| String value = null; |
| if (ch == '=') { |
| nextChar(); |
| skipWhitespace(); |
| if (ch == '\'' || ch == '"') { |
| char quote = (char) ch; |
| nextChar(); |
| StringBuilder sb = new StringBuilder(); |
| while (ch != -1 && ch != quote) { |
| sb.append((char) ch); |
| nextChar(); |
| } |
| value = sb.toString() // hack to replace common entities |
| .replace("<", "<") |
| .replace(">", ">") |
| .replace("&", "&"); |
| nextChar(); |
| } else { |
| StringBuilder sb = new StringBuilder(); |
| while (ch != -1 && !isUnquotedAttrValueTerminator((char) ch)) { |
| sb.append((char) ch); |
| nextChar(); |
| } |
| value = sb.toString(); |
| } |
| skipWhitespace(); |
| } |
| map.put(name, value); |
| } |
| |
| return map; |
| } |
| |
| private boolean isIdentifierStart(char ch) { |
| return Character.isUnicodeIdentifierStart(ch); |
| } |
| |
| private String readIdentifier() throws IOException { |
| StringBuilder sb = new StringBuilder(); |
| sb.append((char) ch); |
| nextChar(); |
| while (ch != -1 && Character.isUnicodeIdentifierPart(ch)) { |
| sb.append((char) ch); |
| nextChar(); |
| } |
| return sb.toString(); |
| } |
| |
| private String readAttributeName() throws IOException { |
| StringBuilder sb = new StringBuilder(); |
| sb.append((char) ch); |
| nextChar(); |
| while (ch != -1 && Character.isUnicodeIdentifierPart(ch) |
| || ch == '-' |
| || xml && ch == ':') { |
| sb.append((char) ch); |
| nextChar(); |
| } |
| return sb.toString(); |
| } |
| |
| private boolean isWhitespace(char ch) { |
| return Character.isWhitespace(ch); |
| } |
| |
| private void skipWhitespace() throws IOException { |
| while (isWhitespace((char) ch)) { |
| nextChar(); |
| } |
| } |
| |
| private boolean isUnquotedAttrValueTerminator(char ch) { |
| switch (ch) { |
| case '\f': case '\n': case '\r': case '\t': |
| case ' ': |
| case '"': case '\'': case '`': |
| case '=': case '<': case '>': |
| return true; |
| default: |
| return false; |
| } |
| } |
| } |
| |
| /** |
| * A class to check the links in a set of HTML files. |
| */ |
| static class LinkChecker extends HtmlParser { |
| private final Map<Path, IDTable> allFiles; |
| private final Map<URI, IDTable> allURIs; |
| |
| private int files; |
| private int links; |
| private int badSchemes; |
| private int duplicateIds; |
| private int missingIds; |
| |
| private Path currFile; |
| private IDTable currTable; |
| private boolean html5; |
| private boolean xml; |
| |
| private int errors; |
| |
| LinkChecker(PrintStream out, Function<Path,String> fileReader) { |
| super(out, fileReader); |
| allFiles = new HashMap<>(); |
| allURIs = new HashMap<>(); |
| } |
| |
| void checkDirectory(Path dir) throws IOException { |
| checkFiles(List.of(dir), false, Collections.emptySet()); |
| } |
| |
| void checkFiles(List<Path> files, boolean skipSubdirs, Set<Path> excludeFiles) throws IOException { |
| for (Path file : files) { |
| Files.walkFileTree(file, new SimpleFileVisitor<Path>() { |
| int depth = 0; |
| |
| @Override |
| public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { |
| if ((skipSubdirs && depth > 0) || excludeFiles.contains(dir)) { |
| return FileVisitResult.SKIP_SUBTREE; |
| } |
| depth++; |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult visitFile(Path p, BasicFileAttributes attrs) { |
| if (excludeFiles.contains(p)) { |
| return FileVisitResult.CONTINUE; |
| } |
| |
| if (Files.isRegularFile(p) && p.getFileName().toString().endsWith(".html")) { |
| checkFile(p); |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| |
| @Override |
| public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { |
| depth--; |
| return super.postVisitDirectory(dir, e); |
| } |
| }); |
| } |
| } |
| |
| void checkFile(Path file) { |
| try { |
| read(file); |
| } catch (IOException e) { |
| error(file, 0, e); |
| } |
| } |
| |
| int getErrorCount() { |
| return errors; |
| } |
| |
| public void report() { |
| List<Path> missingFiles = getMissingFiles(); |
| if (!missingFiles.isEmpty()) { |
| report("Missing files: (" + missingFiles.size() + ")"); |
| missingFiles.stream() |
| .sorted() |
| .forEach(this::reportMissingFile); |
| |
| } |
| |
| if (!allURIs.isEmpty()) { |
| report(false, "External URLs:"); |
| allURIs.keySet().stream() |
| .sorted(new URIComparator()) |
| .forEach(uri -> report(false, " %s", uri.toString())); |
| } |
| |
| int anchors = 0; |
| for (IDTable t : allFiles.values()) { |
| anchors += t.map.values().stream() |
| .filter(e -> !e.getReferences().isEmpty()) |
| .count(); |
| } |
| for (IDTable t : allURIs.values()) { |
| anchors += t.map.values().stream() |
| .filter(e -> !e.references.isEmpty()) |
| .count(); |
| } |
| |
| report(false, "Checked " + files + " files."); |
| report(false, "Found " + links + " references to " + anchors + " anchors " |
| + "in " + allFiles.size() + " files and " + allURIs.size() + " other URIs."); |
| report(!missingFiles.isEmpty(), "%6d missing files", missingFiles.size()); |
| report(duplicateIds > 0, "%6d duplicate ids", duplicateIds); |
| report(missingIds > 0, "%6d missing ids", missingIds); |
| |
| Map<String, Integer> schemeCounts = new TreeMap<>(); |
| Map<String, Integer> hostCounts = new TreeMap<>(new HostComparator()); |
| for (URI uri : allURIs.keySet()) { |
| String scheme = uri.getScheme(); |
| if (scheme != null) { |
| schemeCounts.put(scheme, schemeCounts.computeIfAbsent(scheme, s -> 0) + 1); |
| } |
| String host = uri.getHost(); |
| if (host != null) { |
| hostCounts.put(host, hostCounts.computeIfAbsent(host, h -> 0) + 1); |
| } |
| } |
| |
| if (schemeCounts.size() > 0) { |
| report(false, "Schemes"); |
| schemeCounts.forEach((s, n) -> report(!isSchemeOK(s), "%6d %s", n, s)); |
| } |
| |
| if (hostCounts.size() > 0) { |
| report(false, "Hosts"); |
| hostCounts.forEach((h, n) -> report(false, "%6d %s", n, h)); |
| } |
| } |
| |
| private void report(String message, Object... args) { |
| out.println(String.format(message, args)); |
| } |
| |
| private void report(boolean highlight, String message, Object... args) { |
| out.print(highlight ? "* " : " "); |
| out.println(String.format(message, args)); |
| } |
| |
| private void reportMissingFile(Path file) { |
| report("%s", relativePath(file)); |
| IDTable table = allFiles.get(file); |
| Set<Path> refs = new TreeSet<>(); |
| for (ID id : table.map.values()) { |
| if (id.references != null) { |
| for (Position p : id.references) { |
| refs.add(p.path); |
| } |
| } |
| } |
| int n = 0; |
| int MAX_REFS = 10; |
| for (Path ref : refs) { |
| report(" in " + relativePath(ref)); |
| if (++n == MAX_REFS) { |
| report(" ... and %d more", refs.size() - n); |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void startFile(Path path) { |
| currFile = path.toAbsolutePath().normalize(); |
| currTable = allFiles.computeIfAbsent(currFile, p -> new IDTable(p)); |
| html5 = false; |
| files++; |
| } |
| |
| @Override |
| public void endFile() { |
| currTable.check(); |
| } |
| |
| @Override |
| public void docType(String doctype) { |
| html5 = doctype.matches("(?i)<\\?doctype\\s+html>"); |
| } |
| |
| @Override @SuppressWarnings("fallthrough") |
| public void startElement(String name, Map<String, String> attrs, boolean selfClosing) { |
| int line = getLineNumber(); |
| switch (name) { |
| case "a": |
| String nameAttr = html5 ? null : attrs.get("name"); |
| if (nameAttr != null) { |
| foundAnchor(line, nameAttr); |
| } |
| // fallthrough |
| case "link": |
| String href = attrs.get("href"); |
| if (href != null) { |
| foundReference(line, href); |
| } |
| break; |
| } |
| |
| String idAttr = attrs.get("id"); |
| if (idAttr != null) { |
| foundAnchor(line, idAttr); |
| } |
| } |
| |
| @Override |
| public void endElement(String name) { } |
| |
| private void foundAnchor(int line, String name) { |
| currTable.addID(line, name); |
| } |
| |
| private void foundReference(int line, String ref) { |
| links++; |
| try { |
| URI uri = new URI(ref); |
| if (uri.isAbsolute()) { |
| foundReference(line, uri); |
| } else { |
| Path p; |
| String uriPath = uri.getPath(); |
| if (uriPath == null || uriPath.isEmpty()) { |
| p = currFile; |
| } else { |
| p = currFile.getParent().resolve(uriPath).normalize(); |
| } |
| foundReference(line, p, uri.getFragment()); |
| } |
| } catch (URISyntaxException e) { |
| error(currFile, line, "invalid URI: " + e); |
| } |
| } |
| |
| private void foundReference(int line, Path p, String fragment) { |
| IDTable t = allFiles.computeIfAbsent(p, key -> new IDTable(key)); |
| t.addReference(fragment, currFile, line); |
| } |
| |
| private void foundReference(int line, URI uri) { |
| if (!isSchemeOK(uri.getScheme())) { |
| error(currFile, line, "bad scheme in URI"); |
| badSchemes++; |
| } |
| |
| String fragment = uri.getFragment(); |
| try { |
| URI noFrag = new URI(uri.toString().replaceAll("#\\Q" + fragment + "\\E$", "")); |
| IDTable t = allURIs.computeIfAbsent(noFrag, key -> new IDTable(key.toString())); |
| t.addReference(fragment, currFile, line); |
| } catch (URISyntaxException e) { |
| throw new Error(e); |
| } |
| } |
| |
| private boolean isSchemeOK(String uriScheme) { |
| if (uriScheme == null) { |
| return true; |
| } |
| |
| switch (uriScheme) { |
| case "file": |
| case "ftp": |
| case "http": |
| case "https": |
| case "javascript": |
| case "mailto": |
| return true; |
| |
| default: |
| return false; |
| } |
| } |
| |
| private List<Path> getMissingFiles() { |
| return allFiles.entrySet().stream() |
| .filter(e -> !Files.exists(e.getKey())) |
| .map(e -> e.getKey()) |
| .collect(Collectors.toList()); |
| } |
| |
| @Override |
| protected void error(Path file, int lineNumber, String message) { |
| super.error(relativePath(file), lineNumber, message); |
| errors++; |
| } |
| |
| @Override |
| protected void error(Path file, int lineNumber, Throwable t) { |
| super.error(relativePath(file), lineNumber, t); |
| errors++; |
| } |
| |
| private Path relativePath(Path path) { |
| return path.startsWith(currDir) ? currDir.relativize(path) : path; |
| } |
| |
| /** |
| * A position in a file, as identified by a file name and line number. |
| */ |
| static class Position implements Comparable<Position> { |
| Path path; |
| int line; |
| |
| Position(Path path, int line) { |
| this.path = path; |
| this.line = line; |
| } |
| |
| @Override |
| public int compareTo(Position o) { |
| int v = path.compareTo(o.path); |
| return v != 0 ? v : Integer.compare(line, o.line); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } else if (obj == null || getClass() != obj.getClass()) { |
| return false; |
| } else { |
| final Position other = (Position) obj; |
| return Objects.equals(this.path, other.path) |
| && this.line == other.line; |
| } |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(path) * 37 + line; |
| } |
| } |
| |
| /** |
| * Infor for an ID within an HTML file, and a set of positions that reference it. |
| */ |
| static class ID { |
| boolean declared; |
| Set<Position> references; |
| |
| Set<Position> getReferences() { |
| return (references) == null ? Collections.emptySet() : references; |
| } |
| } |
| |
| /** |
| * A table for the set of IDs in an HTML file. |
| */ |
| class IDTable { |
| private String name; |
| private boolean checked; |
| private final Map<String, ID> map = new HashMap<>(); |
| |
| IDTable(Path p) { |
| this(relativePath(p).toString()); |
| } |
| |
| IDTable(String name) { |
| this.name = name; |
| } |
| |
| void addID(int line, String name) { |
| if (checked) { |
| throw new IllegalStateException("Adding ID after file has been read"); |
| } |
| Objects.requireNonNull(name); |
| ID id = map.computeIfAbsent(name, x -> new ID()); |
| if (id.declared) { |
| error(currFile, line, "name already declared: " + name); |
| duplicateIds++; |
| } else { |
| id.declared = true; |
| } |
| } |
| |
| void addReference(String name, Path from, int line) { |
| if (checked) { |
| if (name != null) { |
| ID id = map.get(name); |
| if (id == null || !id.declared) { |
| error(from, line, "id not found: " + this.name + "#" + name); |
| } |
| } |
| } else { |
| ID id = map.computeIfAbsent(name, x -> new ID()); |
| if (id.references == null) { |
| id.references = new TreeSet<>(); |
| } |
| id.references.add(new Position(from, line)); |
| } |
| } |
| |
| void check() { |
| map.forEach((name, id) -> { |
| if (name != null && !id.declared) { |
| //log.error(currFile, 0, "id not declared: " + name); |
| for (Position ref : id.references) { |
| error(ref.path, ref.line, "id not found: " + this.name + "#" + name); |
| } |
| missingIds++; |
| } |
| }); |
| checked = true; |
| } |
| } |
| |
| static class URIComparator implements Comparator<URI> { |
| final HostComparator hostComparator = new HostComparator(); |
| |
| @Override |
| public int compare(URI o1, URI o2) { |
| if (o1.isOpaque() || o2.isOpaque()) { |
| return o1.compareTo(o2); |
| } |
| String h1 = o1.getHost(); |
| String h2 = o2.getHost(); |
| String s1 = o1.getScheme(); |
| String s2 = o2.getScheme(); |
| if (h1 == null || h1.isEmpty() || s1 == null || s1.isEmpty() |
| || h2 == null || h2.isEmpty() || s2 == null || s2.isEmpty()) { |
| return o1.compareTo(o2); |
| } |
| int v = hostComparator.compare(h1, h2); |
| if (v != 0) { |
| return v; |
| } |
| v = s1.compareTo(s2); |
| if (v != 0) { |
| return v; |
| } |
| return o1.compareTo(o2); |
| } |
| } |
| |
| static class HostComparator implements Comparator<String> { |
| @Override |
| public int compare(String h1, String h2) { |
| List<String> l1 = new ArrayList<>(Arrays.asList(h1.split("\\."))); |
| Collections.reverse(l1); |
| String r1 = String.join(".", l1); |
| List<String> l2 = new ArrayList<>(Arrays.asList(h2.split("\\."))); |
| Collections.reverse(l2); |
| String r2 = String.join(".", l2); |
| return r1.compareTo(r2); |
| } |
| } |
| |
| } |
| } |