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