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