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