8190312: javadoc -link doesn't work with http: -> https: URL redirects
authorjjg
Mon, 26 Nov 2018 11:17:13 -0800
changeset 52687 526f5cf13972
parent 52686 00c47178ea6c
child 52688 3db8758f0f79
8190312: javadoc -link doesn't work with http: -> https: URL redirects Reviewed-by: hannesw
src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/resources/doclets.properties
src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/Extern.java
test/langtools/jdk/javadoc/doclet/testLinkOption/TestRedirectLinks.java
--- 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;
+        }
+    }
+}