[MJAVADOC-427] "Error fetching URL" for valid non-Java API links

javadoc tool currently doesn't follow redirects for -link URLs, so we
follow them ourselves and pass the last redirect location to javadoc.
diff --git a/src/it/projects/MJAVADOC-325/verify.bsh b/src/it/projects/MJAVADOC-325/verify.bsh
index 7c4ce44..3c60e6c 100644
--- a/src/it/projects/MJAVADOC-325/verify.bsh
+++ b/src/it/projects/MJAVADOC-325/verify.bsh
@@ -30,7 +30,7 @@
 }

 

 String optionsContent = FileUtils.fileRead( optionsFile );

-String javaApiLink = "'http://docs.oracle.com/javase/1,5,0/docs/api'";

+String javaApiLink = "'https://docs.oracle.com/javase/1.5.0/docs/api'";

 

 if ( !optionsContent.contains( javaApiLink ) )

 {

diff --git a/src/it/projects/MJAVADOC-427/invoker.properties b/src/it/projects/MJAVADOC-427/invoker.properties
new file mode 100644
index 0000000..aee4e3b
--- /dev/null
+++ b/src/it/projects/MJAVADOC-427/invoker.properties
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+invoker.goals=clean javadoc:javadoc
+
+# slf4j javadoc is hosted on https site with a "let's encrypt" certificate, signed by IdenTrust only trusted since 8u101 (JDK-8154757)
+invoker.java.version = 1.8.0.101+
diff --git a/src/it/projects/MJAVADOC-427/pom.xml b/src/it/projects/MJAVADOC-427/pom.xml
new file mode 100644
index 0000000..bf7a406
--- /dev/null
+++ b/src/it/projects/MJAVADOC-427/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing,
+  software distributed under the License is distributed on an
+  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  KIND, either express or implied.  See the License for the
+  specific language governing permissions and limitations
+  under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.plugins.maven-javadoc-plugin.it</groupId>
+  <artifactId>mjavadoc-427</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>jar</packaging>
+  <url>https://issues.apache.org/jira/browse/MJAVADOC-427</url>
+  <description>Tests that the plugin follows redirects</description>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <!-- url of slf4j api is http, and javadoc is redirected to https -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <version>1.7.12</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>@pom.version@</version>
+          <configuration>
+            <detectLinks>true</detectLinks>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+</project>
diff --git a/src/it/projects/MJAVADOC-427/src/main/java/mjavadoc427/App.java b/src/it/projects/MJAVADOC-427/src/main/java/mjavadoc427/App.java
new file mode 100644
index 0000000..75194f3
--- /dev/null
+++ b/src/it/projects/MJAVADOC-427/src/main/java/mjavadoc427/App.java
@@ -0,0 +1,35 @@
+package mjavadoc427;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.slf4j.LoggerFactory;
+
+/**
+ * Link to slf4j {@link LoggerFactory}.
+ */
+public class App
+{
+
+    public LoggerFactory getLoggerFactory()
+    {
+        return null;
+    }
+
+}
diff --git a/src/it/projects/MJAVADOC-427/verify.groovy b/src/it/projects/MJAVADOC-427/verify.groovy
new file mode 100644
index 0000000..8e3c9ab
--- /dev/null
+++ b/src/it/projects/MJAVADOC-427/verify.groovy
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+def file = new File( basedir, 'target/site/apidocs/mjavadoc427/App.html' );
+
+assert file.exists()
+
+// assert that javadoc of class correctly contains link, just like method details
+assert file.text =~ /Link to slf4j <a href=".*?".*?><code>LoggerFactory<\/code><\/a>/
+assert file.text =~ /<pre>public.*?<a href=".*?".*?>LoggerFactory<\/a>.*?getLoggerFactory.*?\(\)<\/pre>/
diff --git a/src/it/projects/detectLinks/verify.bsh b/src/it/projects/detectLinks/verify.bsh
index 4e68ec2..3669ab0 100644
--- a/src/it/projects/detectLinks/verify.bsh
+++ b/src/it/projects/detectLinks/verify.bsh
@@ -69,7 +69,7 @@
         System.err.println( "-link not added: " + options1 );

         return false;

     }

-    if ( !contentOptions1.substring( link1 ).contains( "http://commons.apache.org/lang/apidocs" ) )

+    if ( !contentOptions1.substring( link1 ).contains( "commons.apache.org" ) )

     {

         System.err.println( "link for commons-lang not added: " + options1 );

         if ( !log.contains( "Error fetching link: http://commons.apache.org/lang/apidocs" ) )

@@ -77,7 +77,7 @@
             return false;

         }

     }

-    if ( !contentOptions1.substring( link1 ).contains( "http://junit.org/apidocs" ) )

