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