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      * @throws MavenInvocationException if any
785      * @since 2.6
786      */
787     protected static void invokeMaven( Log log, File localRepositoryDir, File projectFile, List<String> goals,
788                                        Properties properties, File invokerLog )
789         throws MavenInvocationException
790     {
791         if ( projectFile == null )
792         {
793             throw new IllegalArgumentException( "projectFile should be not null." );
794         }
795         if ( !projectFile.isFile() )
796         {
797             throw new IllegalArgumentException( projectFile.getAbsolutePath() + " is not a file." );
798         }
799         if ( goals == null || goals.size() == 0 )
800         {
801             throw new IllegalArgumentException( "goals should be not empty." );
802         }
803         if ( localRepositoryDir == null || !localRepositoryDir.isDirectory() )
804         {
805             throw new IllegalArgumentException( "localRepositoryDir '" + localRepositoryDir
806                 + "' should be a directory." );
807         }
808 
809         String mavenHome = getMavenHome( log );
810         if ( StringUtils.isEmpty( mavenHome ) )
811         {
812             String msg = "Could NOT invoke Maven because no Maven Home is defined. You need to have set the M2_HOME "
813                 + "system env variable or a maven.home Java system properties.";
814             if ( log != null )
815             {
816                 log.error( msg );
817             }
818             else
819             {
820                 System.err.println( msg );
821             }
822             return;
823         }
824 
825         Invoker invoker = new DefaultInvoker();
826         invoker.setMavenHome( new File( mavenHome ) );
827         invoker.setLocalRepositoryDirectory( localRepositoryDir );
828 
829         InvocationRequest request = new DefaultInvocationRequest();
830         request.setBaseDirectory( projectFile.getParentFile() );
831         request.setPomFile( projectFile );
832         request.setBatchMode( true );
833         if ( log != null )
834         {
835             request.setDebug( log.isDebugEnabled() );
836         }
837         else
838         {
839             request.setDebug( true );
840         }
841         request.setGoals( goals );
842         if ( properties != null )
843         {
844             request.setProperties( properties );
845         }
846         File javaHome = getJavaHome( log );
847         if ( javaHome != null )
848         {
849             request.setJavaHome( javaHome );
850         }
851 
852         if ( log != null && log.isDebugEnabled() )
853         {
854             log.debug( "Invoking Maven for the goals: " + goals + " with "
855                 + ( properties == null ? "no properties" : "properties=" + properties ) );
856         }
857         InvocationResult result = invoke( log, invoker, request, invokerLog, goals, properties, null );
858 
859         if ( result.getExitCode() != 0 )
860         {
861             String invokerLogContent = readFile( invokerLog, "UTF-8" );
862 
863             // see DefaultMaven
864             if ( invokerLogContent != null && ( !invokerLogContent.contains( "Scanning for projects..." )
865                 || invokerLogContent.contains( OutOfMemoryError.class.getName() ) ) )
866             {
867                 if ( log != null )
868                 {
869                     log.error( "Error occurred during initialization of VM, trying to use an empty MAVEN_OPTS..." );
870 
871                     if ( log.isDebugEnabled() )
872                     {
873                         log.debug( "Reinvoking Maven for the goals: " + goals + " with an empty MAVEN_OPTS..." );
874                     }
875                 }
876                 result = invoke( log, invoker, request, invokerLog, goals, properties, "" );
877             }
878         }
879 
880         if ( result.getExitCode() != 0 )
881         {
882             String invokerLogContent = readFile( invokerLog, "UTF-8" );
883 
884             // see DefaultMaven
885             if ( invokerLogContent != null && ( !invokerLogContent.contains( "Scanning for projects..." )
886                 || invokerLogContent.contains( OutOfMemoryError.class.getName() ) ) )
887             {
888                 throw new MavenInvocationException( ERROR_INIT_VM );
889             }
890 
891             throw new MavenInvocationException( "Error when invoking Maven, consult the invoker log file: "
892                 + invokerLog.getAbsolutePath() );
893         }
894     }
895 
896     /**
897      * Read the given file and return the content or null if an IOException occurs.
898      *
899      * @param javaFile not null
900      * @param encoding could be null
901      * @return the content with unified line separator of the given javaFile using the given encoding.
902      * @see FileUtils#fileRead(File, String)
903      * @since 2.6.1
904      */
905     protected static String readFile( final File javaFile, final String encoding )
906     {
907         try
908         {
909             return FileUtils.fileRead( javaFile, encoding );
910         }
911         catch ( IOException e )
912         {
913             return null;
914         }
915     }
916 
917     /**
918      * Split the given path with colon and semi-colon, to support Solaris and Windows path. Examples:
919      *
920      * <pre>
921      * splitPath( "/home:/tmp" )     = ["/home", "/tmp"]
922      * splitPath( "/home;/tmp" )     = ["/home", "/tmp"]
923      * splitPath( "C:/home:C:/tmp" ) = ["C:/home", "C:/tmp"]
924      * splitPath( "C:/home;C:/tmp" ) = ["C:/home", "C:/tmp"]
925      * </pre>
926      *
927      * @param path which can contain multiple paths separated with a colon (<code>:</code>) or a semi-colon
928      *            (<code>;</code>), platform independent. Could be null.
929      * @return the path splitted by colon or semi-colon or <code>null</code> if path was <code>null</code>.
930      * @since 2.6.1
931      */
932     protected static String[] splitPath( final String path )
933     {
934         if ( path == null )
935         {
936             return null;
937         }
938 
939         List<String> subpaths = new ArrayList<>();
940         PathTokenizer pathTokenizer = new PathTokenizer( path );
941         while ( pathTokenizer.hasMoreTokens() )
942         {
943             subpaths.add( pathTokenizer.nextToken() );
944         }
945 
946         return subpaths.toArray( new String[subpaths.size()] );
947     }
948 
949     /**
950      * Unify the given path with the current System path separator, to be platform independent. Examples:
951      *
952      * <pre>
953      * unifyPathSeparator( "/home:/tmp" ) = "/home:/tmp" (Solaris box)
954      * unifyPathSeparator( "/home:/tmp" ) = "/home;/tmp" (Windows box)
955      * </pre>
956      *
957      * @param path which can contain multiple paths by separating them with a colon (<code>:</code>) or a semi-colon
958      *            (<code>;</code>), platform independent. Could be null.
959      * @return the same path but separated with the current System path separator or <code>null</code> if path was
960      *         <code>null</code>.
961      * @since 2.6.1
962      * @see #splitPath(String)
963      * @see File#pathSeparator
964      */
965     protected static String unifyPathSeparator( final String path )
966     {
967         if ( path == null )
968         {
969             return null;
970         }
971 
972         return StringUtils.join( splitPath( path ), File.pathSeparator );
973     }
974 
975     // ----------------------------------------------------------------------
976     // private methods
977     // ----------------------------------------------------------------------
978 
979     /**
980      * @param jarFile not null
981      * @return all class names from the given jar file.
982      * @throws IOException if any or if the jarFile is null or doesn't exist.
983      */
984     private static List<String> getClassNamesFromJar( File jarFile )
985         throws IOException
986     {
987         if ( jarFile == null || !jarFile.exists() || !jarFile.isFile() )
988         {
989             throw new IOException( "The jar '" + jarFile + "' doesn't exist or is not a file." );
990         }
991 
992         List<String> classes = new ArrayList<>();
993         try ( JarInputStream jarStream = new JarInputStream( new FileInputStream( jarFile ) ) )
994         {
995             for ( JarEntry jarEntry = jarStream.getNextJarEntry(); jarEntry != null; jarEntry =
996                 jarStream.getNextJarEntry() )
997             {
998                 if ( jarEntry.getName().toLowerCase( Locale.ENGLISH ).endsWith( ".class" ) )
999                 {
1000                     String name = jarEntry.getName().substring( 0, jarEntry.getName().indexOf( "." ) );
1001 
1002                     classes.add( name.replaceAll( "/", "\\." ) );
1003                 }
1004 
1005                 jarStream.closeEntry();
1006             }
1007         }
1008 
1009         return classes;
1010     }
1011 
1012     /**
1013      * @param log could be null
1014      * @param invoker not null
1015      * @param request not null
1016      * @param invokerLog not null
1017      * @param goals not null
1018      * @param properties could be null
1019      * @param mavenOpts could be null
1020      * @return the invocation result
1021      * @throws MavenInvocationException if any
1022      * @since 2.6
1023      */
1024     private static InvocationResult invoke( Log log, Invoker invoker, InvocationRequest request, File invokerLog,
1025                                             List<String> goals, Properties properties, String mavenOpts )
1026         throws MavenInvocationException
1027     {
1028         PrintStream ps;
1029         OutputStream os = null;
1030         if ( invokerLog != null )
1031         {
1032             if ( log != null && log.isDebugEnabled() )
1033             {
1034                 log.debug( "Using " + invokerLog.getAbsolutePath() + " to log the invoker" );
1035             }
1036 
1037             try
1038             {
1039                 if ( !invokerLog.exists() )
1040                 {
1041                     // noinspection ResultOfMethodCallIgnored
1042                     invokerLog.getParentFile().mkdirs();
1043                 }
1044                 os = new FileOutputStream( invokerLog );
1045                 ps = new PrintStream( os, true, "UTF-8" );
1046             }
1047             catch ( FileNotFoundException e )
1048             {
1049                 if ( log != null && log.isErrorEnabled() )
1050                 {
1051                     log.error( "FileNotFoundException: " + e.getMessage() + ". Using System.out to log the invoker." );
1052                 }
1053                 ps = System.out;
1054             }
1055             catch ( UnsupportedEncodingException e )
1056             {
1057                 if ( log != null && log.isErrorEnabled() )
1058                 {
1059                     log.error( "UnsupportedEncodingException: " + e.getMessage()
1060                         + ". Using System.out to log the invoker." );
1061                 }
1062                 ps = System.out;
1063             }
1064         }
1065         else
1066         {
1067             if ( log != null && log.isDebugEnabled() )
1068             {
1069                 log.debug( "Using System.out to log the invoker." );
1070             }
1071 
1072             ps = System.out;
1073         }
1074 
1075         if ( mavenOpts != null )
1076         {
1077             request.setMavenOpts( mavenOpts );
1078         }
1079 
1080         InvocationOutputHandler outputHandler = new PrintStreamHandler( ps, false );
1081         request.setOutputHandler( outputHandler );
1082 
1083         outputHandler.consumeLine( "Invoking Maven for the goals: " + goals + " with "
1084             + ( properties == null ? "no properties" : "properties=" + properties ) );
1085         outputHandler.consumeLine( "" );
1086         outputHandler.consumeLine( "M2_HOME=" + getMavenHome( log ) );
1087         outputHandler.consumeLine( "MAVEN_OPTS=" + getMavenOpts( log ) );
1088         outputHandler.consumeLine( "JAVA_HOME=" + getJavaHome( log ) );
1089         outputHandler.consumeLine( "JAVA_OPTS=" + getJavaOpts( log ) );
1090         outputHandler.consumeLine( "" );
1091 
1092         try
1093         {
1094             return invoker.execute( request );
1095         }
1096         finally
1097         {
1098             IOUtil.close( os );
1099         }
1100     }
1101 
1102     /**
1103      * @param log a logger could be null
1104      * @return the Maven home defined in the <code>maven.home</code> system property or defined in <code>M2_HOME</code>
1105      *         system env variables or null if never set.
1106      * @since 2.6
1107      */
1108     private static String getMavenHome( Log log )
1109     {
1110         String mavenHome = System.getProperty( "maven.home" );
1111         if ( mavenHome == null )
1112         {
1113             try
1114             {
1115                 mavenHome = CommandLineUtils.getSystemEnvVars().getProperty( "M2_HOME" );
1116             }
1117             catch ( IOException e )
1118             {
1119                 if ( log != null && log.isDebugEnabled() )
1120                 {
1121                     log.debug( "IOException: " + e.getMessage() );
1122                 }
1123             }
1124         }
1125 
1126         File m2Home = new File( mavenHome );
1127         if ( !m2Home.exists() )
1128         {
1129             if ( log != null && log.isErrorEnabled() )
1130             {
1131                 log.error( "Cannot find Maven application directory. Either specify \'maven.home\' system property, or "
1132                     + "M2_HOME environment variable." );
1133             }
1134         }
1135 
1136         return mavenHome;
1137     }
1138 
1139     /**
1140      * @param log a logger could be null
1141      * @return the <code>MAVEN_OPTS</code> env variable value
1142      * @since 2.6
1143      */
1144     private static String getMavenOpts( Log log )
1145     {
1146         String mavenOpts = null;
1147         try
1148         {
1149             mavenOpts = CommandLineUtils.getSystemEnvVars().getProperty( "MAVEN_OPTS" );
1150         }
1151         catch ( IOException e )
1152         {
1153             if ( log != null && log.isDebugEnabled() )
1154             {
1155                 log.debug( "IOException: " + e.getMessage() );
1156             }
1157         }
1158 
1159         return mavenOpts;
1160     }
1161 
1162     /**
1163      * @param log a logger could be null
1164      * @return the <code>JAVA_HOME</code> from System.getProperty( "java.home" ) By default,
1165      *         <code>System.getProperty( "java.home" ) = JRE_HOME</code> and <code>JRE_HOME</code> should be in the
1166      *         <code>JDK_HOME</code>
1167      * @since 2.6
1168      */
1169     private static File getJavaHome( Log log )
1170     {
1171         File javaHome = null;
1172 
1173         String javaHomeValue = null;
1174         try
1175         {
1176             javaHomeValue = CommandLineUtils.getSystemEnvVars().getProperty( "JAVA_HOME" );
1177         }
1178         catch ( IOException e )
1179         {
1180             if ( log != null && log.isDebugEnabled() )
1181             {
1182                 log.debug( "IOException: " + e.getMessage() );
1183             }
1184         }
1185 
1186         // if maven.home is set, we can assume JAVA_HOME must be used for testing
1187         if ( System.getProperty( "maven.home" ) == null || javaHomeValue == null )
1188         {
1189             // JEP220 (Java9) restructured the JRE/JDK runtime image
1190             if ( SystemUtils.IS_OS_MAC_OSX || JavaVersion.JAVA_VERSION.isAtLeast( "9" ) )
1191             {
1192                 javaHome = SystemUtils.getJavaHome();
1193             }
1194             else
1195             {
1196                 javaHome = new File( SystemUtils.getJavaHome(), ".." );
1197             }
1198         }
1199 
1200         if ( javaHome == null || !javaHome.exists() )
1201         {
1202             javaHome = new File( javaHomeValue );
1203         }
1204 
1205         if ( javaHome == null || !javaHome.exists() )
1206         {
1207             if ( log != null && log.isErrorEnabled() )
1208             {
1209                 log.error( "Cannot find Java application directory. Either specify \'java.home\' system property, or "
1210                     + "JAVA_HOME environment variable." );
1211             }
1212         }
1213 
1214         return javaHome;
1215     }
1216 
1217     /**
1218      * @param log a logger could be null
1219      * @return the <code>JAVA_OPTS</code> env variable value
1220      * @since 2.6
1221      */
1222     private static String getJavaOpts( Log log )
1223     {
1224         String javaOpts = null;
1225         try
1226         {
1227             javaOpts = CommandLineUtils.getSystemEnvVars().getProperty( "JAVA_OPTS" );
1228         }
1229         catch ( IOException e )
1230         {
1231             if ( log != null && log.isDebugEnabled() )
1232             {
1233                 log.debug( "IOException: " + e.getMessage() );
1234             }
1235         }
1236 
1237         return javaOpts;
1238     }
1239 
1240     /**
1241      * A Path tokenizer takes a path and returns the components that make up that path. The path can use path separators
1242      * of either ':' or ';' and file separators of either '/' or '\'.
1243      *
1244      * @version revision 439418 taken on 2009-09-12 from Ant Project (see
1245      *          http://svn.apache.org/repos/asf/ant/core/trunk/src/main/org/apache/tools/ant/PathTokenizer.java)
1246      */
1247     private static class PathTokenizer
1248     {
1249         /**
1250          * A tokenizer to break the string up based on the ':' or ';' separators.
1251          */
1252         private StringTokenizer tokenizer;
1253 
1254         /**
1255          * A String which stores any path components which have been read ahead due to DOS filesystem compensation.
1256          */
1257         private String lookahead = null;
1258 
1259         /**
1260          * A boolean that determines if we are running on Novell NetWare, which exhibits slightly different path name
1261          * characteristics (multi-character volume / drive names)
1262          */
1263         private boolean onNetWare = Os.isFamily( "netware" );
1264 
1265         /**
1266          * Flag to indicate whether or not we are running on a platform with a DOS style filesystem
1267          */
1268         private boolean dosStyleFilesystem;
1269 
1270         /**
1271          * Constructs a path tokenizer for the specified path.
1272          *
1273          * @param path The path to tokenize. Must not be <code>null</code>.
1274          */
1275         PathTokenizer( String path )
1276         {
1277             if ( onNetWare )
1278             {
1279                 // For NetWare, use the boolean=true mode, so we can use delimiter
1280                 // information to make a better decision later.
1281                 tokenizer = new StringTokenizer( path, ":;", true );
1282             }
1283             else
1284             {
1285                 // on Windows and Unix, we can ignore delimiters and still have
1286                 // enough information to tokenize correctly.
1287                 tokenizer = new StringTokenizer( path, ":;", false );
1288             }
1289             dosStyleFilesystem = File.pathSeparatorChar == ';';
1290         }
1291 
1292         /**
1293          * Tests if there are more path elements available from this tokenizer's path. If this method returns
1294          * <code>true</code>, then a subsequent call to nextToken will successfully return a token.
1295          *
1296          * @return <code>true</code> if and only if there is at least one token in the string after the current
1297          *         position; <code>false</code> otherwise.
1298          */
1299         public boolean hasMoreTokens()
1300         {
1301             return lookahead != null || tokenizer.hasMoreTokens();
1302 
1303         }
1304 
1305         /**
1306          * Returns the next path element from this tokenizer.
1307          *
1308          * @return the next path element from this tokenizer.
1309          * @exception NoSuchElementException if there are no more elements in this tokenizer's path.
1310          */
1311         public String nextToken()
1312             throws NoSuchElementException
1313         {
1314             String token;
1315             if ( lookahead != null )
1316             {
1317                 token = lookahead;
1318                 lookahead = null;
1319             }
1320             else
1321             {
1322                 token = tokenizer.nextToken().trim();
1323             }
1324 
1325             if ( !onNetWare )
1326             {
1327                 if ( token.length() == 1 && Character.isLetter( token.charAt( 0 ) ) && dosStyleFilesystem
1328                     && tokenizer.hasMoreTokens() )
1329                 {
1330                     // we are on a dos style system so this path could be a drive
1331                     // spec. We look at the next token
1332                     String nextToken = tokenizer.nextToken().trim();
1333                     if ( nextToken.startsWith( "\\" ) || nextToken.startsWith( "/" ) )
1334                     {
1335                         // we know we are on a DOS style platform and the next path
1336                         // starts with a slash or backslash, so we know this is a
1337                         // drive spec
1338                         token += ":" + nextToken;
1339                     }
1340                     else
1341                     {
1342                         // store the token just read for next time
1343                         lookahead = nextToken;
1344                     }
1345                 }
1346             }
1347             else
1348             {
1349                 // we are on NetWare, tokenizing is handled a little differently,
1350                 // due to the fact that NetWare has multiple-character volume names.
1351                 if ( token.equals( File.pathSeparator ) || token.equals( ":" ) )
1352                 {
1353                     // ignore ";" and get the next token
1354                     token = tokenizer.nextToken().trim();
1355                 }
1356 
1357                 if ( tokenizer.hasMoreTokens() )
1358                 {
1359                     // this path could be a drive spec, so look at the next token
1360                     String nextToken = tokenizer.nextToken().trim();
1361 
1362                     // make sure we aren't going to get the path separator next
1363                     if ( !nextToken.equals( File.pathSeparator ) )
1364                     {
1365                         if ( nextToken.equals( ":" ) )
1366                         {
1367                             if ( !token.startsWith( "/" ) && !token.startsWith( "\\" ) && !token.startsWith( "." )
1368                                 && !token.startsWith( ".." ) )
1369                             {
1370                                 // it indeed is a drive spec, get the next bit
1371                                 String oneMore = tokenizer.nextToken().trim();
1372                                 if ( !oneMore.equals( File.pathSeparator ) )
1373                                 {
1374                                     token += ":" + oneMore;
1375                                 }
1376                                 else
1377                                 {
1378                                     token += ":";
1379                                     lookahead = oneMore;
1380                                 }
1381                             }
1382                             // implicit else: ignore the ':' since we have either a
1383                             // UNIX or a relative path
1384                         }
1385                         else
1386                         {
1387                             // store the token just read for next time
1388                             lookahead = nextToken;
1389                         }
1390                     }
1391                 }
1392             }
1393             return token;
1394         }
1395     }
1396 
1397     /**
1398      * Ignores line like 'Picked up JAVA_TOOL_OPTIONS: ...' as can happen on CI servers.
1399      *
1400      * @author Robert Scholte
1401      * @since 3.0.1
1402      */
1403     protected static class JavadocOutputStreamConsumer
1404         extends CommandLineUtils.StringStreamConsumer
1405     {
1406         @Override
1407         public void consumeLine( String line )
1408         {
1409             if ( !line.startsWith( "Picked up " ) )
1410             {
1411                 super.consumeLine( line );
1412             }
1413         }
1414     }
1415 
1416     static List<String> toList( String src )
1417     {
1418         return toList( src, null, null );
1419     }
1420 
1421     static List<String> toList( String src, String elementPrefix, String elementSuffix )
1422     {
1423         if ( StringUtils.isEmpty( src ) )
1424         {
1425             return null;
1426         }
1427 
1428         List<String> result = new ArrayList<>();
1429 
1430         StringTokenizer st = new StringTokenizer( src, "[,:;]" );
1431         StringBuilder sb = new StringBuilder( 256 );
1432         while ( st.hasMoreTokens() )
1433         {
1434             sb.setLength( 0 );
1435             if ( StringUtils.isNotEmpty( elementPrefix ) )
1436             {
1437                 sb.append( elementPrefix );
1438             }
1439 
1440             sb.append( st.nextToken() );
1441 
1442             if ( StringUtils.isNotEmpty( elementSuffix ) )
1443             {
1444                 sb.append( elementSuffix );
1445             }
1446 
1447             result.add( sb.toString() );
1448         }
1449 
1450         return result;
1451     }
1452 
1453     static <T> List<T> toList( T[] multiple )
1454     {
1455         return toList( null, multiple );
1456     }
1457 
1458     static <T> List<T> toList( T single, T[] multiple )
1459     {
1460         if ( single == null && ( multiple == null || multiple.length < 1 ) )
1461         {
1462             return null;
1463         }
1464 
1465         List<T> result = new ArrayList<>();
1466         if ( single != null )
1467         {
1468             result.add( single );
1469         }
1470 
1471         if ( multiple != null && multiple.length > 0 )
1472         {
1473             result.addAll( Arrays.asList( multiple ) );
1474         }
1475 
1476         return result;
1477     }
1478 
1479     // TODO: move to plexus-utils or use something appropriate from there
1480     public static String toRelative( File basedir, String absolutePath )
1481     {
1482         String relative;
1483 
1484         absolutePath = absolutePath.replace( '\\', '/' );
1485         String basedirPath = basedir.getAbsolutePath().replace( '\\', '/' );
1486 
1487         if ( absolutePath.startsWith( basedirPath ) )
1488         {
1489             relative = absolutePath.substring( basedirPath.length() );
1490             if ( relative.startsWith( "/" ) )
1491             {
1492                 relative = relative.substring( 1 );
1493             }
1494             if ( relative.length() <= 0 )
1495             {
1496                 relative = ".";
1497             }
1498         }
1499         else
1500         {
1501             relative = absolutePath;
1502         }
1503 
1504         return relative;
1505     }
1506 
1507     /**
1508      * Convenience method to determine that a collection is not empty or null.
1509      * @param collection the collection to verify
1510      * @return {@code true} if not {@code null} and not empty, otherwise {@code false}
1511      */
1512     public static boolean isNotEmpty( final Collection<?> collection )
1513     {
1514         return collection != null && !collection.isEmpty();
1515     }
1516 
1517     /**
1518      * Convenience method to determine that a collection is empty or null.
1519      * @param collection the collection to verify
1520      * @return {@code true} if {@code null} or empty, otherwise {@code false}
1521      */
1522     public static boolean isEmpty( final Collection<?> collection )
1523     {
1524         return collection == null || collection.isEmpty();
1525     }
1526 
1527     /**
1528      * Execute an Http request at the given URL, follows redirects, and returns the last redirect locations. For URLs
1529      * that aren't http/https, this does nothing and simply returns the given URL unchanged.
1530      *
1531      * @param url URL.
1532      * @param settings Maven settings.
1533      * @return Last redirect location.
1534      * @throws IOException if there was an error during the Http request.
1535      */
1536     protected static URL getRedirectUrl( URL url, Settings settings )
1537         throws IOException
1538     {
1539         String protocol = url.getProtocol();
1540         if ( !"http".equals( protocol ) && !"https".equals( protocol ) )
1541         {
1542             return url;
1543         }
1544 
1545         try ( CloseableHttpClient httpClient = createHttpClient( settings, url ) )
1546         {
1547             HttpClientContext httpContext = HttpClientContext.create();
1548             HttpGet httpMethod = new HttpGet( url.toString() );
1549             HttpResponse response = httpClient.execute( httpMethod, httpContext );
1550             int status = response.getStatusLine().getStatusCode();
1551             if ( status != HttpStatus.SC_OK )
1552             {
1553                 throw new FileNotFoundException( "Unexpected HTTP status code " + status + " getting resource "
1554                     + url.toExternalForm() + "." );
1555             }
1556 
1557             List<URI> redirects = httpContext.getRedirectLocations();
1558 
1559             if ( isEmpty( redirects ) )
1560             {
1561                 return url;
1562             }
1563             else
1564             {
1565                 URI last = redirects.get( redirects.size() - 1 );
1566 
1567                 // URI must refer to directory, so prevent redirects to index.html
1568                 // see https://issues.apache.org/jira/browse/MJAVADOC-539
1569                 String truncate = "index.html";
1570                 if ( last.getPath().endsWith( "/" + truncate ) )
1571                 {
1572                     try
1573                     {
1574                         String fixedPath = last.getPath().substring( 0, last.getPath().length() - truncate.length() );
1575                         last = new URI( last.getScheme(), last.getAuthority(), fixedPath, last.getQuery(),
1576                                 last.getFragment() );
1577                     }
1578                     catch ( URISyntaxException ex )
1579                     {
1580                         // not supposed to happen, but when it does just keep the last URI
1581                     }
1582                 }
1583                 return last.toURL();
1584             }
1585         }
1586     }
1587 
1588     /**
1589      * Validates an <code>URL</code> to point to a valid <code>package-list</code> resource.
1590      *
1591      * @param url The URL to validate.
1592      * @param settings The user settings used to configure the connection to the URL or {@code null}.
1593      * @param validateContent <code>true</code> to validate the content of the <code>package-list</code> resource;
1594      *            <code>false</code> to only check the existence of the <code>package-list</code> resource.
1595      * @return <code>true</code> if <code>url</code> points to a valid <code>package-list</code> resource;
1596      *         <code>false</code> else.
1597      * @throws IOException if reading the resource fails.
1598      * @see #createHttpClient(org.apache.maven.settings.Settings, java.net.URL)
1599      * @since 2.8
1600      */
1601     protected static boolean isValidPackageList( URL url, Settings settings, boolean validateContent )
1602         throws IOException
1603     {
1604         if ( url == null )
1605         {
1606             throw new IllegalArgumentException( "The url is null" );
1607         }
1608 
1609         try ( BufferedReader reader = getReader( url, settings ) )
1610         {
1611             if ( validateContent )
1612             {
1613                 for ( String line = reader.readLine(); line != null; line = reader.readLine() )
1614                 {
1615                     if ( !isValidPackageName( line ) )
1616                     {
1617                         return false;
1618                     }
1619                 }
1620             }
1621             return true;
1622         }
1623     }
1624 
1625     protected static boolean isValidElementList( URL url, Settings settings, boolean validateContent )
1626                     throws IOException
1627     {
1628         if ( url == null )
1629         {
1630             throw new IllegalArgumentException( "The url is null" );
1631         }
1632 
1633         try ( BufferedReader reader = getReader( url, settings ) )
1634         {
1635             if ( validateContent )
1636             {
1637                 for ( String line = reader.readLine(); line != null; line = reader.readLine() )
1638                 {
1639                     if ( line.startsWith( "module:" ) )
1640                     {
1641                         continue;
1642                     }
1643 
1644                     if ( !isValidPackageName( line ) )
1645                     {
1646                         return false;
1647                     }
1648                 }
1649             }
1650             return true;
1651         }
1652     }
1653 
1654     private static BufferedReader getReader( URL url, Settings settings ) throws IOException
1655     {
1656         BufferedReader reader = null;
1657 
1658         if ( "file".equals( url.getProtocol() ) )
1659         {
1660             // Intentionally using the platform default encoding here since this is what Javadoc uses internally.
1661             reader = new BufferedReader( new InputStreamReader( url.openStream() ) );
1662         }
1663         else
1664         {
1665             // http, https...
1666             final CloseableHttpClient httpClient = createHttpClient( settings, url );
1667 
1668             final HttpGet httpMethod = new HttpGet( url.toString() );
1669 
1670             HttpResponse response;
1671             HttpClientContext httpContext = HttpClientContext.create();
1672             try
1673             {
1674                 response = httpClient.execute( httpMethod, httpContext );
1675             }
1676             catch ( SocketTimeoutException e )
1677             {
1678                 // could be a sporadic failure, one more retry before we give up
1679                 response = httpClient.execute( httpMethod, httpContext );
1680             }
1681 
1682             int status = response.getStatusLine().getStatusCode();
1683             if ( status != HttpStatus.SC_OK )
1684             {
1685                 throw new FileNotFoundException( "Unexpected HTTP status code " + status + " getting resource "
1686                     + url.toExternalForm() + "." );
1687             }
1688             else
1689             {
1690                 int pos = url.getPath().lastIndexOf( '/' );
1691                 List<URI> redirects = httpContext.getRedirectLocations();
1692                 if ( pos >= 0 && isNotEmpty( redirects ) )
1693                 {
1694                     URI location = redirects.get( redirects.size() - 1 );
1695                     String suffix = url.getPath().substring( pos );
1696                     // Redirections shall point to the same file, e.g. /package-list
1697                     if ( !location.getPath().endsWith( suffix ) )
1698                     {
1699                         throw new FileNotFoundException( url.toExternalForm() + " redirects to "
1700                                 + location.toURL().toExternalForm() + "." );
1701                     }
1702                 }
1703             }
1704 
1705             // Intentionally using the platform default encoding here since this is what Javadoc uses internally.
1706             reader = new BufferedReader( new InputStreamReader( response.getEntity().getContent() ) )
1707             {
1708                 @Override
1709                 public void close()
1710                     throws IOException
1711                 {
1712                     super.close();
1713 
1714                     if ( httpMethod != null )
1715                     {
1716                         httpMethod.releaseConnection();
1717                     }
1718                     if ( httpClient != null )
1719                     {
1720                         httpClient.close();
1721                     }
1722                 }
1723             };
1724         }
1725 
1726         return reader;
1727     }
1728 
1729     private static boolean isValidPackageName( String str )
1730     {
1731         if ( StringUtils.isEmpty( str ) )
1732         {
1733             // unnamed package is valid (even if bad practice :) )
1734             return true;
1735         }
1736 
1737         int idx;
1738         while ( ( idx = str.indexOf( '.' ) ) != -1 )
1739         {
1740             if ( !isValidClassName( str.substring( 0, idx ) ) )
1741             {
1742                 return false;
1743             }
1744 
1745             str = str.substring( idx + 1 );
1746         }
1747 
1748         return isValidClassName( str );
1749     }
1750 
1751     private static boolean isValidClassName( String str )
1752     {
1753         if ( StringUtils.isEmpty( str ) || !Character.isJavaIdentifierStart( str.charAt( 0 ) ) )
1754         {
1755             return false;
1756         }
1757 
1758         for ( int i = str.length() - 1; i > 0; i-- )
1759         {
1760             if ( !Character.isJavaIdentifierPart( str.charAt( i ) ) )
1761             {
1762                 return false;
1763             }
1764         }
1765 
1766         return true;
1767     }
1768 
1769     /**
1770      * Creates a new {@code HttpClient} instance.
1771      *
1772      * @param settings The settings to use for setting up the client or {@code null}.
1773      * @param url The {@code URL} to use for setting up the client or {@code null}.
1774      * @return A new {@code HttpClient} instance.
1775      * @see #DEFAULT_TIMEOUT
1776      * @since 2.8
1777      */
1778     private static CloseableHttpClient createHttpClient( Settings settings, URL url )
1779     {
1780         HttpClientBuilder builder = HttpClients.custom();
1781         
1782         Registry<ConnectionSocketFactory> csfRegistry =
1783             RegistryBuilder.<ConnectionSocketFactory>create()
1784                 .register( "http", PlainConnectionSocketFactory.getSocketFactory() )
1785                 .register( "https", SSLConnectionSocketFactory.getSystemSocketFactory() )
1786                 .build();
1787         
1788         builder.setConnectionManager( new PoolingHttpClientConnectionManager( csfRegistry ) );
1789         builder.setDefaultRequestConfig( RequestConfig.custom()
1790                                          .setSocketTimeout( DEFAULT_TIMEOUT )
1791                                          .setConnectTimeout( DEFAULT_TIMEOUT )
1792                                          .setCircularRedirectsAllowed( true )
1793                                          .setCookieSpec( CookieSpecs.IGNORE_COOKIES )
1794                                          .build() );
1795         
1796         // Some web servers don't allow the default user-agent sent by httpClient
1797         builder.setUserAgent( "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)" );
1798 
1799         // Some server reject requests that do not have an Accept header
1800         builder.setDefaultHeaders( Arrays.asList( new BasicHeader( HttpHeaders.ACCEPT, "*/*" ) ) );
1801 
1802         if ( settings != null && settings.getActiveProxy() != null )
1803         {
1804             Proxy activeProxy = settings.getActiveProxy();
1805 
1806             ProxyInfo proxyInfo = new ProxyInfo();
1807             proxyInfo.setNonProxyHosts( activeProxy.getNonProxyHosts() );
1808 
1809             if ( StringUtils.isNotEmpty( activeProxy.getHost() )
1810                 && ( url == null || !ProxyUtils.validateNonProxyHosts( proxyInfo, url.getHost() ) ) )
1811             {
1812                 HttpHost proxy = new HttpHost( activeProxy.getHost(), activeProxy.getPort() );
1813                 builder.setProxy( proxy );
1814 
1815                 if ( StringUtils.isNotEmpty( activeProxy.getUsername() ) && activeProxy.getPassword() != null )
1816                 {
1817                     Credentials credentials =
1818                         new UsernamePasswordCredentials( activeProxy.getUsername(), activeProxy.getPassword() );
1819 
1820                     CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
1821                     credentialsProvider.setCredentials( AuthScope.ANY, credentials );
1822                     builder.setDefaultCredentialsProvider( credentialsProvider );
1823                 }
1824             }
1825         }
1826         return builder.build();
1827     }
1828 
1829     static boolean equalsIgnoreCase( String value, String... strings )
1830     {
1831         for ( String s : strings )
1832         {
1833             if ( s.equalsIgnoreCase( value ) )
1834             {
1835                 return true;
1836             }
1837         }
1838         return false;
1839     }
1840 
1841     static boolean equals( String value, String... strings )
1842     {
1843         for ( String s : strings )
1844         {
1845             if ( s.equals( value ) )
1846             {
1847                 return true;
1848             }
1849         }
1850         return false;
1851     }
1852 }