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