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