+    if ( !contentOptions1.substring( link1 ).contains( "junit.org" ) )

     {

         System.err.println( "link for junit not added: " + options1 );

         if ( !log.contains( "Error fetching link: http://junit.org/apidocs" ) )

@@ -108,7 +108,7 @@
         System.err.println( "-link not added: " + options2 );

         return false;

     }

-    if ( !contentOptions2.substring( link2 ).contains( "http://commons.apache.org/lang/apidocs" ) )

+    if ( !contentOptions2.substring( link2 ).contains( "commons.apache.org" ) )

     {

         System.err.println( "link for commons-lang not added: " + options2 );

         if ( !log.contains( "Error fetching link: http://commons.apache.org/lang/apidocs" ) )

@@ -116,7 +116,7 @@
             return false;

         }

     }

-    if ( !contentOptions2.substring( link2 ).contains( "http://junit.org/apidocs" ) )

+    if ( !contentOptions2.substring( link2 ).contains( "junit.org" ) )

     {

         System.err.println( "link for junit not added: " + options2 );

         if ( !log.contains( "Error fetching link: http://junit.org/apidocs" ) )

diff --git a/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java b/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java
index afe3654..b403c00 100644
--- a/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java
+++ b/src/main/java/org/apache/maven/plugins/javadoc/AbstractJavadocMojo.java
@@ -3163,7 +3163,7 @@
 

         links.addAll( getDependenciesLinks() );

 

-        return links;

+        return followLinks( links );

     }

 

     private Set<Group> collectGroups()

@@ -5839,6 +5839,31 @@
     }

 

     /**

+     * Follows all of the given links, and returns their last redirect locations. Ordering is kept.

+     * This is necessary because javadoc tool doesn't follow links, see JDK-8190312 (MJAVADOC-427, MJAVADOC-487)

+     *

+     * @param links Links to follow.

+     * @return Last redirect location of all the links.

+     */

+    private Set<String> followLinks( Set<String> links )

+    {

+        Set<String> redirectLinks = new LinkedHashSet<>( links.size() );

+        for ( String link : links )

+        {

+            try

+            {

+                redirectLinks.add( JavadocUtil.getRedirectUrl( new URI( link ).toURL(), settings ).toString() );

+            }

+            catch ( Exception e )

+            {

+                // only print in debug, it should have been logged already in warn/error because link isn't valid

+                getLog().debug( "Could not follow " + link + ". Reason: " + e.getMessage() );

+            }

+        }

+        return redirectLinks;

+    }

+

+    /**

      * @param link not null

      * @param detecting <code>true</code> if the link is generated by

      * <code>detectLinks</code>, or <code>false</code> otherwise

diff --git a/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java b/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
index 73ae0f2..e60699f 100644
--- a/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
+++ b/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
@@ -29,6 +29,7 @@
 import org.apache.http.client.HttpClient;

 import org.apache.http.client.methods.HttpGet;

 import org.apache.http.client.params.ClientPNames;

+import org.apache.http.client.protocol.HttpClientContext;

 import org.apache.http.conn.params.ConnRoutePNames;

 import org.apache.http.impl.client.DefaultHttpClient;

 import org.apache.http.impl.conn.PoolingClientConnectionManager;

@@ -72,6 +73,7 @@
 import java.io.UnsupportedEncodingException;

 import java.lang.reflect.Modifier;

 import java.net.SocketTimeoutException;

+import java.net.URI;

 import java.net.URL;

 import java.net.URLClassLoader;

 import java.util.ArrayList;

@@ -1639,6 +1641,49 @@
     }

 

     /**

+     * Execute an Http request at the given URL, follows redirects, and returns the last redirect locations. For URLs

+     * that aren't http/https, this does nothing and simply returns the given URL unchanged.

+     *

+     * @param url URL.

+     * @param settings Maven settings.

+     * @return Last redirect location.

+     * @throws IOException if there was an error during the Http request.

+     */

+    protected static URL getRedirectUrl( URL url, Settings settings )

+        throws IOException

+    {

+        String protocol = url.getProtocol();

+        if ( !"http".equals( protocol ) && !"https".equals( protocol ) )

+        {

+            return url;

+        }

+        HttpClient httpClient = null;

+        try

+        {

+            httpClient = createHttpClient( settings, url );

+            HttpClientContext httpContext = HttpClientContext.create();

+            HttpGet httpMethod = new HttpGet( url.toString() );

+            HttpResponse response = httpClient.execute( httpMethod, httpContext );

+            int status = response.getStatusLine().getStatusCode();

+            if ( status != HttpStatus.SC_OK )

+            {

+                throw new FileNotFoundException( "Unexpected HTTP status code " + status + " getting resource "

+                    + url.toExternalForm() + "." );

+            }

+

+            List<URI> redirects = httpContext.getRedirectLocations();

+            return redirects.isEmpty() ? url : redirects.get( redirects.size() - 1 ).toURL();

+        }

+        finally

+        {

+            if ( httpClient != null )

+            {

+                httpClient.getConnectionManager().shutdown();

+            }

+        }

+    }

