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.plugins.javadoc;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileNotFoundException;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStreamReader;
28  import java.io.OutputStream;
29  import java.io.PrintStream;
30  import java.io.UnsupportedEncodingException;
31  import java.lang.reflect.Modifier;
32  import java.net.SocketTimeoutException;
33  import java.net.URI;
34  import java.net.URISyntaxException;
35  import java.net.URL;
36  import java.net.URLClassLoader;
37  import java.nio.charset.Charset;
38  import java.nio.charset.IllegalCharsetNameException;
39  import java.nio.file.FileVisitResult;
40  import java.nio.file.Files;
41  import java.nio.file.Path;
42  import java.nio.file.Paths;
43  import java.nio.file.SimpleFileVisitor;
44  import java.nio.file.attribute.BasicFileAttributes;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.LinkedHashSet;
50  import java.util.List;
51  import java.util.NoSuchElementException;
52  import java.util.Properties;
53  import java.util.Set;
54  import java.util.StringTokenizer;
55  import java.util.jar.JarEntry;
56  import java.util.jar.JarInputStream;
57  import java.util.regex.Matcher;
58  import java.util.regex.Pattern;
59  import java.util.regex.PatternSyntaxException;
60  
61  import org.apache.http.HttpHeaders;
62  import org.apache.http.HttpHost;
63  import org.apache.http.HttpResponse;
64  import org.apache.http.HttpStatus;
65  import org.apache.http.auth.AuthScope;
66  import org.apache.http.auth.Credentials;
67  import org.apache.http.auth.UsernamePasswordCredentials;
68  import org.apache.http.client.CredentialsProvider;
69  import org.apache.http.client.config.CookieSpecs;
70  import org.apache.http.client.config.RequestConfig;
71  import org.apache.http.client.methods.HttpGet;
72  import org.apache.http.client.protocol.HttpClientContext;
73  import org.apache.http.config.Registry;
74  import org.apache.http.config.RegistryBuilder;
75  import org.apache.http.conn.socket.ConnectionSocketFactory;
76  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
77  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
78  import org.apache.http.impl.client.BasicCredentialsProvider;
79  import org.apache.http.impl.client.CloseableHttpClient;
80  import org.apache.http.impl.client.HttpClientBuilder;
81  import org.apache.http.impl.client.HttpClients;
82  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
83  import org.apache.http.message.BasicHeader;
84  import org.apache.maven.plugin.logging.Log;
85  import org.apache.maven.project.MavenProject;
86  import org.apache.maven.settings.Proxy;
87  import org.apache.maven.settings.Settings;
88  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
89  import org.apache.maven.shared.invoker.DefaultInvoker;
90  import org.apache.maven.shared.invoker.InvocationOutputHandler;
91  import org.apache.maven.shared.invoker.InvocationRequest;
92  import org.apache.maven.shared.invoker.InvocationResult;
93  import org.apache.maven.shared.invoker.Invoker;
94  import org.apache.maven.shared.invoker.MavenInvocationException;
95  import org.apache.maven.shared.invoker.PrintStreamHandler;
96  import org.apache.maven.shared.utils.io.DirectoryScanner;
97  import org.apache.maven.shared.utils.io.FileUtils;
98  import org.apache.maven.wagon.proxy.ProxyInfo;
99  import org.apache.maven.wagon.proxy.ProxyUtils;
100 import org.codehaus.plexus.languages.java.version.JavaVersion;
101 import org.codehaus.plexus.util.cli.CommandLineException;
102 import org.codehaus.plexus.util.cli.CommandLineUtils;
103 import org.codehaus.plexus.util.cli.Commandline;
104 
105 /**
106  * Set of utilities methods for Javadoc.
107  *
108  * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
109  * @since 2.4
110  */
111 public class JavadocUtil {
112     /** The default timeout used when fetching url, i.e. 2000. */
113     public static final int DEFAULT_TIMEOUT = 2000;
114 
115     /** Error message when VM could not be started using invoker. */
116     protected static final String ERROR_INIT_VM =
117             "Error occurred during initialization of VM, try to reduce the Java heap size for the MAVEN_OPTS "
118                     + "environment variable using -Xms:<size> and -Xmx:<size>.";
119 
120     /**
121      * Method that removes invalid classpath elements in the specified paths.
122      * <b>Note</b>: All elements in {@code paths} could be absolute or relative against the project's base directory.
123      * When pruning classpath elements, you can optionally include files in the result, otherwise only directories are
124      * permitted.
125      *
126      * @param project the current Maven project not null
127      * @param paths the collection of paths that will be validated
128      * @param includeFiles whether to include files in the result as well
129      * @return a list of valid classpath elements as absolute paths
130      */
131     public static Collection<Path> prunePaths(MavenProject project, Collection<String> paths, boolean includeFiles) {
132         final Path projectBasedir = project.getBasedir().toPath();
133 
134         Set<Path> pruned = new LinkedHashSet<>(paths.size());
135         for (String path : paths) {
136             if (path == null) {
137                 continue;
138             }
139 
140             Path resolvedPath = projectBasedir.resolve(path);
141 
142             if (Files.isDirectory(resolvedPath) || includeFiles && Files.isRegularFile(resolvedPath)) {
143                 pruned.add(resolvedPath.toAbsolutePath());
144             }
145         }
146 
147         return pruned;
148     }
149 
150     /**
151      * Method that removes the invalid classpath directories in the specified directories.
152      * <b>Note</b>: All elements in {@code dirs} could be absolute or relative against the project's base directory.
153      *
154      * @param project the current Maven project not null
155      * @param dirs the collection of directories that will be validated
156      * @return a list of valid claspath elements as absolute paths
157      */
158     public static Collection<Path> pruneDirs(MavenProject project, Collection<String> dirs) {
159         return prunePaths(project, dirs, false);
160     }
161 
162     /**
163      * Method that removes the invalid files in the specified files. <b>Note</b>: All elements in <code>files</code>
164      * should be an absolute <code>String</code> path.
165      *
166      * @param files the list of <code>String</code> files paths that will be validated.
167      * @return a List of valid <code>File</code> objects.
168      */
169     protected static List<String> pruneFiles(Collection<String> files) {
170         List<String> pruned = new ArrayList<>(files.size());
171         for (String f : files) {
172             if (!shouldPruneFile(f, pruned)) {
173                 pruned.add(f);
174             }
175         }
176 
177         return pruned;
178     }
179 
180     /**
181      * Determine whether a file should be excluded from the provided list of paths, based on whether it exists and is
182      * already present in the list.
183      *
184      * @param f The files.
185      * @param pruned The list of pruned files..
186      * @return true if the file could be pruned false otherwise.
187      */
188     public static boolean shouldPruneFile(String f, List<String> pruned) {
189         if (f != null) {
190             if (Files.isRegularFile(Paths.get(f)) && !pruned.contains(f)) {
191                 return false;
192             }
193         }
194 
195         return true;
196     }
197 
198     /**
199      * Method that gets all the source files to be excluded from the javadoc on the given source paths.
200      *
201      * @param sourcePaths the path to the source files
202      * @param excludedPackages the package names to be excluded in the javadoc
203      * @return a List of the packages to be excluded in the generated javadoc
204      */
205     protected static List<String> getExcludedPackages(
206             Collection<Path> sourcePaths, Collection<String> excludedPackages) {
207         List<String> excludedNames = new ArrayList<>();
208         for (Path sourcePath : sourcePaths) {
209             excludedNames.addAll(getExcludedPackages(sourcePath, excludedPackages));
210         }
211 
212         return excludedNames;
213     }
214 
215     /**
216      * Convenience method to wrap a command line option-argument in single quotes (i.e. <code>'</code>). Intended for values which
217      * may contain whitespace. <br>
218      * Line feeds (i.e. <code>\n</code>) are replaced with spaces, and single quotes are backslash escaped.
219      *
220      * @param value the option-argument
221      * @return quoted option-argument
222      */
223     protected static String quotedArgument(String value) {
224         String arg = value;
225 
226         if (arg != null && !arg.isEmpty()) {
227             arg = arg.replace("'", "\\'");
228             arg = "'" + arg + "'";
229 
230             // To prevent javadoc error
231             arg = arg.replace("\n", " ");
232         }
233 
234         return arg;
235     }
236 
237     /**
238      * Convenience method to format a path argument so that it is properly interpreted by the javadoc tool. Intended for
239      * path values which may contain whitespaces.
240      *
241      * @param value the argument value.
242      * @return path argument with quote
243      */
244     protected static String quotedPathArgument(String value) {
245         String path = value;
246 
247         if (path != null && !path.isEmpty()) {
248             path = path.replace('\\', '/');
249             if (path.contains("'")) {
250                 StringBuilder pathBuilder = new StringBuilder();
251                 pathBuilder.append('\'');
252                 String[] split = path.split("'");
253 
254                 for (int i = 0; i < split.length; i++) {
255                     if (i != split.length - 1) {
256                         pathBuilder.append(split[i]).append("\\'");
257                     } else {
258                         pathBuilder.append(split[i]);
259                     }
260                 }
261                 pathBuilder.append('\'');
262                 path = pathBuilder.toString();
263             } else {
264                 path = "'" + path + "'";
265             }
266         }
267 
268         return path;
269     }
270 
271     /**
272      * Convenience method that copy all <code>doc-files</code> directories from <code>javadocDir</code> to the
273      * <code>outputDirectory</code>.
274      *
275      * @param outputDirectory the output directory
276      * @param javadocDir the javadoc directory
277      * @param excludedocfilessubdir the excludedocfilessubdir parameter
278      * @throws IOException if any
279      * @since 2.5
280      */
281     protected static void copyJavadocResources(File outputDirectory, File javadocDir, String excludedocfilessubdir)
282             throws IOException {
283         if (!javadocDir.isDirectory()) {
284             return;
285         }
286 
287         List<String> excludes = new ArrayList<>(Arrays.asList(FileUtils.getDefaultExcludes()));
288 
289         if (excludedocfilessubdir != null && !excludedocfilessubdir.isEmpty()) {
290             StringTokenizer st = new StringTokenizer(excludedocfilessubdir, ":");
291             String current;
292             while (st.hasMoreTokens()) {
293                 current = st.nextToken();
294                 excludes.add("**/" + current + "/**");
295             }
296         }
297 
298         List<String> docFiles = FileUtils.getDirectoryNames(
299                 javadocDir, "resources,**/doc-files", String.join(",", excludes), false, true);
300         for (String docFile : docFiles) {
301             File docFileOutput = new File(outputDirectory, docFile);
302             FileUtils.mkdir(docFileOutput.getAbsolutePath());
303             FileUtils.copyDirectoryStructure(new File(javadocDir, docFile), docFileOutput);
304             List<String> files = FileUtils.getFileAndDirectoryNames(
305                     docFileOutput, String.join(",", excludes), null, true, true, true, true);
306             for (String filename : files) {
307                 File file = new File(filename);
308 
309                 if (file.isDirectory()) {
310                     FileUtils.deleteDirectory(file);
311                 } else {
312                     file.delete();
313                 }
314             }
315         }
316     }
317 
318     /**
319      * Method that gets the files or classes that would be included in the javadocs using the subpackages parameter.
320      *
321      * @param sourceDirectory the directory where the source files are located
322      * @param fileList the list of all relative files found in the sourceDirectory
323      * @param excludePackages package names to be excluded in the javadoc
324      * @return a StringBuilder that contains the appended file names of the files to be included in the javadoc
325      */
326     protected static List<String> getIncludedFiles(
327             File sourceDirectory, String[] fileList, Collection<String> excludePackages) {
328         List<String> files = new ArrayList<>();
329 
330         List<Pattern> excludePackagePatterns = new ArrayList<>(excludePackages.size());
331         for (String excludePackage : excludePackages) {
332             excludePackagePatterns.add(Pattern.compile(excludePackage
333                     .replace('.', File.separatorChar)
334                     .replace("\\", "\\\\")
335                     .replace("*", ".+")
336                     .concat("[\\\\/][^\\\\/]+\\.java")));
337         }
338 
339         for (String file : fileList) {
340             boolean excluded = false;
341             for (Pattern excludePackagePattern : excludePackagePatterns) {
342                 if (excludePackagePattern.matcher(file).matches()) {
343                     excluded = true;
344                     break;
345                 }
346             }
347 
348             if (!excluded) {
349                 files.add(file.replace('\\', '/'));
350             }
351         }
352 
353         return files;
354     }
355 
356     /**
357      * Method that gets the complete package names (including subpackages) of the packages that were defined in the
358      * excludePackageNames parameter.
359      *
360      * @param sourceDirectory the directory where the source files are located
361      * @param excludePackagenames package names to be excluded in the javadoc
362      * @return a List of the packagenames to be excluded
363      */
364     protected static Collection<String> getExcludedPackages(
365             final Path sourceDirectory, Collection<String> excludePackagenames) {
366         final String regexFileSeparator = File.separator.replace("\\", "\\\\");
367 
368         final Collection<String> fileList = new ArrayList<>();
369 
370         try {
371             Files.walkFileTree(sourceDirectory, new SimpleFileVisitor<Path>() {
372                 @Override
373                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
374                     if (file.getFileName().toString().endsWith(".java")) {
375                         fileList.add(
376                                 sourceDirectory.relativize(file.getParent()).toString());
377                     }
378                     return FileVisitResult.CONTINUE;
379                 }
380             });
381         } catch (IOException e) {
382             // noop
383         }
384 
385         List<String> files = new ArrayList<>();
386         for (String excludePackagename : excludePackagenames) {
387             // Usage of wildcard was bad specified and bad implemented, i.e. using String.contains()
388             //   without respecting surrounding context
389             // Following implementation should match requirements as defined in the examples:
390             // - A wildcard at the beginning should match one or more directories
391             // - Any other wildcard must match exactly one directory
392             Pattern p = Pattern.compile(excludePackagename
393                     .replace(".", regexFileSeparator)
394                     .replaceFirst("^\\*", ".+")
395                     .replace("*", "[^" + regexFileSeparator + "]+"));
396 
397             for (String aFileList : fileList) {
398                 if (p.matcher(aFileList).matches()) {
399                     files.add(aFileList.replace(File.separatorChar, '.'));
400                 }
401             }
402         }
403 
404         return files;
405     }
406 
407     /**
408      * Convenience method that gets the files to be included in the javadoc.
409      *
410      * @param sourceDirectory the directory where the source files are located
411      * @param sourceFileIncludes files to include
412      * @param sourceFileExcludes files to exclude
413      * @param excludePackages packages to be excluded from the javadocs
414      * @return the files from which javadoc should be generated
415      */
416     protected static List<String> getFilesFromSource(
417             File sourceDirectory,
418             List<String> sourceFileIncludes,
419             List<String> sourceFileExcludes,
420             Collection<String> excludePackages) {
421         DirectoryScanner ds = new DirectoryScanner();
422         if (sourceFileIncludes == null) {
423             sourceFileIncludes = Collections.singletonList("**/*.java");
424         }
425         ds.setIncludes(sourceFileIncludes.toArray(new String[sourceFileIncludes.size()]));
426         if (sourceFileExcludes != null && sourceFileExcludes.size() > 0) {
427             ds.setExcludes(sourceFileExcludes.toArray(new String[sourceFileExcludes.size()]));
428         }
429         ds.setBasedir(sourceDirectory);
430         ds.scan();
431 
432         String[] fileList = ds.getIncludedFiles();
433 
434         List<String> files = new ArrayList<>();
435         if (fileList.length != 0) {
436             files.addAll(getIncludedFiles(sourceDirectory, fileList, excludePackages));
437         }
438 
439         return files;
440     }
441 
442     /**
443      * Call the Javadoc tool and parse its output to find its version, i.e.:
444      *
445      * <pre>
446      * javadoc.exe( or.sh ) - J - version
447      * </pre>
448      *
449      * @param javadocExe not null file
450      * @return the javadoc version as float
451      * @throws IOException if javadocExe is null, doesn't exist or is not a file
452      * @throws CommandLineException if any
453      * @throws IllegalArgumentException if no output was found in the command line
454      * @throws PatternSyntaxException if the output contains a syntax error in the regular-expression pattern.
455      * @see #extractJavadocVersion(String)
456      */
457     protected static JavaVersion getJavadocVersion(File javadocExe)
458             throws IOException, CommandLineException, IllegalArgumentException {
459         if ((javadocExe == null) || (!javadocExe.exists()) || (!javadocExe.isFile())) {
460             throw new IOException("The javadoc executable '" + javadocExe + "' doesn't exist or is not a file. ");
461         }
462 
463         Commandline cmd = new Commandline();
464         cmd.setExecutable(javadocExe.getAbsolutePath());
465         cmd.setWorkingDirectory(javadocExe.getParentFile());
466         cmd.createArg().setValue("-J-version");
467 
468         CommandLineUtils.StringStreamConsumer out = new JavadocOutputStreamConsumer();
469         CommandLineUtils.StringStreamConsumer err = new JavadocOutputStreamConsumer();
470 
471         int exitCode = CommandLineUtils.executeCommandLine(cmd, out, err);
472         String errOutput = err.getOutput(); // non-null
473         if (exitCode != 0) {
474             StringBuilder msg = new StringBuilder("Exit code: " + exitCode + " - " + errOutput);
475             msg.append('\n');
476             msg.append("Command line was:").append(CommandLineUtils.toString(cmd.getCommandline()));
477             throw new CommandLineException(msg.toString());
478         }
479 
480         if (!errOutput.isEmpty()) {
481             return JavaVersion.parse(extractJavadocVersion(errOutput));
482         } else {
483             String outOutput = out.getOutput(); // non-null
484             if (!outOutput.isEmpty()) {
485                 return JavaVersion.parse(extractJavadocVersion(outOutput));
486             }
487         }
488 
489         throw new IllegalArgumentException("No output found from the command line 'javadoc -J-version'");
490     }
491 
492     private static final Pattern EXTRACT_JAVADOC_VERSION_PATTERN =
493             Pattern.compile("(?s).*?[^a-zA-Z](([0-9]+\\.?[0-9]*)(\\.[0-9]+)?).*");
494 
495     /**
496      * Parse the output for 'javadoc -J-version' and return the javadoc version recognized. <br>
497      * Here are some output for 'javadoc -J-version' depending the JDK used:
498      * <table><caption>Output for 'javadoc -J-version' per JDK</caption>
499      * <tr>
500      * <th>JDK</th>
501      * <th>Output for 'javadoc -J-version'</th>
502      * </tr>
503      * <tr>
504      * <td>Sun 1.4</td>
505      * <td>java full version "1.4.2_12-b03"</td>
506      * </tr>
507      * <tr>
508      * <td>Sun 1.5</td>
509      * <td>java full version "1.5.0_07-164"</td>
510      * </tr>
511      * <tr>
512      * <td>IBM 1.4</td>
513      * <td>javadoc full version "J2RE 1.4.2 IBM Windows 32 build cn1420-20040626"</td>
514      * </tr>
515      * <tr>
516      * <td>IBM 1.5 (French JVM)</td>
517      * <td>javadoc version complète de "J2RE 1.5.0 IBM Windows 32 build pwi32pdev-20070426a"</td>
518      * </tr>
519      * <tr>
520      * <td>FreeBSD 1.5</td>
521      * <td>java full version "diablo-1.5.0-b01"</td>
522      * </tr>
523      * <tr>
524      * <td>BEA jrockit 1.5</td>
525      * <td>java full version "1.5.0_11-b03"</td>
526      * </tr>
527      * </table>
528      *
529      * @param output for 'javadoc -J-version'
530      * @return the version of the javadoc for the output, only digits and dots
531      * @throws PatternSyntaxException if the output doesn't match with the output pattern
532      *             {@code (?s).*?[^a-zA-Z]([0-9]+\\.?[0-9]*)(\\.([0-9]+))?.*}.
533      * @throws IllegalArgumentException if the output is null
534      */
535     protected static String extractJavadocVersion(String output) throws IllegalArgumentException {
536         if (output == null || output.isEmpty()) {
537             throw new IllegalArgumentException("The output could not be null.");
538         }
539 
540         Pattern pattern = EXTRACT_JAVADOC_VERSION_PATTERN;
541 
542         Matcher matcher = pattern.matcher(output);
543         if (!matcher.matches()) {
544             throw new PatternSyntaxException(
545                     "Unrecognized version of Javadoc: '" + output + "'",
546                     pattern.pattern(),
547                     pattern.toString().length() - 1);
548         }
549 
550         return matcher.group(1);
551     }
552 
553     private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_0 = Pattern.compile("^\\s*(\\d+)\\s*?\\s*$");
554 
555     private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_1 =
556             Pattern.compile("^\\s*(\\d+)\\s*k(b)?\\s*$", Pattern.CASE_INSENSITIVE);
557 
558     private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_2 =
559             Pattern.compile("^\\s*(\\d+)\\s*m(b)?\\s*$", Pattern.CASE_INSENSITIVE);
560 
561     private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_3 =
562             Pattern.compile("^\\s*(\\d+)\\s*g(b)?\\s*$", Pattern.CASE_INSENSITIVE);
563 
564     private static final Pattern PARSE_JAVADOC_MEMORY_PATTERN_4 =
565             Pattern.compile("^\\s*(\\d+)\\s*t(b)?\\s*$", Pattern.CASE_INSENSITIVE);
566 
567     /**
568      * Parse a memory string which be used in the JVM arguments <code>-Xms</code> or <code>-Xmx</code>. <br>
569      * Here are some supported memory string depending the JDK used:
570      * <table><caption>Memory argument support per JDK</caption>
571      * <tr>
572      * <th>JDK</th>
573      * <th>Memory argument support for <code>-Xms</code> or <code>-Xmx</code></th>
574      * </tr>
575      * <tr>
576      * <td>SUN</td>
577      * <td>1024k | 128m | 1g | 1t</td>
578      * </tr>
579      * <tr>
580      * <td>IBM</td>
581      * <td>1024k | 1024b | 128m | 128mb | 1g | 1gb</td>
582      * </tr>
583      * <tr>
584      * <td>BEA</td>
585      * <td>1024k | 1024kb | 128m | 128mb | 1g | 1gb</td>
586      * </tr>
587      * </table>
588      *
589      * @param memory the memory to be parsed, not null.
590      * @return the memory parsed with a supported unit. If no unit specified in the <code>memory</code> parameter, the
591      *         default unit is <code>m</code>. The units <code>g | gb</code> or <code>t | tb</code> will be converted in
592      *         <code>m</code>.
593      * @throws IllegalArgumentException if the <code>memory</code> parameter is null or doesn't match any pattern.
594      */
595     protected static String parseJavadocMemory(String memory) throws IllegalArgumentException {
596         if (memory == null || memory.isEmpty()) {
597             throw new IllegalArgumentException("The memory could not be null.");
598         }
599 
600         Matcher m0 = PARSE_JAVADOC_MEMORY_PATTERN_0.matcher(memory);
601         if (m0.matches()) {
602             return m0.group(1) + "m";
603         }
604 
605         Matcher m1 = PARSE_JAVADOC_MEMORY_PATTERN_1.matcher(memory);
606         if (m1.matches()) {
607             return m1.group(1) + "k";
608         }
609 
610         Matcher m2 = PARSE_JAVADOC_MEMORY_PATTERN_2.matcher(memory);
611         if (m2.matches()) {
612             return m2.group(1) + "m";
613         }
614 
615         Matcher m3 = PARSE_JAVADOC_MEMORY_PATTERN_3.matcher(memory);
616         if (m3.matches()) {
617             return (Integer.parseInt(m3.group(1)) * 1024) + "m";
618         }
619 
620         Matcher m4 = PARSE_JAVADOC_MEMORY_PATTERN_4.matcher(memory);
621         if (m4.matches()) {
622             return (Integer.parseInt(m4.group(1)) * 1024 * 1024) + "m";
623         }
624 
625         throw new IllegalArgumentException("Could convert not to a memory size: " + memory);
626     }
627 
628     /**
629      * Validate if a charset is supported on this platform.
630      *
631      * @param charsetName the charsetName to be check.
632      * @return <code>true</code> if the given charset is supported by the JVM, <code>false</code> otherwise.
633      */
634     protected static boolean validateEncoding(String charsetName) {
635         if (charsetName == null || charsetName.isEmpty()) {
636             return false;
637         }
638 
639         try {
640             return Charset.isSupported(charsetName);
641         } catch (IllegalCharsetNameException e) {
642             return false;
643         }
644     }
645 
646     /**
647      * Auto-detect the class names of the implementation of <code>com.sun.tools.doclets.Taglet</code> class from a given
648      * jar file. <br>
649      * <b>Note</b>: <code>JAVA_HOME/lib/tools.jar</code> is a requirement to find
650      * <code>com.sun.tools.doclets.Taglet</code> class.
651      *
652      * @param jarFile not null
653      * @return the list of <code>com.sun.tools.doclets.Taglet</code> class names from a given jarFile.
654      * @throws IOException if jarFile is invalid or not found, or if the <code>JAVA_HOME/lib/tools.jar</code> is not
655      *             found.
656      * @throws ClassNotFoundException if any
657      * @throws NoClassDefFoundError if any
658      */
659     protected static List<String> getTagletClassNames(File jarFile)
660             throws IOException, ClassNotFoundException, NoClassDefFoundError {
661         List<String> classes = getClassNamesFromJar(jarFile);
662         URLClassLoader cl;
663 
664         // Needed to find com.sun.tools.doclets.Taglet class
665         File tools = new File(System.getProperty("java.home"), "../lib/tools.jar");
666         if (tools.exists() && tools.isFile()) {
667             cl = new URLClassLoader(
668                     new URL[] {jarFile.toURI().toURL(), tools.toURI().toURL()}, null);
669         } else {
670             cl = new URLClassLoader(new URL[] {jarFile.toURI().toURL()}, ClassLoader.getSystemClassLoader());
671         }
672 
673         List<String> tagletClasses = new ArrayList<>();
674 
675         Class<?> tagletClass;
676 
677         try {
678             tagletClass = cl.loadClass("com.sun.tools.doclets.Taglet");
679         } catch (ClassNotFoundException e) {
680             tagletClass = cl.loadClass("jdk.javadoc.doclet.Taglet");
681         }
682 
683         for (String s : classes) {
684             Class<?> c = cl.loadClass(s);
685 
686             if (tagletClass.isAssignableFrom(c) && !Modifier.isAbstract(c.getModifiers())) {
687                 tagletClasses.add(c.getName());
688             }
689         }
690 
691         try {
692             cl.close();
693         } catch (IOException ex) {
694             // no big deal
695         }
696 
697         return tagletClasses;
698     }
699 
700     /**
701      * Copy the given url to the given file.
702      *
703      * @param url not null url
704      * @param file not null file where the url will be created
705      * @throws IOException if any
706      * @since 2.6
707      */
708     protected static void copyResource(URL url, File file) throws IOException {
709         if (file == null) {
710             throw new NullPointerException("The file can't be null.");
711         }
712         if (url == null) {
713             throw new NullPointerException("The url could not be null.");
714         }
715 
716         FileUtils.copyURLToFile(url, file);
717     }
718 
719     /**
720      * Invoke Maven for the given project file with a list of goals and properties, the output will be in the invokerlog
721      * file. <br>
722      * <b>Note</b>: the Maven Home should be defined in the <code>maven.home</code> Java system property or defined in
723      * <code>M2_HOME</code> system env variables.
724      *
725      * @param log a logger could be null.
726      * @param localRepositoryDir the localRepository not null.
727      * @param projectFile a not null project file.
728      * @param goals a not null goals list.
729      * @param properties the properties for the goals, could be null.
730      * @param invokerLog the log file where the invoker will be written, if null using <code>System.out</code>.
731      * @param globalSettingsFile reference to settings file, could be null.
732      * @param userSettingsFile reference to user settings file, could be null.
733      * @param globalToolchainsFile reference to toolchains file, could be null.
734      * @param userToolchainsFile reference to user toolchains file, could be null.
735      * @throws MavenInvocationException if any
736      * @since 2.6
737      */
738     protected static void invokeMaven(
739             Log log,
740             File localRepositoryDir,
741             File projectFile,
742             List<String> goals,
743             Properties properties,
744             File invokerLog,
745             File globalSettingsFile,
746             File userSettingsFile,
747             File globalToolchainsFile,
748             File userToolchainsFile)
749             throws MavenInvocationException {
750         if (projectFile == null) {
751             throw new IllegalArgumentException("projectFile should be not null.");
752         }
753         if (!projectFile.isFile()) {
754             throw new IllegalArgumentException(projectFile.getAbsolutePath() + " is not a file.");
755         }
756         if (goals == null || goals.size() == 0) {
757             throw new IllegalArgumentException("goals should be not empty.");
758         }
759         if (localRepositoryDir == null || !localRepositoryDir.isDirectory()) {
760             throw new IllegalArgumentException(
761                     "localRepositoryDir '" + localRepositoryDir + "' should be a directory.");
762         }
763 
764         String mavenHome = getMavenHome(log);
765         if (mavenHome == null || mavenHome.isEmpty()) {
766             String msg = "Could NOT invoke Maven because no Maven Home is defined. You need to have set the M2_HOME "
767                     + "system env variable or a maven.home Java system properties.";
768             if (log != null) {
769                 log.error(msg);
770             } else {
771                 System.err.println(msg);
772             }
773             return;
774         }
775 
776         Invoker invoker = new DefaultInvoker();
777         invoker.setMavenHome(new File(mavenHome));
778         invoker.setLocalRepositoryDirectory(localRepositoryDir);
779 
780         InvocationRequest request = new DefaultInvocationRequest();
781         request.setBaseDirectory(projectFile.getParentFile());
782         request.setPomFile(projectFile);
783         if (globalSettingsFile != null && globalSettingsFile.isFile()) {
784             request.setGlobalSettingsFile(globalSettingsFile);
785         }
786         if (userSettingsFile != null && userSettingsFile.isFile()) {
787             request.setUserSettingsFile(userSettingsFile);
788         }
789         if (globalToolchainsFile != null && globalToolchainsFile.isFile()) {
790             request.setGlobalToolchainsFile(globalToolchainsFile);
791         }
792         if (userToolchainsFile != null && userToolchainsFile.isFile()) {
793             request.setToolchainsFile(userToolchainsFile);
794         }
795         request.setBatchMode(true);
796         if (log != null) {
797             request.setDebug(log.isDebugEnabled());
798         } else {
799             request.setDebug(true);
800         }
801         request.setGoals(goals);
802         if (properties != null) {
803             request.setProperties(properties);
804         }
805         File javaHome = getJavaHome(log);
806         if (javaHome != null) {
807             request.setJavaHome(javaHome);
808         }
809 
810         if (log != null && log.isDebugEnabled()) {
811             log.debug("Invoking Maven for the goals: " + goals + " with "
812                     + (properties == null ? "no properties" : "properties=" + properties));
813         }
814         InvocationResult result = invoke(log, invoker, request, invokerLog, goals, properties, null);
815 
816         if (result.getExitCode() != 0) {
817             String invokerLogContent = readFile(invokerLog, "UTF-8");
818 
819             // see DefaultMaven
820             if (invokerLogContent != null
821                     && (!invokerLogContent.contains("Scanning for projects...")
822                             || invokerLogContent.contains(OutOfMemoryError.class.getName()))) {
823                 if (log != null) {
824                     log.error("Error occurred during initialization of VM, trying to use an empty MAVEN_OPTS...");
825 
826                     if (log.isDebugEnabled()) {
827                         log.debug("Reinvoking Maven for the goals: " + goals + " with an empty MAVEN_OPTS...");
828                     }
829                 }
830                 result = invoke(log, invoker, request, invokerLog, goals, properties, "");
831             }
832         }
833 
834         if (result.getExitCode() != 0) {
835             String invokerLogContent = readFile(invokerLog, "UTF-8");
836 
837             // see DefaultMaven
838             if (invokerLogContent != null
839                     && (!invokerLogContent.contains("Scanning for projects...")
840                             || invokerLogContent.contains(OutOfMemoryError.class.getName()))) {
841                 throw new MavenInvocationException(ERROR_INIT_VM);
842             }
843 
844             throw new MavenInvocationException(
845                     "Error when invoking Maven, consult the invoker log file: " + invokerLog.getAbsolutePath());
846         }
847     }
848 
849     /**
850      * Read the given file and return the content or null if an IOException occurs.
851      *
852      * @param javaFile not null
853      * @param encoding could be null
854      * @return the content with unified line separator of the given javaFile using the given encoding.
855      * @see FileUtils#fileRead(File, String)
856      * @since 2.6.1
857      */
858     protected static String readFile(final File javaFile, final String encoding) {
859         try {
860             return FileUtils.fileRead(javaFile, encoding);
861         } catch (IOException e) {
862             return null;
863         }
864     }
865 
866     /**
867      * Split the given path with colon and semi-colon, to support Solaris and Windows path. Examples:
868      *
869      * <pre>
870      * splitPath( "/home:/tmp" )     = ["/home", "/tmp"]
871      * splitPath( "/home;/tmp" )     = ["/home", "/tmp"]
872      * splitPath( "C:/home:C:/tmp" ) = ["C:/home", "C:/tmp"]
873      * splitPath( "C:/home;C:/tmp" ) = ["C:/home", "C:/tmp"]
874      * </pre>
875      *
876      * @param path which can contain multiple paths separated with a colon (<code>:</code>) or a semi-colon
877      *            (<code>;</code>), platform independent. Could be null.
878      * @return the path splitted by colon or semi-colon or <code>null</code> if path was <code>null</code>.
879      * @since 2.6.1
880      */
881     protected static String[] splitPath(final String path) {
882         if (path == null) {
883             return null;
884         }
885 
886         List<String> subpaths = new ArrayList<>();
887         PathTokenizer pathTokenizer = new PathTokenizer(path);
888         while (pathTokenizer.hasMoreTokens()) {
889             subpaths.add(pathTokenizer.nextToken());
890         }
891 
892         return subpaths.toArray(new String[subpaths.size()]);
893     }
894 
895     /**
896      * Unify the given path with the current System path separator, to be platform independent. Examples:
897      *
898      * <pre>
899      * unifyPathSeparator( "/home:/tmp" ) = "/home:/tmp" (Solaris box)
900      * unifyPathSeparator( "/home:/tmp" ) = "/home;/tmp" (Windows box)
901      * </pre>
902      *
903      * @param path which can contain multiple paths by separating them with a colon (<code>:</code>) or a semi-colon
904      *            (<code>;</code>), platform independent. Could be null.
905      * @return the same path but separated with the current System path separator or <code>null</code> if path was
906      *         <code>null</code>.
907      * @since 2.6.1
908      * @see #splitPath(String)
909      * @see File#pathSeparator
910      */
911     protected static String unifyPathSeparator(final String path) {
912         if (path == null) {
913             return null;
914         }
915 
916         return String.join(File.pathSeparator, splitPath(path));
917     }
918 
919     // ----------------------------------------------------------------------
920     // private methods
921     // ----------------------------------------------------------------------
922 
923     /**
924      * @param jarFile not null
925      * @return all class names from the given jar file.
926      * @throws IOException if any or if the jarFile is null or doesn't exist.
927      */
928     private static List<String> getClassNamesFromJar(File jarFile) throws IOException {
929         if (jarFile == null || !jarFile.exists() || !jarFile.isFile()) {
930             throw new IOException("The jar '" + jarFile + "' doesn't exist or is not a file.");
931         }
932 
933         List<String> classes = new ArrayList<>();
934         Pattern pattern = Pattern.compile("(?i)^(META-INF/versions/(?<v>[0-9]+)/)?(?<n>.+)[.]class$");
935         try (JarInputStream jarStream = new JarInputStream(new FileInputStream(jarFile))) {
936             for (JarEntry jarEntry = jarStream.getNextJarEntry();
937                     jarEntry != null;
938                     jarEntry = jarStream.getNextJarEntry()) {
939                 Matcher matcher = pattern.matcher(jarEntry.getName());
940                 if (matcher.matches()) {
941                     String version = matcher.group("v");
942                     if ((version == null || version.isEmpty()) || JavaVersion.JAVA_VERSION.isAtLeast(version)) {
943                         String name = matcher.group("n");
944 
945                         classes.add(name.replaceAll("/", "\\."));
946                     }
947                 }
948 
949                 jarStream.closeEntry();
950             }
951         }
952 
953         return classes;
954     }
955 
956     /**
957      * @param log could be null
958      * @param invoker not null
959      * @param request not null
960      * @param invokerLog not null
961      * @param goals not null
962      * @param properties could be null
963      * @param mavenOpts could be null
964      * @return the invocation result
965      * @throws MavenInvocationException if any
966      * @since 2.6
967      */
968     private static InvocationResult invoke(
969             Log log,
970             Invoker invoker,
971             InvocationRequest request,
972             File invokerLog,
973             List<String> goals,
974             Properties properties,
975             String mavenOpts)
976             throws MavenInvocationException {
977         PrintStream ps;
978         OutputStream os = null;
979         if (invokerLog != null) {
980             if (log != null && log.isDebugEnabled()) {
981                 log.debug("Using " + invokerLog.getAbsolutePath() + " to log the invoker");
982             }
983 
984             try {
985                 if (!invokerLog.exists()) {
986                     // noinspection ResultOfMethodCallIgnored
987                     invokerLog.getParentFile().mkdirs();
988                 }
989                 os = new FileOutputStream(invokerLog);
990                 ps = new PrintStream(os, true, "UTF-8");
991             } catch (FileNotFoundException e) {
992                 if (log != null && log.isErrorEnabled()) {
993                     log.error("FileNotFoundException: " + e.getMessage() + ". Using System.out to log the invoker.");
994                 }
995                 ps = System.out;
996             } catch (UnsupportedEncodingException e) {
997                 if (log != null && log.isErrorEnabled()) {
998                     log.error("UnsupportedEncodingException: " + e.getMessage()
999                             + ". Using System.out to log the invoker.");
1000                 }
1001                 ps = System.out;
1002             }
1003         } else {
1004             if (log != null && log.isDebugEnabled()) {
1005                 log.debug("Using System.out to log the invoker.");
1006             }
1007 
1008             ps = System.out;
1009         }
1010 
1011         if (mavenOpts != null) {
1012             request.setMavenOpts(mavenOpts);
1013         }
1014 
1015         InvocationOutputHandler outputHandler = new PrintStreamHandler(ps, false);
1016         request.setOutputHandler(outputHandler);
1017 
1018         try (OutputStream closeMe = os) {
1019             outputHandler.consumeLine("Invoking Maven for the goals: " + goals + " with "
1020                     + (properties == null ? "no properties" : "properties=" + properties));
1021             outputHandler.consumeLine("");
1022             outputHandler.consumeLine("M2_HOME=" + getMavenHome(log));
1023             outputHandler.consumeLine("MAVEN_OPTS=" + getMavenOpts(log));
1024             outputHandler.consumeLine("JAVA_HOME=" + getJavaHome(log));
1025             outputHandler.consumeLine("JAVA_OPTS=" + getJavaOpts(log));
1026             outputHandler.consumeLine("");
1027             return invoker.execute(request);
1028         } catch (IOException ioe) {
1029             throw new MavenInvocationException("IOException while consuming invocation output", ioe);
1030         }
1031     }
1032 
1033     /**
1034      * @param log a logger could be null
1035      * @return the Maven home defined in the <code>maven.home</code> system property
1036      *         or null if never set.
1037      * @since 2.6
1038      */
1039     private static String getMavenHome(Log log) {
1040         String mavenHome = System.getProperty("maven.home");
1041 
1042         File m2Home = new File(mavenHome);
1043         if (!m2Home.exists()) {
1044             if (log != null && log.isErrorEnabled()) {
1045                 log.error("Cannot find Maven application directory. Either specify 'maven.home' system property.");
1046             }
1047         }
1048 
1049         return mavenHome;
1050     }
1051 
1052     /**
1053      * @param log a logger could be null
1054      * @return the <code>MAVEN_OPTS</code> env variable value
1055      * @since 2.6
1056      */
1057     private static String getMavenOpts(Log log) {
1058         return CommandLineUtils.getSystemEnvVars().getProperty("MAVEN_OPTS");
1059     }
1060 
1061     /**
1062      * @param log a logger could be null
1063      * @return the <code>JAVA_HOME</code> from System.getProperty( "java.home" ) By default,
1064      *         <code>System.getProperty( "java.home" ) = JRE_HOME</code> and <code>JRE_HOME</code> should be in the
1065      *         <code>JDK_HOME</code>
1066      * @since 2.6
1067      */
1068     private static File getJavaHome(Log log) {
1069         File javaHome = null;
1070 
1071         String javaHomeValue = CommandLineUtils.getSystemEnvVars().getProperty("JAVA_HOME");
1072 
1073         // if maven.home is set, we can assume JAVA_HOME must be used for testing
1074         if (System.getProperty("maven.home") == null || javaHomeValue == null) {
1075             // JEP220 (Java9) restructured the JRE/JDK runtime image
1076             if (SystemUtils.IS_OS_MAC_OSX || JavaVersion.JAVA_VERSION.isAtLeast("9")) {
1077                 javaHome = SystemUtils.getJavaHome();
1078             } else {
1079                 javaHome = new File(SystemUtils.getJavaHome(), "..");
1080             }
1081         }
1082 
1083         if (javaHome == null || !javaHome.exists()) {
1084             javaHome = new File(javaHomeValue);
1085         }
1086 
1087         if (javaHome == null || !javaHome.exists()) {
1088             if (log != null && log.isErrorEnabled()) {
1089                 log.error("Cannot find Java application directory. Either specify 'java.home' system property, or "
1090                         + "JAVA_HOME environment variable.");
1091             }
1092         }
1093 
1094         return javaHome;
1095     }
1096 
1097     /**
1098      * @param log a logger could be null
1099      * @return the <code>JAVA_OPTS</code> env variable value
1100      * @since 2.6
1101      */
1102     private static String getJavaOpts(Log log) {
1103         return CommandLineUtils.getSystemEnvVars().getProperty("JAVA_OPTS");
1104     }
1105 
1106     /**
1107      * A Path tokenizer takes a path and returns the components that make up that path. The path can use path separators
1108      * of either ':' or ';' and file separators of either '/' or '\'.
1109      *
1110      * @version revision 439418 taken on 2009-09-12 from Ant Project (see
1111      *          http://svn.apache.org/repos/asf/ant/core/trunk/src/main/org/apache/tools/ant/PathTokenizer.java)
1112      */
1113     private static class PathTokenizer {
1114         /**
1115          * A tokenizer to break the string up based on the ':' or ';' separators.
1116          */
1117         private StringTokenizer tokenizer;
1118 
1119         /**
1120          * A String which stores any path components which have been read ahead due to DOS filesystem compensation.
1121          */
1122         private String lookahead = null;
1123 
1124         /**
1125          * Flag to indicate whether or not we are running on a platform with a DOS style filesystem
1126          */
1127         private boolean dosStyleFilesystem;
1128 
1129         /**
1130          * Constructs a path tokenizer for the specified path.
1131          *
1132          * @param path The path to tokenize. Must not be <code>null</code>.
1133          */
1134         PathTokenizer(String path) {
1135             tokenizer = new StringTokenizer(path, ":;", false);
1136             dosStyleFilesystem = File.pathSeparatorChar == ';';
1137         }
1138 
1139         /**
1140          * Tests if there are more path elements available from this tokenizer's path. If this method returns
1141          * <code>true</code>, then a subsequent call to nextToken will successfully return a token.
1142          *
1143          * @return <code>true</code> if and only if there is at least one token in the string after the current
1144          *         position; <code>false</code> otherwise.
1145          */
1146         public boolean hasMoreTokens() {
1147             return lookahead != null || tokenizer.hasMoreTokens();
1148         }
1149 
1150         /**
1151          * Returns the next path element from this tokenizer.
1152          *
1153          * @return the next path element from this tokenizer.
1154          * @exception NoSuchElementException if there are no more elements in this tokenizer's path.
1155          */
1156         public String nextToken() throws NoSuchElementException {
1157             String token;
1158             if (lookahead != null) {
1159                 token = lookahead;
1160                 lookahead = null;
1161             } else {
1162                 token = tokenizer.nextToken().trim();
1163             }
1164 
1165             if (token.length() == 1
1166                     && Character.isLetter(token.charAt(0))
1167                     && dosStyleFilesystem
1168                     && tokenizer.hasMoreTokens()) {
1169                 // we are on a dos style system so this path could be a drive
1170                 // spec. We look at the next token
1171                 String nextToken = tokenizer.nextToken().trim();
1172                 if (nextToken.startsWith("\\") || nextToken.startsWith("/")) {
1173                     // we know we are on a DOS style platform and the next path
1174                     // starts with a slash or backslash, so we know this is a
1175                     // drive spec
1176                     token += ":" + nextToken;
1177                 } else {
1178                     // store the token just read for next time
1179                     lookahead = nextToken;
1180                 }
1181             }
1182             return token;
1183         }
1184     }
1185 
1186     /**
1187      * Ignores line like 'Picked up JAVA_TOOL_OPTIONS: ...' as can happen on CI servers.
1188      *
1189      * @author Robert Scholte
1190      * @since 3.0.1
1191      */
1192     protected static class JavadocOutputStreamConsumer extends CommandLineUtils.StringStreamConsumer {
1193         @Override
1194         public void consumeLine(String line) {
1195             if (!line.startsWith("Picked up ")) {
1196                 super.consumeLine(line);
1197             }
1198         }
1199     }
1200 
1201     static List<String> toList(String src) {
1202         return toList(src, null, null);
1203     }
1204 
1205     static List<String> toList(String src, String elementPrefix, String elementSuffix) {
1206         if (src == null || src.isEmpty()) {
1207             return null;
1208         }
1209 
1210         List<String> result = new ArrayList<>();
1211 
1212         StringTokenizer st = new StringTokenizer(src, "[,:;]");
1213         StringBuilder sb = new StringBuilder(256);
1214         while (st.hasMoreTokens()) {
1215             sb.setLength(0);
1216             if (elementPrefix != null && !elementPrefix.isEmpty()) {
1217                 sb.append(elementPrefix);
1218             }
1219 
1220             sb.append(st.nextToken());
1221 
1222             if (elementSuffix != null && !elementSuffix.isEmpty()) {
1223                 sb.append(elementSuffix);
1224             }
1225 
1226             result.add(sb.toString());
1227         }
1228 
1229         return result;
1230     }
1231 
1232     static <T> List<T> toList(T[] multiple) {
1233         return toList(null, multiple);
1234     }
1235 
1236     static <T> List<T> toList(T single, T[] multiple) {
1237         if (single == null && (multiple == null || multiple.length < 1)) {
1238             return null;
1239         }
1240 
1241         List<T> result = new ArrayList<>();
1242         if (single != null) {
1243             result.add(single);
1244         }
1245 
1246         if (multiple != null && multiple.length > 0) {
1247             result.addAll(Arrays.asList(multiple));
1248         }
1249 
1250         return result;
1251     }
1252 
1253     // TODO: deprecate in favor of Path.relativize
1254     public static String toRelative(File basedir, String absolutePath) {
1255         String relative;
1256 
1257         absolutePath = absolutePath.replace('\\', '/');
1258         String basedirPath = basedir.getAbsolutePath().replace('\\', '/');
1259 
1260         if (absolutePath.startsWith(basedirPath)) {
1261             relative = absolutePath.substring(basedirPath.length());
1262             if (relative.startsWith("/")) {
1263                 relative = relative.substring(1);
1264             }
1265             if (relative.length() <= 0) {
1266                 relative = ".";
1267             }
1268         } else {
1269             relative = absolutePath;
1270         }
1271 
1272         return relative;
1273     }
1274 
1275     /**
1276      * Convenience method to determine that a collection is not empty or null.
1277      * @param collection the collection to verify
1278      * @return {@code true} if not {@code null} and not empty, otherwise {@code false}
1279      */
1280     public static boolean isNotEmpty(final Collection<?> collection) {
1281         return collection != null && !collection.isEmpty();
1282     }
1283 
1284     /**
1285      * Convenience method to determine that a collection is empty or null.
1286      * @param collection the collection to verify
1287      * @return {@code true} if {@code null} or empty, otherwise {@code false}
1288      */
1289     public static boolean isEmpty(final Collection<?> collection) {
1290         return collection == null || collection.isEmpty();
1291     }
1292 
1293     /**
1294      * Execute an HTTP request to the given URL, follow redirects, and return the last redirect location. For URLs
1295      * that aren't http/https, this does nothing and simply returns the given URL unchanged.
1296      *
1297      * @param url URL
1298      * @param settings Maven settings
1299      * @return final URL after all redirects have been followed
1300      * @throws IOException if there was an error during the HTTP request
1301      */
1302     protected static URL getRedirectUrl(URL url, Settings settings) throws IOException {
1303         String protocol = url.getProtocol();
1304         if (!"http".equals(protocol) && !"https".equals(protocol)) {
1305             return url;
1306         }
1307 
1308         try (CloseableHttpClient httpClient = createHttpClient(settings, url)) {
1309             HttpClientContext httpContext = HttpClientContext.create();
1310             HttpGet httpMethod = new HttpGet(url.toString());
1311             HttpResponse response = httpClient.execute(httpMethod, httpContext);
1312             int status = response.getStatusLine().getStatusCode();
1313             if (status != HttpStatus.SC_OK) {
1314                 throw new FileNotFoundException(
1315                         "Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + ".");
1316             }
1317 
1318             List<URI> redirects = httpContext.getRedirectLocations();
1319 
1320             if (isEmpty(redirects)) {
1321                 return url;
1322             } else {
1323                 URI last = redirects.get(redirects.size() - 1);
1324 
1325                 // URI must refer to directory, so prevent redirects to index.html
1326                 // see https://issues.apache.org/jira/browse/MJAVADOC-539
1327                 String truncate = "index.html";
1328                 if (last.getPath().endsWith("/" + truncate)) {
1329                     try {
1330                         String fixedPath =
1331                                 last.getPath().substring(0, last.getPath().length() - truncate.length());
1332                         last = new URI(
1333                                 last.getScheme(), last.getAuthority(), fixedPath, last.getQuery(), last.getFragment());
1334                     } catch (URISyntaxException ex) {
1335                         // not supposed to happen, but when it does just keep the last URI
1336                     }
1337                 }
1338                 return last.toURL();
1339             }
1340         }
1341     }
1342 
1343     /**
1344      * Validates an <code>URL</code> to point to a valid <code>package-list</code> resource.
1345      *
1346      * @param url The URL to validate.
1347      * @param settings The user settings used to configure the connection to the URL or {@code null}.
1348      * @param validateContent <code>true</code> to validate the content of the <code>package-list</code> resource;
1349      *            <code>false</code> to only check the existence of the <code>package-list</code> resource.
1350      * @return <code>true</code> if <code>url</code> points to a valid <code>package-list</code> resource;
1351      *         <code>false</code> else.
1352      * @throws IOException if reading the resource fails.
1353      * @see #createHttpClient(org.apache.maven.settings.Settings, java.net.URL)
1354      * @since 2.8
1355      */
1356     protected static boolean isValidPackageList(URL url, Settings settings, boolean validateContent)
1357             throws IOException {
1358         if (url == null) {
1359             throw new IllegalArgumentException("The url is null");
1360         }
1361 
1362         try (BufferedReader reader = getReader(url, settings)) {
1363             if (validateContent) {
1364                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
1365                     if (!isValidPackageName(line)) {
1366                         return false;
1367                     }
1368                 }
1369             }
1370             return true;
1371         }
1372     }
1373 
1374     protected static boolean isValidElementList(URL url, Settings settings, boolean validateContent)
1375             throws IOException {
1376         if (url == null) {
1377             throw new IllegalArgumentException("The url is null");
1378         }
1379 
1380         try (BufferedReader reader = getReader(url, settings)) {
1381             if (validateContent) {
1382                 for (String line = reader.readLine(); line != null; line = reader.readLine()) {
1383                     if (line.startsWith("module:")) {
1384                         continue;
1385                     }
1386 
1387                     if (!isValidPackageName(line)) {
1388                         return false;
1389                     }
1390                 }
1391             }
1392             return true;
1393         }
1394     }
1395 
1396     private static BufferedReader getReader(URL url, Settings settings) throws IOException {
1397         BufferedReader reader = null;
1398 
1399         if ("file".equals(url.getProtocol())) {
1400             // Intentionally using the platform default encoding here since this is what Javadoc uses internally.
1401             reader = new BufferedReader(new InputStreamReader(url.openStream()));
1402         } else {
1403             // http, https...
1404             final CloseableHttpClient httpClient = createHttpClient(settings, url);
1405 
1406             final HttpGet httpMethod = new HttpGet(url.toString());
1407 
1408             HttpResponse response;
1409             HttpClientContext httpContext = HttpClientContext.create();
1410             try {
1411                 response = httpClient.execute(httpMethod, httpContext);
1412             } catch (SocketTimeoutException e) {
1413                 // could be a sporadic failure, one more retry before we give up
1414                 response = httpClient.execute(httpMethod, httpContext);
1415             }
1416 
1417             int status = response.getStatusLine().getStatusCode();
1418             if (status != HttpStatus.SC_OK) {
1419                 throw new FileNotFoundException(
1420                         "Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + ".");
1421             } else {
1422                 int pos = url.getPath().lastIndexOf('/');
1423                 List<URI> redirects = httpContext.getRedirectLocations();
1424                 if (pos >= 0 && isNotEmpty(redirects)) {
1425                     URI location = redirects.get(redirects.size() - 1);
1426                     String suffix = url.getPath().substring(pos);
1427                     // Redirections shall point to the same file, e.g. /package-list
1428                     if (!location.getPath().endsWith(suffix)) {
1429                         throw new FileNotFoundException(url.toExternalForm() + " redirects to "
1430                                 + location.toURL().toExternalForm() + ".");
1431                     }
1432                 }
1433             }
1434 
1435             // Intentionally using the platform default encoding here since this is what Javadoc uses internally.
1436             reader = new BufferedReader(
1437                     new InputStreamReader(response.getEntity().getContent())) {
1438                 @Override
1439                 public void close() throws IOException {
1440                     super.close();
1441 
1442                     if (httpMethod != null) {
1443                         httpMethod.releaseConnection();
1444                     }
1445                     if (httpClient != null) {
1446                         httpClient.close();
1447                     }
1448                 }
1449             };
1450         }
1451 
1452         return reader;
1453     }
1454 
1455     private static boolean isValidPackageName(String str) {
1456         if (str == null || str.isEmpty()) {
1457             // unnamed package is valid (even if bad practice :) )
1458             return true;
1459         }
1460 
1461         int idx;
1462         while ((idx = str.indexOf('.')) != -1) {
1463             if (!isValidClassName(str.substring(0, idx))) {
1464                 return false;
1465             }
1466 
1467             str = str.substring(idx + 1);
1468         }
1469 
1470         return isValidClassName(str);
1471     }
1472 
1473     private static boolean isValidClassName(String str) {
1474         if ((str == null || str.isEmpty()) || !Character.isJavaIdentifierStart(str.charAt(0))) {
1475             return false;
1476         }
1477 
1478         for (int i = str.length() - 1; i > 0; i--) {
1479             if (!Character.isJavaIdentifierPart(str.charAt(i))) {
1480                 return false;
1481             }
1482         }
1483 
1484         return true;
1485     }
1486 
1487     /**
1488      * Creates a new {@code HttpClient} instance.
1489      *
1490      * @param settings The settings to use for setting up the client or {@code null}.
1491      * @param url The {@code URL} to use for setting up the client or {@code null}.
1492      * @return A new {@code HttpClient} instance.
1493      * @see #DEFAULT_TIMEOUT
1494      * @since 2.8
1495      */
1496     private static CloseableHttpClient createHttpClient(Settings settings, URL url) {
1497         HttpClientBuilder builder = HttpClients.custom();
1498 
1499         Registry<ConnectionSocketFactory> csfRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
1500                 .register("http", PlainConnectionSocketFactory.getSocketFactory())
1501                 .register("https", SSLConnectionSocketFactory.getSystemSocketFactory())
1502                 .build();
1503 
1504         builder.setConnectionManager(new PoolingHttpClientConnectionManager(csfRegistry));
1505         builder.setDefaultRequestConfig(RequestConfig.custom()
1506                 .setSocketTimeout(DEFAULT_TIMEOUT)
1507                 .setConnectTimeout(DEFAULT_TIMEOUT)
1508                 .setCircularRedirectsAllowed(true)
1509                 .setCookieSpec(CookieSpecs.IGNORE_COOKIES)
1510                 .build());
1511 
1512         // Some web servers don't allow the default user-agent sent by httpClient
1513         builder.setUserAgent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)");
1514 
1515         // Some server reject requests that do not have an Accept header
1516         builder.setDefaultHeaders(Arrays.asList(new BasicHeader(HttpHeaders.ACCEPT, "*/*")));
1517 
1518         if (settings != null && settings.getActiveProxy() != null) {
1519             Proxy activeProxy = settings.getActiveProxy();
1520 
1521             ProxyInfo proxyInfo = new ProxyInfo();
1522             proxyInfo.setNonProxyHosts(activeProxy.getNonProxyHosts());
1523 
1524             String activeProxyHost = activeProxy.getHost();
1525             if (activeProxyHost != null
1526                     && !activeProxyHost.isEmpty()
1527                     && (url == null || !ProxyUtils.validateNonProxyHosts(proxyInfo, url.getHost()))) {
1528                 HttpHost proxy = new HttpHost(activeProxy.getHost(), activeProxy.getPort());
1529                 builder.setProxy(proxy);
1530 
1531                 String activeProxyUsername = activeProxy.getUsername();
1532                 if (activeProxyUsername != null
1533                         && !activeProxyUsername.isEmpty()
1534                         && activeProxy.getPassword() != null) {
1535                     Credentials credentials =
1536                             new UsernamePasswordCredentials(activeProxyUsername, activeProxy.getPassword());
1537 
1538                     CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
1539                     credentialsProvider.setCredentials(AuthScope.ANY, credentials);
1540                     builder.setDefaultCredentialsProvider(credentialsProvider);
1541                 }
1542             }
1543         }
1544         return builder.build();
1545     }
1546 
1547     static boolean equalsIgnoreCase(String value, String... strings) {
1548         for (String s : strings) {
1549             if (s.equalsIgnoreCase(value)) {
1550                 return true;
1551             }
1552         }
1553         return false;
1554     }
1555 
1556     static boolean equals(String value, String... strings) {
1557         for (String s : strings) {
1558             if (s.equals(value)) {
1559                 return true;
1560             }
1561         }
1562         return false;
1563     }
1564 }