8190312: javadoc -link doesn't work with http: -> https: URL redirects
Reviewed-by: hannesw
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties Mon Nov 26 14:13:22 2018 -0500
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties Mon Nov 26 11:17:13 2018 -0800
@@ -230,6 +230,7 @@
but the packages defined in {0} are in named modules.
doclet.linkMismatch_ModuleLinkedtoPackage=The code being documented uses modules but the packages defined \
in {0} are in the unnamed module.
+doclet.urlRedirected=URL {0} was redirected to {1} -- Update the command-line options to suppress this warning.
#Documentation for Enums
doclet.enum_values_doc.fullbody=\
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Extern.java Mon Nov 26 14:13:22 2018 -0500
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Extern.java Mon Nov 26 11:17:13 2018 -0800
@@ -25,8 +25,15 @@
package jdk.javadoc.internal.doclets.toolkit.util;
-import java.io.*;
-import java.net.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
@@ -35,6 +42,7 @@
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.PackageElement;
import javax.tools.Diagnostic;
+import javax.tools.Diagnostic.Kind;
import javax.tools.DocumentationTool;
import jdk.javadoc.doclet.Reporter;
@@ -85,7 +93,7 @@
private class Item {
/**
- * Element name, found in the "element-list" file in the {@link path}.
+ * Element name, found in the "element-list" file in the {@link #path}.
*/
final String elementName;
@@ -157,7 +165,7 @@
*/
public boolean isModule(String elementName) {
Item elem = moduleItems.get(elementName);
- return (elem == null) ? false : true;
+ return elem != null;
}
/**
@@ -245,14 +253,6 @@
}
}
- private URL toURL(String url) throws Fault {
- try {
- return new URL(url);
- } catch (MalformedURLException e) {
- throw new Fault(resources.getText("doclet.MalformedURL", url), e);
- }
- }
-
private class Fault extends Exception {
private static final long serialVersionUID = 0;
@@ -296,7 +296,9 @@
private void readElementListFromURL(String urlpath, URL elemlisturlpath) throws Fault {
try {
URL link = elemlisturlpath.toURI().resolve(DocPaths.ELEMENT_LIST.getPath()).toURL();
- readElementList(link.openStream(), urlpath, false);
+ try (InputStream in = open(link)) {
+ readElementList(in, urlpath, false);
+ }
} catch (URISyntaxException | MalformedURLException exc) {
throw new Fault(resources.getText("doclet.MalformedURL", elemlisturlpath.toString()), exc);
} catch (IOException exc) {
@@ -313,7 +315,9 @@
private void readAlternateURL(String urlpath, URL elemlisturlpath) throws Fault {
try {
URL link = elemlisturlpath.toURI().resolve(DocPaths.PACKAGE_LIST.getPath()).toURL();
- readElementList(link.openStream(), urlpath, false);
+ try (InputStream in = open(link)) {
+ readElementList(in, urlpath, false);
+ }
} catch (URISyntaxException | MalformedURLException exc) {
throw new Fault(resources.getText("doclet.MalformedURL", elemlisturlpath.toString()), exc);
} catch (IOException exc) {
@@ -377,9 +381,9 @@
private void readElementList(InputStream input, String path, boolean relative)
throws Fault, IOException {
try (BufferedReader in = new BufferedReader(new InputStreamReader(input))) {
- String elemname = null;
+ String elemname;
+ DocPath elempath;
String moduleName = null;
- DocPath elempath = null;
DocPath basePath = DocPath.create(path);
while ((elemname = in.readLine()) != null) {
if (elemname.length() > 0) {
@@ -406,9 +410,25 @@
}
}
+ private void checkLinkCompatibility(String packageName, String moduleName, String path) throws Fault {
+ PackageElement pe = utils.elementUtils.getPackageElement(packageName);
+ if (pe != null) {
+ ModuleElement me = (ModuleElement)pe.getEnclosingElement();
+ if (me == null || me.isUnnamed()) {
+ if (moduleName != null) {
+ throw new Fault(resources.getText("doclet.linkMismatch_PackagedLinkedtoModule",
+ path), null);
+ }
+ } else if (moduleName == null) {
+ throw new Fault(resources.getText("doclet.linkMismatch_ModuleLinkedtoPackage",
+ path), null);
+ }
+ }
+ }
+
public boolean isUrl (String urlCandidate) {
try {
- URL ignore = new URL(urlCandidate);
+ new URL(urlCandidate);
//No exception was thrown, so this must really be a URL.
return true;
} catch (MalformedURLException e) {
@@ -417,17 +437,70 @@
}
}
- private void checkLinkCompatibility(String packageName, String moduleName, String path) throws Fault {
- PackageElement pe = configuration.utils.elementUtils.getPackageElement(packageName);
- if (pe != null) {
- ModuleElement me = (ModuleElement)pe.getEnclosingElement();
- if (me == null || me.isUnnamed()) {
- if (moduleName != null)
- throw new Fault(resources.getText("doclet.linkMismatch_PackagedLinkedtoModule",
- path), null);
- } else if (moduleName == null)
- throw new Fault(resources.getText("doclet.linkMismatch_ModuleLinkedtoPackage",
- path), null);
+ private URL toURL(String url) throws Fault {
+ try {
+ return new URL(url);
+ } catch (MalformedURLException e) {
+ throw new Fault(resources.getText("doclet.MalformedURL", url), e);
}
}
+
+ /**
+ * Open a stream to a URL, following a limited number of redirects
+ * if necessary.
+ *
+ * @param url the URL
+ * @return the stream
+ * @throws IOException if an error occurred accessing the URL
+ */
+ private InputStream open(URL url) throws IOException {
+ URLConnection conn = url.openConnection();
+
+ boolean redir;
+ int redirects = 0;
+ InputStream in;
+
+ do {
+ // Open the input stream before getting headers,
+ // because getHeaderField() et al swallow IOExceptions.
+ in = conn.getInputStream();
+ redir = false;
+
+ if (conn instanceof HttpURLConnection) {
+ HttpURLConnection http = (HttpURLConnection)conn;
+ int stat = http.getResponseCode();
+ // See:
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
+ // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection
+ switch (stat) {
+ case 300: // Multiple Choices
+ case 301: // Moved Permanently
+ case 302: // Found (previously Moved Temporarily)
+ case 303: // See Other
+ case 307: // Temporary Redirect
+ case 308: // Permanent Redirect
+ URL base = http.getURL();
+ String loc = http.getHeaderField("Location");
+ URL target = null;
+ if (loc != null) {
+ target = new URL(base, loc);
+ }
+ http.disconnect();
+ if (target == null || redirects >= 5) {
+ throw new IOException("illegal URL redirect");
+ }
+ redir = true;
+ conn = target.openConnection();
+ redirects++;
+ }
+ }
+ } while (redir);
+
+ if (!url.equals(conn.getURL())) {
+ configuration.getReporter().print(Kind.WARNING,
+ resources.getText("doclet.urlRedirected", url, conn.getURL()));
+ }
+
+ return in;
+ }
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/jdk/javadoc/doclet/testLinkOption/TestRedirectLinks.java Mon Nov 26 11:17:13 2018 -0800
@@ -0,0 +1,326 @@
+/*
+ * 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.
+ */
+
+/*
+ * @test
+ * @bug 8190312
+ * @summary test redirected URLs for -link
+ * @library /tools/lib ../lib
+ * @modules jdk.compiler/com.sun.tools.javac.api
+ * jdk.compiler/com.sun.tools.javac.main
+ * jdk.javadoc/jdk.javadoc.internal.api
+ * jdk.javadoc/jdk.javadoc.internal.tool
+ * @build toolbox.ToolBox toolbox.JavacTask JavadocTester
+ * @run main TestRedirectLinks
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
+
+import toolbox.JavacTask;
+import toolbox.ToolBox;
+
+
+public class TestRedirectLinks extends JavadocTester {
+ /**
+ * The entry point of the test.
+ * @param args the array of command line arguments.
+ */
+ public static void main(String... args) throws Exception {
+ TestRedirectLinks tester = new TestRedirectLinks();
+ tester.runTests();
+ }
+
+ private ToolBox tb = new ToolBox();
+
+ /*
+ * This test requires access to a URL that is redirected
+ * from http: to https:.
+ * For now, we use the main JDK API on docs.oracle.com.
+ * The test is skipped if access to the server is not available.
+ * (A better solution is to use a local testing web server.)
+ */
+ @Test
+ public void testRedirects() throws Exception {
+ // first, test to see if access to external URLs is available
+ URL testURL = new URL("http://docs.oracle.com/en/java/javase/11/docs/api/element-list");
+ boolean haveRedirectURL = false;
+ try {
+ URLConnection conn = testURL.openConnection();
+ conn.connect();
+ out.println("Opened connection to " + testURL);
+ if (conn instanceof HttpURLConnection) {
+ HttpURLConnection httpConn = (HttpURLConnection) conn;
+ int status = httpConn.getResponseCode();
+ if (status / 100 == 3) {
+ haveRedirectURL = true;
+ }
+ out.println("Status: " + status);
+ int n = 0;
+ while (httpConn.getHeaderField(n) != null) {
+ out.println("Header: " + httpConn.getHeaderFieldKey(n) + ": " + httpConn.getHeaderField(n));
+ n++;
+ }
+ }
+ } catch (Exception e) {
+ out.println("Exception occurred: " + e);
+ }
+
+ if (!haveRedirectURL) {
+ out.println("Setup failed; this test skipped");
+ return;
+ }
+
+ String apiURL = "http://docs.oracle.com/en/java/javase/11/docs/api";
+ String outRedirect = "outRedirect";
+ javadoc("-d", outRedirect,
+ "-html4",
+ "-sourcepath", testSrc,
+ "-link", apiURL,
+ "pkg");
+ checkExit(Exit.OK);
+ checkOutput("pkg/B.html", true,
+ "<a href=\"" + apiURL + "/java.base/java/lang/String.html?is-external=true\" "
+ + "title=\"class or interface in java.lang\" class=\"externalLink\">Link-Plain to String Class</a>");
+ checkOutput("pkg/C.html", true,
+ "<a href=\"" + apiURL + "/java.base/java/lang/Object.html?is-external=true\" "
+ + "title=\"class or interface in java.lang\" class=\"externalLink\">Object</a>");
+ }
+
+ private Path libApi = Path.of("libApi");
+ private HttpServer oldServer = null;
+ private HttpsServer newServer = null;
+
+ /**
+ * This test verifies redirection using temporary localhost web servers,
+ * such that one server redirects to the other.
+ */
+ @Test
+ public void testWithServers() throws Exception {
+ // Set up a simple library
+ Path libSrc = Path.of("libSrc");
+ tb.writeJavaFiles(libSrc.resolve("mA"),
+ "module mA { exports p1; exports p2; }",
+ "package p1; public class C1 { }",
+ "package p2; public class C2 { }");
+ tb.writeJavaFiles(libSrc.resolve("mB"),
+ "module mB { exports p3; exports p4; }",
+ "package p3; public class C3 { }",
+ "package p4; public class C4 { }");
+
+ Path libModules = Path.of("libModules");
+ Files.createDirectories(libModules);
+
+ new JavacTask(tb)
+ .outdir(libModules)
+ .options("--module-source-path", libSrc.toString(),
+ "--module", "mA,mB")
+ .run()
+ .writeAll();
+
+ javadoc("-d", libApi.toString(),
+ "--module-source-path", libSrc.toString(),
+ "--module", "mA,mB" );
+
+ // start web servers
+ InetAddress localHost = InetAddress.getLocalHost();
+ try {
+ oldServer = HttpServer.create(new InetSocketAddress(localHost, 0), 0);
+ String oldURL = "http:/" + oldServer.getAddress();
+ oldServer.createContext("/", this::handleOldRequest);
+ out.println("Starting old server (" + oldServer.getClass().getSimpleName() + ") on " + oldURL);
+ oldServer.start();
+
+ SSLContext sslContext = new SimpleSSLContext().get();
+ newServer = HttpsServer.create(new InetSocketAddress(localHost, 0), 0);
+ String newURL = "https:/" + newServer.getAddress();
+ newServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ newServer.createContext("/", this::handleNewRequest);
+ out.println("Starting new server (" + newServer.getClass().getSimpleName() + ") on " + newURL);
+ newServer.start();
+
+ // Set up API to use that library
+ Path src = Path.of("src");
+ tb.writeJavaFiles(src.resolve("mC"),
+ "module mC { requires mA; requires mB; exports p5; exports p6; }",
+ "package p5; public class C5 extends p1.C1 { }",
+ "package p6; public class C6 { public p4.C4 c4; }");
+
+ // Set defaults for HttpsURLConfiguration for the duration of this
+ // invocation of javadoc to use our testing sslContext
+ HostnameVerifier prevHostNameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+ SSLSocketFactory prevSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+ try {
+ HttpsURLConnection.setDefaultHostnameVerifier((hostName, session) -> true);
+ HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
+
+ javadoc("-d", "api",
+ "--module-source-path", src.toString(),
+ "--module-path", libModules.toString(),
+ "-link", "http:/" + oldServer.getAddress(),
+ "--module", "mC" );
+
+ } finally {
+ HttpsURLConnection.setDefaultHostnameVerifier(prevHostNameVerifier);
+ HttpsURLConnection.setDefaultSSLSocketFactory(prevSSLSocketFactory);
+ }
+
+ // Verify the following:
+ // 1: A warning about the redirection is generated.
+ // 2: The contents of the redirected link were read successfully,
+ // identifying the remote API
+ // 3: The original URL is still used in the generated docs, to avoid assuming
+ // that all the other files at that link have been redirected as well.
+ checkOutput(Output.OUT, true,
+ "javadoc: warning - URL " + oldURL + "/element-list was redirected to " + newURL + "/element-list");
+ checkOutput("mC/p5/C5.html", true,
+ "extends <a href=\"" + oldURL + "/mA/p1/C1.html?is-external=true\" " +
+ "title=\"class or interface in p1\" class=\"externalLink\">C1</a>");
+ checkOutput("mC/p6/C6.html", true,
+ "<a href=\"" + oldURL + "/mB/p4/C4.html?is-external=true\" " +
+ "title=\"class or interface in p4\" class=\"externalLink\">C4</a>");
+ } finally {
+ if (oldServer != null) {
+ out.println("Stopping old server on " + oldServer.getAddress());
+ oldServer.stop(0);
+ }
+ if (newServer != null) {
+ out.println("Stopping new server on " + newServer.getAddress());
+ newServer.stop(0);
+ }
+ }
+ }
+
+ private void handleOldRequest(HttpExchange x) throws IOException {
+ out.println("old request: "
+ + x.getProtocol() + " "
+ + x.getRequestMethod() + " "
+ + x.getRequestURI());
+ String newProtocol = (newServer instanceof HttpsServer) ? "https" : "http";
+ String redirectTo = newProtocol + ":/" + newServer.getAddress() + x.getRequestURI();
+ out.println(" redirect to: " + redirectTo);
+ x.getResponseHeaders().add("Location", redirectTo);
+ x.sendResponseHeaders(HttpURLConnection.HTTP_MOVED_PERM, 0);
+ x.getResponseBody().close();
+ }
+
+ private void handleNewRequest(HttpExchange x) throws IOException {
+ out.println("new request: "
+ + x.getProtocol() + " "
+ + x.getRequestMethod() + " "
+ + x.getRequestURI());
+ Path file = libApi.resolve(x.getRequestURI().getPath().substring(1).replace('/', File.separatorChar));
+ System.err.println(file);
+ if (Files.exists(file)) {
+ byte[] bytes = Files.readAllBytes(file);
+ // in the context of this test, the only request should be element-list,
+ // which we can say is text/plain.
+ x.getResponseHeaders().add("Content-type", "text/plain");
+ x.sendResponseHeaders(HttpURLConnection.HTTP_OK, bytes.length);
+ try (OutputStream responseStream = x.getResponseBody()) {
+ responseStream.write(bytes);
+ }
+ } else {
+ x.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0);
+ x.getResponseBody().close();
+ }
+ }
+
+ /**
+ * Creates a simple usable SSLContext for an HttpsServer using
+ * a default keystore in the test tree.
+ * <p>
+ * This class is based on
+ * test/jdk/java/net/httpclient/whitebox/java.net.http/jdk/internal/net/http/SimpleSSLContext.java
+ */
+ static class SimpleSSLContext {
+
+ private final SSLContext ssl;
+
+ /**
+ * Loads default keystore.
+ */
+ SimpleSSLContext() throws Exception {
+ Path p = Path.of(System.getProperty("test.src", ".")).toAbsolutePath();
+ while (!Files.exists(p.resolve("TEST.ROOT"))) {
+ p = p.getParent();
+ if (p == null) {
+ throw new IOException("can't find TEST.ROOT");
+ }
+ }
+
+ System.err.println("Test suite root: " + p);
+ Path testKeys = p.resolve("../lib/jdk/test/lib/net/testkeys").normalize();
+ if (!Files.exists(testKeys)) {
+ throw new IOException("can't find testkeys");
+ }
+ System.err.println("Test keys: " + testKeys);
+
+ try (InputStream fis = Files.newInputStream(testKeys)) {
+ ssl = init(fis);
+ }
+ }
+
+ private SSLContext init(InputStream i) throws Exception {
+ char[] passphrase = "passphrase".toCharArray();
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(i, passphrase);
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
+ kmf.init(ks, passphrase);
+
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
+ tmf.init(ks);
+
+ SSLContext ssl = SSLContext.getInstance("TLS");
+ ssl.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+ return ssl;
+ }
+
+ SSLContext get() {
+ return ssl;
+ }
+ }
+}