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