2612bef51e
Reviewed-by: ksrini
1797 lines
64 KiB
Java
1797 lines
64 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|