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