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