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