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