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