View Javadoc
1   package org.apache.maven.tools.plugin.javadoc;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.io.InputStreamReader;
26  import java.io.Reader;
27  import java.net.MalformedURLException;
28  import java.net.SocketTimeoutException;
29  import java.net.URI;
30  import java.net.URISyntaxException;
31  import java.net.URL;
32  import java.util.AbstractMap;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.Collections;
36  import java.util.EnumMap;
37  import java.util.EnumSet;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Objects;
42  import java.util.Optional;
43  import java.util.function.BiFunction;
44  import java.util.regex.Pattern;
45  
46  import org.apache.http.HttpHeaders;
47  import org.apache.http.HttpHost;
48  import org.apache.http.HttpResponse;
49  import org.apache.http.HttpStatus;
50  import org.apache.http.auth.AuthScope;
51  import org.apache.http.auth.Credentials;
52  import org.apache.http.auth.UsernamePasswordCredentials;
53  import org.apache.http.client.CredentialsProvider;
54  import org.apache.http.client.config.CookieSpecs;
55  import org.apache.http.client.config.RequestConfig;
56  import org.apache.http.client.methods.HttpGet;
57  import org.apache.http.client.protocol.HttpClientContext;
58  import org.apache.http.config.Registry;
59  import org.apache.http.config.RegistryBuilder;
60  import org.apache.http.conn.socket.ConnectionSocketFactory;
61  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
62  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
63  import org.apache.http.impl.client.BasicCredentialsProvider;
64  import org.apache.http.impl.client.CloseableHttpClient;
65  import org.apache.http.impl.client.HttpClientBuilder;
66  import org.apache.http.impl.client.HttpClients;
67  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
68  import org.apache.http.message.BasicHeader;
69  import org.apache.maven.settings.Proxy;
70  import org.apache.maven.settings.Settings;
71  import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
72  import org.apache.maven.wagon.proxy.ProxyInfo;
73  import org.apache.maven.wagon.proxy.ProxyUtils;
74  import org.codehaus.plexus.util.StringUtils;
75  
76  /**
77   * Allows to create links to a site generated by javadoc (incl. deep-linking).
78   * The site may be either accessible (online) or non-accessible (offline) when using this class.
79   */
80  class JavadocSite
81  {
82      private static final String PREFIX_MODULE = "module:";
83  
84      final URI baseUri;
85  
86      final Settings settings;
87  
88      final Map<String, String> containedPackageNamesAndModules; // empty in case this an offline site
89  
90      final boolean requireModuleNameInPath;
91  
92      static final EnumMap<FullyQualifiedJavadocReference.MemberType, 
93                           EnumSet<JavadocLinkGenerator.JavadocToolVersionRange>>
94          VERSIONS_PER_TYPE;
95      static
96      {
97          VERSIONS_PER_TYPE = new EnumMap<>( FullyQualifiedJavadocReference.MemberType.class );
98          VERSIONS_PER_TYPE.put( MemberType.CONSTRUCTOR,
99                                          EnumSet.of( JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
100                                                     JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9,
101                                                     JavadocLinkGenerator.JavadocToolVersionRange.JDK10_OR_HIGHER ) );
102         VERSIONS_PER_TYPE.put( MemberType.METHOD,
103                                         EnumSet.of( JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
104                                                     JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9,
105                                                     JavadocLinkGenerator.JavadocToolVersionRange.JDK10_OR_HIGHER ) );
106         VERSIONS_PER_TYPE.put( MemberType.FIELD, EnumSet.of( JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
107                                                              JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9 ) );
108     }
109 
110     JavadocLinkGenerator.JavadocToolVersionRange version; // null in case not yet known for online sites
111 
112     /**
113      * Constructor for online sites having an accessible {@code package-list} or {@code element-list}.
114      * @param url
115      * @param settings
116      * @throws IOException
117      */
118     JavadocSite( final URI url, final Settings settings )
119         throws IOException
120     {
121         Map<String, String> containedPackageNamesAndModules;
122         boolean requireModuleNameInPath = false;
123         try
124         {
125             // javadoc > 1.2 && < 10
126             containedPackageNamesAndModules = getPackageListWithModules( url.resolve( "package-list" ), settings );
127         }
128         catch ( FileNotFoundException e )
129         {
130             try
131             {
132                 // javadoc 10+
133                 containedPackageNamesAndModules = getPackageListWithModules( url.resolve( "element-list" ), settings );
134 
135                 Optional<String> firstModuleName =
136                     containedPackageNamesAndModules.values().stream().filter( StringUtils::isNotBlank ).findFirst();
137                 if ( firstModuleName.isPresent() )
138                 {
139                     // are module names part of the URL (since JDK11)?
140                     try ( Reader reader =
141                         getReader( url.resolve( firstModuleName.get() + "/module-summary.html" ).toURL(), null ) )
142                     {
143                         requireModuleNameInPath = true;
144                     }
145                     catch ( IOException ioe )
146                     {
147                         // ignore
148                     }
149                 }
150             }
151             catch ( FileNotFoundException e2 )
152             {
153                 throw new IOException( "Found neither 'package-list' nor 'element-list' below url " + url
154                     + ". The given URL does probably not specify the root of a javadoc site or has been generated with"
155                     + " javadoc 1.2 or older." );
156             }
157         }
158         this.containedPackageNamesAndModules = containedPackageNamesAndModules;
159         this.baseUri = url;
160         this.settings = settings;
161         this.version = null;
162         this.requireModuleNameInPath = requireModuleNameInPath;
163     }
164 
165     /** Constructor for offline sites. This throws {@link UnsupportedOperationException} 
166      *  for {@link #hasEntryFor(Optional, Optional)}. */
167     JavadocSite( final URI url, JavadocLinkGenerator.JavadocToolVersionRange version, boolean requireModuleNameInPath )
168     {
169         Objects.requireNonNull( url );
170         this.baseUri = url;
171         Objects.requireNonNull( version );
172         this.version = version;
173         this.settings = null;
174         this.containedPackageNamesAndModules = Collections.emptyMap();
175         this.requireModuleNameInPath = requireModuleNameInPath;
176     }
177 
178     static Map<String, String> getPackageListWithModules( final URI url, final Settings settings )
179         throws IOException
180     {
181         Map<String, String> containedPackageNamesAndModules = new HashMap<>();
182         try ( BufferedReader reader = getReader( url.toURL(), settings ) )
183         {
184             String line;
185             String module = null;
186             while ( ( line = reader.readLine() ) != null )
187             {
188                 // each line starting with "module:" contains the module name afterwards
189                 if ( line.startsWith( PREFIX_MODULE ) )
190                 {
191                     module = line.substring( PREFIX_MODULE.length() );
192                 }
193                 else
194                 {
195                     containedPackageNamesAndModules.put( line, module );
196                 }
197             }
198             return containedPackageNamesAndModules;
199         }
200     }
201 
202     static boolean findLineContaining( final URI url, final Settings settings, Pattern pattern )
203         throws IOException
204     {
205         try ( BufferedReader reader = getReader( url.toURL(), settings ) )
206         {
207             return reader.lines().anyMatch( pattern.asPredicate() );
208         }
209     }
210 
211     public URI getBaseUri()
212     {
213         return baseUri;
214     }
215 
216     public boolean hasEntryFor( Optional<String> moduleName, Optional<String> packageName )
217     {
218         if ( containedPackageNamesAndModules.isEmpty() )
219         {
220             throw new UnsupportedOperationException( "Operation hasEntryFor(...) is not supported for offline "
221                 + "javadoc sites" );
222         }
223         if ( packageName.isPresent() )
224         {
225             if ( moduleName.isPresent() )
226             {
227                 String actualModuleName = containedPackageNamesAndModules.get( packageName.get() );
228                 if ( !moduleName.get().equals( actualModuleName ) )
229                 {
230                     return false;
231                 }
232             }
233             else
234             {
235                 if ( !containedPackageNamesAndModules.containsKey( packageName.get() ) )
236                 {
237                     return false;
238                 }
239             }
240         }
241         else if ( moduleName.isPresent() )
242         {
243             if ( !containedPackageNamesAndModules.containsValue( moduleName.get() ) )
244             {
245                 return false;
246             }
247         }
248         else
249         {
250             throw new IllegalArgumentException( "Either module name or package name must be set!" );
251         }
252         return true;
253     }
254 
255     /**
256      * Generates a link to a javadoc html page below the javadoc site represented by this object.
257      * The link is not validated (i.e. might point to a non-existing page)
258      * @param 
259      * @return the (deep-)link towards a javadoc page
260      * @throws IllegalArgumentException if no link can be created
261      */
262     public URI createLink( String packageName, String className )
263     {
264         try
265         {
266             if ( className.endsWith( "[]" ) )
267             {
268                 // url must point to simple class
269                 className = className.substring( 0, className.length()  - 2 );
270             }
271             return createLink( baseUri, Optional.empty(), Optional.of( packageName ),
272                                Optional.of( className ) );
273         }
274         catch ( URISyntaxException e )
275         {
276             throw new IllegalArgumentException( "Could not create link for " + packageName + "." + className, e );
277         }
278     }
279 
280     /**
281      * Splits up a given binary name into package name and class name part.
282      * @param binaryName a binary name according to 
283      * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1">JLS 13.1</a>
284      * @return a key value pair where the key is the package name and the value the class name
285      * @throws IllegalArgumentException if no link can be created
286      */
287     static Map.Entry<String, String> getPackageAndClassName( String binaryName )
288     {
289      // assume binary name according to https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1
290         int indexOfDollar = binaryName.indexOf( '$' );
291         if ( indexOfDollar >= 0 ) 
292         {
293             // emit some warning, as non resolvable: unclear which type of member follows if it is non digit
294             throw new IllegalArgumentException( "Can only resolve binary names of top level classes" );
295         }
296         int indexOfLastDot = binaryName.lastIndexOf( '.' );
297         if ( indexOfLastDot < 0 )
298         {
299             throw new IllegalArgumentException( "Resolving primitives is not supported. "
300                 + "Binary name must contain at least one dot: " + binaryName );
301         }
302         if ( indexOfLastDot == binaryName.length() - 1 )
303         {
304             throw new IllegalArgumentException( "Invalid binary name ending with a dot: " + binaryName );
305         }
306         String packageName = binaryName.substring( 0, indexOfLastDot );
307         String className = binaryName.substring( indexOfLastDot + 1, binaryName.length() );
308         return new AbstractMap.SimpleEntry<>( packageName, className );
309     }
310 
311     /**
312      * Generates a link to a javadoc html page below the javadoc site represented by this object.
313      * The link is not validated (i.e. might point to a non-existing page)
314      * @param javadocReference a code reference from a javadoc tag
315      * @return  the (deep-)link towards a javadoc page
316      * @throws IllegalArgumentException if no link can be created
317      */
318     public URI createLink( FullyQualifiedJavadocReference javadocReference )
319         throws IllegalArgumentException
320     {
321         final Optional<String> moduleName;
322         if ( !requireModuleNameInPath )
323         {
324             moduleName = Optional.empty();
325         }
326         else
327         {
328             moduleName =
329                 Optional.ofNullable( javadocReference.getModuleName().orElse( 
330                     containedPackageNamesAndModules.get( javadocReference.getPackageName().orElse( null ) ) ) );
331         }
332         return createLink( javadocReference, baseUri, this::appendMemberAsFragment, moduleName );
333     }
334 
335     static URI createLink( FullyQualifiedJavadocReference javadocReference, URI baseUri,
336                            BiFunction<URI, FullyQualifiedJavadocReference, URI> fragmentAppender )
337     {
338         return createLink( javadocReference, baseUri, fragmentAppender, Optional.empty() );
339     }
340 
341     static URI createLink( FullyQualifiedJavadocReference javadocReference, URI baseUri,
342                            BiFunction<URI, FullyQualifiedJavadocReference, URI> fragmentAppender,
343                            Optional<String> pathPrefix )
344         throws IllegalArgumentException
345     {
346         try
347         {
348             URI uri = createLink( baseUri, javadocReference.getModuleName(), javadocReference.getPackageName(),
349                                   javadocReference.getClassName() );
350             return fragmentAppender.apply( uri, javadocReference );
351         }
352         catch ( URISyntaxException e )
353         {
354             throw new IllegalArgumentException( "Could not create link for " + javadocReference, e );
355         }
356     }
357 
358     static URI createLink( URI baseUri, Optional<String> moduleName, Optional<String> packageName,
359                            Optional<String> className ) throws URISyntaxException
360     {
361         StringBuilder link = new StringBuilder();
362         if ( moduleName.isPresent() )
363         {
364             link.append( moduleName.get() + "/" );
365         }
366         if ( packageName.isPresent() )
367         {
368             link.append( packageName.get().replace( '.', '/' ) );
369         }
370         if ( !className.isPresent() )
371         {
372             if ( packageName.isPresent() )
373             {
374                 link.append( "/package-summary.html" );
375             }
376             else if ( moduleName.isPresent() )
377             {
378                 link.append( "/module-summary.html" );
379             }
380         }
381         else
382         {
383             link.append( '/' ).append( className.get() ).append( ".html" );
384         }
385         return  baseUri.resolve( new URI( null, link.toString(), null ) );
386     }
387 
388     
389     URI appendMemberAsFragment( URI url, FullyQualifiedJavadocReference reference )
390     {
391         try
392         {
393             return appendMemberAsFragment( url, reference.getMember(), reference.getMemberType() );
394         }
395         catch ( URISyntaxException | IOException e )
396         {
397             throw new IllegalArgumentException( "Could not create link for " + reference, e );
398         }
399     }
400 
401     // CHECKSTYLE_OFF: LineLength
402     /**
403      * @param url
404      * @param optionalMember
405      * @param optionalMemberType
406      * @return
407      * @throws URISyntaxException
408      * @throws IOException
409      * @see <a href=
410      *      "https://github.com/openjdk/jdk8u-dev/blob/f0ac31998d8396d92b4ce99aa345c05e6fd0f02a/langtools/src/share/classes/com/sun/tools/doclets/formats/html/markup/HtmlDocWriter.java#L154">
411      *      Name generation in Javadoc8</a>
412      * @see <a href=
413      *      "https://github.com/openjdk/jdk/tree/master/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html">Javadoc
414      *      Tools Source since JDK10</a>
415      * @see <a href=
416      *      "https://github.com/openjdk/jdk/tree/jdk-9%2B181/langtools/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html">Javadoc
417      *      Tools Source JDK9<a/>
418      * @see <a href=
419      *      "https://github.com/openjdk/jdk/tree/jdk8-b93/langtools/src/share/classes/com/sun/tools/javadoc">Javadoc
420      *      Tools Source JDK8</a>
421      */
422     // CHECKSTYLE_ON: LineLength
423     URI appendMemberAsFragment( URI url, Optional<String> optionalMember, Optional<MemberType> optionalMemberType )
424         throws URISyntaxException, IOException
425     {
426         if ( !optionalMember.isPresent() )
427         {
428             return url;
429         }
430         MemberType memberType = optionalMemberType.orElse( null );
431         final String member = optionalMember.get();
432         String fragment = member;
433         if ( version != null )
434         {
435             fragment = getFragmentForMember( version, member, memberType == MemberType.CONSTRUCTOR );
436         }
437         else
438         {
439             // try out all potential formats
440             for ( JavadocLinkGenerator.JavadocToolVersionRange potentialVersion : VERSIONS_PER_TYPE.get( memberType ) )
441             {
442                 fragment = getFragmentForMember( potentialVersion, member, memberType == MemberType.CONSTRUCTOR );
443                 if ( findAnchor( url, fragment ) )
444                 {
445                     // only derive javadoc version if there is no ambiguity
446                     if ( memberType == MemberType.CONSTRUCTOR || memberType == MemberType.METHOD )
447                     {
448                         version = potentialVersion;
449                     }
450                     break;
451                 }
452             }
453         }
454         return new URI( url.getScheme(), url.getSchemeSpecificPart(), fragment );
455     }
456 
457     /**
458      * canonical format given by member is using parentheses and comma.
459      * 
460      * @param version
461      * @param member
462      * @param isConstructor
463      * @return the anchor
464      */
465     static String getFragmentForMember( JavadocLinkGenerator.JavadocToolVersionRange version, String member,
466                                         boolean isConstructor )
467     {
468         String fragment = member;
469         switch ( version )
470         {
471             case JDK7_OR_LOWER:
472                 // separate argument by spaces
473                 fragment = fragment.replace( ",", ", " );
474                 break;
475             case JDK8_OR_9:
476                 // replace [] by ":A"
477                 fragment = fragment.replace( "[]", ":A" );
478                 // separate arguments by "-", enclose all arguments in "-" for javadoc 8
479                 fragment = fragment.replace( '(', '-' ).replace( ')', '-' ).replace( ',', '-' );
480                 break;
481             case JDK10_OR_HIGHER:
482                 if ( isConstructor )
483                 {
484                     int indexOfOpeningParenthesis = fragment.indexOf( '(' );
485                     if ( indexOfOpeningParenthesis >= 0 )
486                     {
487                         fragment = "&lt;init&gt;" + fragment.substring( indexOfOpeningParenthesis );
488                     }
489                     else
490                     {
491                         fragment = "&lt;init&gt;";
492                     }
493                 }
494                 break;
495             default:
496                 throw new IllegalArgumentException( "No valid version range given" );
497         }
498         return fragment;
499     }
500 
501     boolean findAnchor( URI uri, String anchorNameOrId )
502         throws MalformedURLException, IOException
503     {
504         return findLineContaining( uri, settings, getAnchorPattern( anchorNameOrId ) );
505     }
506 
507     static Pattern getAnchorPattern( String anchorNameOrId )
508     {
509         // javadoc 17 uses"<section ... id=<anchor> >"
510         return Pattern.compile( ".*(name|NAME|id)=\\\"" + Pattern.quote( anchorNameOrId ) + "\\\"" );
511     }
512 
513     // ---------------
514     // CHECKSTYLE_OFF: LineLength
515     // the following methods are copies from private methods contained in
516     // https://github.com/apache/maven-javadoc-plugin/blob/231316be785782b61d96783fad111325868cfa1f/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java
517     // CHECKSTYLE_ON: LineLength
518     // ---------------
519     /** The default timeout used when fetching url, i.e. 2000. */
520     public static final int DEFAULT_TIMEOUT = 2000;
521 
522     /**
523      * Creates a new {@code HttpClient} instance.
524      *
525      * @param settings The settings to use for setting up the client or {@code null}.
526      * @param url The {@code URL} to use for setting up the client or {@code null}.
527      * @return A new {@code HttpClient} instance.
528      * @see #DEFAULT_TIMEOUT
529      * @since 2.8
530      */
531     private static CloseableHttpClient createHttpClient( Settings settings, URL url )
532     {
533         HttpClientBuilder builder = HttpClients.custom();
534 
535         Registry<ConnectionSocketFactory> csfRegistry =
536             RegistryBuilder.<ConnectionSocketFactory>create()
537                 .register( "http", PlainConnectionSocketFactory.getSocketFactory() )
538                 .register( "https",  SSLConnectionSocketFactory.getSystemSocketFactory() ).build();
539 
540         builder.setConnectionManager( new PoolingHttpClientConnectionManager( csfRegistry ) );
541         builder.setDefaultRequestConfig( RequestConfig.custom().setSocketTimeout( DEFAULT_TIMEOUT )
542                                          .setConnectTimeout( DEFAULT_TIMEOUT ).setCircularRedirectsAllowed( true )
543                                          .setCookieSpec( CookieSpecs.IGNORE_COOKIES ).build() );
544 
545         // Some web servers don't allow the default user-agent sent by httpClient
546         builder.setUserAgent( "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)" );
547 
548         // Some server reject requests that do not have an Accept header
549         builder.setDefaultHeaders( Arrays.asList( new BasicHeader( HttpHeaders.ACCEPT, "*/*" ) ) );
550 
551         if ( settings != null && settings.getActiveProxy() != null )
552         {
553             Proxy activeProxy = settings.getActiveProxy();
554 
555             ProxyInfo proxyInfo = new ProxyInfo();
556             proxyInfo.setNonProxyHosts( activeProxy.getNonProxyHosts() );
557 
558             if ( StringUtils.isNotEmpty( activeProxy.getHost() )
559                 && ( url == null || !ProxyUtils.validateNonProxyHosts( proxyInfo, url.getHost() ) ) )
560             {
561                 HttpHost proxy = new HttpHost( activeProxy.getHost(), activeProxy.getPort() );
562                 builder.setProxy( proxy );
563 
564                 if ( StringUtils.isNotEmpty( activeProxy.getUsername() ) && activeProxy.getPassword() != null )
565                 {
566                     Credentials credentials =
567                         new UsernamePasswordCredentials( activeProxy.getUsername(), activeProxy.getPassword() );
568 
569                     CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
570                     credentialsProvider.setCredentials( AuthScope.ANY, credentials );
571                     builder.setDefaultCredentialsProvider( credentialsProvider );
572                 }
573             }
574         }
575         return builder.build();
576     }
577 
578     static BufferedReader getReader( URL url, Settings settings )
579         throws IOException
580     {
581         BufferedReader reader = null;
582 
583         if ( "file".equals( url.getProtocol() ) )
584         {
585             // Intentionally using the platform default encoding here since this is what Javadoc uses internally.
586             reader = new BufferedReader( new InputStreamReader( url.openStream() ) );
587         }
588         else
589         {
590             // http, https...
591             final CloseableHttpClient httpClient = createHttpClient( settings, url );
592 
593             final HttpGet httpMethod = new HttpGet( url.toString() );
594 
595             HttpResponse response;
596             HttpClientContext httpContext = HttpClientContext.create();
597             try
598             {
599                 response = httpClient.execute( httpMethod, httpContext );
600             }
601             catch ( SocketTimeoutException e )
602             {
603                 // could be a sporadic failure, one more retry before we give up
604                 response = httpClient.execute( httpMethod, httpContext );
605             }
606 
607             int status = response.getStatusLine().getStatusCode();
608             if ( status != HttpStatus.SC_OK )
609             {
610                 throw new FileNotFoundException( "Unexpected HTTP status code " + status + " getting resource "
611                     + url.toExternalForm() + "." );
612             }
613             else
614             {
615                 int pos = url.getPath().lastIndexOf( '/' );
616                 List<URI> redirects = httpContext.getRedirectLocations();
617                 if ( pos >= 0 && isNotEmpty( redirects ) )
618                 {
619                     URI location = redirects.get( redirects.size() - 1 );
620                     String suffix = url.getPath().substring( pos );
621                     // Redirections shall point to the same file, e.g. /package-list
622                     if ( !location.getPath().endsWith( suffix ) )
623                     {
624                         throw new FileNotFoundException( url.toExternalForm() + " redirects to "
625                             + location.toURL().toExternalForm() + "." );
626                     }
627                 }
628             }
629 
630             // Intentionally using the platform default encoding here since this is what Javadoc uses internally.
631             reader = new BufferedReader( new InputStreamReader( response.getEntity().getContent() ) )
632             {
633                 @Override
634                 public void close()
635                     throws IOException
636                 {
637                     super.close();
638 
639                     if ( httpMethod != null )
640                     {
641                         httpMethod.releaseConnection();
642                     }
643                     if ( httpClient != null )
644                     {
645                         httpClient.close();
646                     }
647                 }
648             };
649         }
650 
651         return reader;
652     }
653 
654     /**
655      * Convenience method to determine that a collection is not empty or null.
656      * 
657      * @param collection the collection to verify
658      * @return {@code true} if not {@code null} and not empty, otherwise {@code false}
659      */
660     public static boolean isNotEmpty( final Collection<?> collection )
661     {
662         return collection != null && !collection.isEmpty();
663     }
664 }