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