+

+    /**

      * Validates an <code>URL</code> to point to a valid <code>package-list</code> resource.

      *

      * @param url The URL to validate.

diff --git a/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java b/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java
index e13f176..ed6ee00 100644
--- a/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java
+++ b/src/test/java/org/apache/maven/plugins/javadoc/JavadocUtilTest.java
@@ -22,7 +22,9 @@
 import java.io.File;

 import java.io.FileNotFoundException;

 import java.io.IOException;

+import java.io.OutputStream;

 import java.net.SocketTimeoutException;

+import java.net.URI;

 import java.net.URL;

 import java.util.ArrayList;

 import java.util.Collections;

@@ -32,6 +34,10 @@
 import java.util.Set;

 import java.util.regex.PatternSyntaxException;

 

+import javax.servlet.ServletException;

+import javax.servlet.http.HttpServletRequest;

+import javax.servlet.http.HttpServletResponse;

+

 import org.apache.commons.lang3.builder.EqualsBuilder;

 import org.apache.maven.plugins.javadoc.JavadocUtil;

 import org.apache.maven.plugins.javadoc.ProxyServer.AuthAsyncProxyServlet;

@@ -39,6 +45,10 @@
 import org.apache.maven.settings.Settings;

 import org.codehaus.plexus.PlexusTestCase;

 import org.codehaus.plexus.util.FileUtils;

+import org.mortbay.jetty.Server;

+import org.mortbay.jetty.handler.AbstractHandler;

+import org.mortbay.jetty.handler.MovedContextHandler;

+import org.mortbay.util.ByteArrayISO8859Writer;

 

 /**

  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>

@@ -507,6 +517,65 @@
         }

     }

 

+    public void testGetRedirectUrlNotHttp()

+        throws Exception

+    {

+        URL url = new URI( "ftp://some.where" ).toURL();

+        assertEquals( url.toString(), JavadocUtil.getRedirectUrl( url, new Settings() ).toString() );

+

+        url = new URI( "file://some/where" ).toURL();

+        assertEquals( url.toString(), JavadocUtil.getRedirectUrl( url, new Settings() ).toString() );

+    }

+

+    /**

+     * Tests a redirect from localhost:port1 to localhost:port2

+     */

+    public void testGetRedirectUrl()

+        throws Exception

+    {

+        Server server = null, redirectServer = null;

+        try

+        {

+            redirectServer = new Server( 0 );

+            redirectServer.addHandler( new AbstractHandler()

+            {

+                @Override

+                public void handle( String target, HttpServletRequest request, HttpServletResponse response,

+                                    int dispatch )

+                    throws IOException, ServletException

+                {

+                    response.setStatus( HttpServletResponse.SC_OK );

+                    ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer( 100 );

+                    writer.write( "<html>Hello world</html>" );

+                    writer.flush();

+                    response.setContentLength( writer.size() );

+                    OutputStream out = response.getOutputStream();

+                    writer.writeTo( out );

+                    out.close();

+                    writer.close();

+                }

+            } );

+            redirectServer.start();

+

+            server = new Server( 0 );

+            MovedContextHandler handler = new MovedContextHandler();

+            int redirectPort = redirectServer.getConnectors()[0].getLocalPort();

+            handler.setNewContextURL( "http://localhost:" + redirectPort );

+            server.addHandler( handler );

+            server.start();

+

+            URL url = new URI( "http://localhost:" + server.getConnectors()[0].getLocalPort() ).toURL();

+            URL redirectUrl = JavadocUtil.getRedirectUrl( url, new Settings() );

+

+            assertTrue( redirectUrl.toString().startsWith( "http://localhost:" + redirectPort ) );

+        }

+        finally

+        {

+            stopSilently( server );

+            stopSilently( redirectServer );

+        }

+    }

+

     /**

      * Method to test copyJavadocResources()

      *

@@ -626,4 +695,19 @@
         assertEquals( path1 + ps + path2 + ps + path1 + ps + path2, JavadocUtil.unifyPathSeparator( path1 + ";"

             + path2 + ":" + path1 + ":" + path2 ) );

     }

+

+    private void stopSilently( Server server )

+    {

+        try

+        {

+            if ( server != null )

+            {

+                server.stop();

+            }

+        }

+        catch ( Exception e )

+        {

+            // ignored

+        }

+    }

 }