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