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