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