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