8190312: javadoc -link doesn't work with http: -> https: URL redirects

Reviewed-by: hannesw
This commit is contained in:
Jonathan Gibbons 2018-11-26 11:17:13 -08:00
parent e0d9ae7699
commit 1d01b4d22f
3 changed files with 428 additions and 28 deletions
src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit
test/langtools/jdk/javadoc/doclet/testLinkOption

@ -230,6 +230,7 @@ doclet.linkMismatch_PackagedLinkedtoModule=The code being documented uses packag
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=\

@ -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.Element;
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 @@ public class Extern {
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 class Extern {
*/
public boolean isModule(String elementName) {
Item elem = moduleItems.get(elementName);
return (elem == null) ? false : true;
return elem != null;
}
/**
@ -245,14 +253,6 @@ public class Extern {
}
}
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 @@ public class Extern {
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 @@ public class Extern {
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 @@ public class Extern {
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 @@ public class Extern {
}
}
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 @@ public class Extern {
}
}
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;
}
}

@ -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;
}
}
}