View Javadoc

1   package org.apache.maven.plugins.linkcheck;
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.File;
23  import java.io.FileNotFoundException;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStream;
27  import java.io.PrintStream;
28  import java.io.Reader;
29  import java.io.UnsupportedEncodingException;
30  import java.io.Writer;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Collections;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Properties;
38  
39  import org.apache.commons.io.FilenameUtils;
40  import org.apache.commons.lang.SystemUtils;
41  import org.apache.maven.artifact.repository.ArtifactRepository;
42  import org.apache.maven.doxia.linkcheck.HttpBean;
43  import org.apache.maven.doxia.linkcheck.LinkCheck;
44  import org.apache.maven.doxia.linkcheck.LinkCheckException;
45  import org.apache.maven.doxia.linkcheck.model.LinkcheckFile;
46  import org.apache.maven.doxia.linkcheck.model.LinkcheckFileResult;
47  import org.apache.maven.doxia.linkcheck.model.LinkcheckModel;
48  import org.apache.maven.doxia.siterenderer.Renderer;
49  import org.apache.maven.model.Reporting;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.project.MavenProject;
52  import org.apache.maven.reporting.AbstractMavenReport;
53  import org.apache.maven.reporting.MavenReportException;
54  import org.apache.maven.settings.Proxy;
55  import org.apache.maven.settings.Settings;
56  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
57  import org.apache.maven.shared.invoker.DefaultInvoker;
58  import org.apache.maven.shared.invoker.InvocationOutputHandler;
59  import org.apache.maven.shared.invoker.InvocationRequest;
60  import org.apache.maven.shared.invoker.InvocationResult;
61  import org.apache.maven.shared.invoker.Invoker;
62  import org.apache.maven.shared.invoker.MavenInvocationException;
63  import org.apache.maven.shared.invoker.PrintStreamHandler;
64  import org.codehaus.plexus.i18n.I18N;
65  import org.codehaus.plexus.util.FileUtils;
66  import org.codehaus.plexus.util.IOUtil;
67  import org.codehaus.plexus.util.ReaderFactory;
68  import org.codehaus.plexus.util.StringUtils;
69  import org.codehaus.plexus.util.WriterFactory;
70  import org.codehaus.plexus.util.cli.CommandLineUtils;
71  
72  /**
73   * Generates a <code>Linkcheck</code> report.
74   *
75   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
76   * @version $Id: LinkcheckReport.java 997509 2010-09-15 21:24:28Z dennisl $
77   * @since 1.0
78   * @goal linkcheck
79   */
80  public class LinkcheckReport
81      extends AbstractMavenReport
82  {
83      // ----------------------------------------------------------------------
84      // Report Components
85      // ----------------------------------------------------------------------
86  
87      /**
88       * Internationalization.
89       *
90       * @component
91       */
92      private I18N i18n;
93  
94      /**
95       * Doxia Site Renderer.
96       *
97       * @component
98       */
99      private Renderer siteRenderer;
100 
101     /**
102      * LinkCheck component.
103      *
104      * @component
105      */
106     private LinkCheck linkCheck;
107 
108     // ----------------------------------------------------------------------
109     // Report Parameters
110     // ----------------------------------------------------------------------
111 
112     /**
113      * The Maven Project.
114      *
115      * @parameter expression="${project}"
116      * @required
117      * @readonly
118      */
119     private MavenProject project;
120 
121     /**
122      * Local Repository.
123      *
124      * @parameter expression="${localRepository}"
125      * @required
126      * @readonly
127      */
128     private ArtifactRepository localRepository;
129 
130     /**
131      * Report output directory.
132      *
133      * @parameter expression="${project.reporting.outputDirectory}"
134      * @required
135      */
136     private File outputDirectory;
137 
138     /**
139      * The Maven Settings.
140      *
141      * @parameter default-value="${settings}"
142      * @required
143      * @readonly
144      */
145     private Settings settings;
146 
147     // ----------------------------------------------------------------------
148     // Linkcheck parameters
149     // ----------------------------------------------------------------------
150 
151     /**
152      * Whether we are offline or not.
153      *
154      * @parameter default-value="${settings.offline}" expression="${linkcheck.offline}"
155      * @required
156      */
157     private boolean offline;
158 
159     /**
160      * If online, the HTTP method should automatically follow HTTP redirects,
161      * <tt>false</tt> otherwise.
162      *
163      * @parameter default-value="true"
164      */
165     private boolean httpFollowRedirect;
166 
167     /**
168      * The location of the Linkcheck cache file.
169      *
170      * @parameter default-value="${project.build.directory}/linkcheck/linkcheck.cache"
171      * @required
172      */
173     protected File linkcheckCache;
174 
175     /**
176      * The location of the Linkcheck report file.
177      *
178      * @parameter default-value="${project.build.directory}/linkcheck/linkcheck.xml"
179      * @required
180      */
181     protected File linkcheckOutput;
182 
183     /**
184      * The HTTP method to use. Currently supported are "GET" and "HEAD".
185      * <dl>
186      * <dt>HTTP GET</dt>
187      * <dd>
188      * The HTTP GET method is defined in section 9.3 of
189      * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
190      * The GET method means retrieve whatever information (in the form of an
191      * entity) is identified by the Request-URI.
192      * </dd>
193      * <dt>HTTP HEAD</dt>
194      * <dd>
195      * The HTTP HEAD method is defined in section 9.4 of
196      * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
197      * The HEAD method is identical to GET except that the server MUST NOT
198      * return a message-body in the response.
199      * </dd>
200      * </dl>
201      *
202      * @parameter default-value="head"
203      * @required
204      */
205     private String httpMethod;
206 
207     /**
208      * The list of HTTP errors to ignored, like <code>404</code>.
209      *
210      * @parameter
211      * @see {@link org.apache.commons.httpclient.HttpStatus} for all defined values.
212      */
213     private int[] excludedHttpStatusErrors;
214 
215     /**
216      * The list of HTTP warnings to ignored, like <code>301</code>.
217      *
218      * @parameter
219      * @see {@link org.apache.commons.httpclient.HttpStatus} for all defined values.
220      */
221     private int[] excludedHttpStatusWarnings;
222 
223     /**
224      * The list of site pages to exclude. By default, this report, i.e. <code>linkcheck.html</code>, will be excluded.
225      * <br/>
226      * <b>Note</b>: No pattern is allowed for excludedPage, only specific file names.
227      *
228      * @parameter
229      */
230     private String[] excludedPages;
231 
232     /**
233      * The list of links to exclude.
234      * <br/>
235      * <b>Note</b>: Patterns like <code>&#42;&#42;/dummy/&#42;</code> are allowed for excludedLink.
236      *
237      * @parameter
238      */
239     private String[] excludedLinks;
240 
241     /**
242      * The file encoding to use when Linkcheck reads the source files. If the property
243      * <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used.
244      *
245      * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
246      */
247     private String encoding;
248 
249     /**
250      * The extra HttpClient parameters to be used when fetching links. For instance:
251      * <pre>
252      * &lt;httpClientParameters&gt;
253      * &nbsp;&lt;property&gt;
254      * &nbsp;&nbsp;&lt;name&gt;http.protocol.max-redirects&lt;/name&gt;
255      * &nbsp;&nbsp;&lt;value&gt;10&lt;/value&gt;
256      * &nbsp;&lt;/property&gt;
257      * &lt;/httpClientParameters&gt;
258      * </pre>
259      * See <a href="http://hc.apache.org/httpclient-3.x/preference-api.html">HttpClient preference page</a>
260      *
261      * @parameter expression="${httpClientParameters}"
262      */
263     private Properties httpClientParameters;
264 
265     /**
266      * Set the timeout to be used when fetching links. A value of zero means the timeout is not used.
267      *
268      * @parameter expression="${timeout}" default-value="2000"
269      */
270     private int timeout;
271 
272     /**
273      * <code>true</code> to skip the report execution, <code>false</code> otherwise.
274      * The purpose is to prevent infinite call when {@link #forceSite} is enable.
275      *
276      * @parameter expression="${linkcheck.skip}" default-value="false"
277      */
278     private boolean skip;
279 
280     /**
281      * <code>true</code> to force the site generation, <code>false</code> otherwise.
282      * Using this parameter ensures that all documents have been correctly generated.
283      *
284      * @parameter expression="${linkcheck.forceSite}" default-value="true"
285      */
286     private boolean forceSite;
287 
288     /**
289      * The base URL to use for absolute links (eg <code>/index.html</code>) in the site.
290      *
291      * @parameter expression="${linkcheck.baseURL}" default-value="${project.url}"
292      */
293     private String baseURL;
294 
295     // ----------------------------------------------------------------------
296     // Instance fields
297     // ----------------------------------------------------------------------
298 
299     /** Result of the linkcheck in {@link #execute()} */
300     private LinkcheckModel result;
301 
302     // ----------------------------------------------------------------------
303     // Public methods
304     // ----------------------------------------------------------------------
305 
306     /** {@inheritDoc} */
307     public String getDescription( Locale locale )
308     {
309         return i18n.getString( "linkcheck-report", locale, "report.linkcheck.description" );
310     }
311 
312     /** {@inheritDoc} */
313     public String getName( Locale locale )
314     {
315         return i18n.getString( "linkcheck-report", locale, "report.linkcheck.name" );
316     }
317 
318     /** {@inheritDoc} */
319     public String getOutputName()
320     {
321         return "linkcheck";
322     }
323 
324     /** {@inheritDoc} */
325     public boolean canGenerateReport()
326     {
327         if ( skip )
328         {
329             return false;
330         }
331 
332         return true;
333     }
334 
335     /** {@inheritDoc} */
336     public void execute()
337         throws MojoExecutionException
338     {
339         if ( !canGenerateReport() )
340         {
341             return;
342         }
343 
344         // encoding
345         if ( StringUtils.isEmpty( encoding ) )
346         {
347             if ( getLog().isWarnEnabled() )
348             {
349                 getLog().warn(
350                                "File encoding has not been set, using platform encoding "
351                                    + ReaderFactory.FILE_ENCODING + ", i.e. build is platform dependent!" );
352             }
353             encoding = ReaderFactory.FILE_ENCODING;
354         }
355 
356         File tmpReportingOutputDirectory = new File( linkcheckOutput.getParentFile(), "tmpsite" );
357         tmpReportingOutputDirectory.mkdirs();
358 
359         File basedir;
360         if ( forceSite )
361         {
362             basedir = tmpReportingOutputDirectory;
363 
364             List documents = null;
365             try
366             {
367                 documents = FileUtils.getFiles( basedir, "**/*.html", null );
368             }
369             catch ( IOException e )
370             {
371                 String msg = "IOException: " + e.getMessage();
372                 if ( getLog().isDebugEnabled() )
373                 {
374                     getLog().error( msg, e );
375                 }
376                 else
377                 {
378                     getLog().error( msg );
379                 }
380             }
381 
382             // if the site was not already generated, invoke it
383             if ( documents == null || ( documents != null && documents.size() == 0 ) )
384             {
385                 getLog().info( "Trying to invoke the maven-site-plugin to be sure that all files are generated..." );
386 
387                 try
388                 {
389                     invokeSite( tmpReportingOutputDirectory );
390                 }
391                 catch ( IOException e )
392                 {
393                     throw new MojoExecutionException( "IOException: " + e.getMessage(), e );
394                 }
395             }
396         }
397         else
398         {
399             if ( getLog().isWarnEnabled() )
400             {
401                 getLog().warn(
402                                "WARRANTY: The number of documents analyzed by Linkcheck could differ with the real "
403                                    + "number of documents!" );
404             }
405 
406             basedir = outputDirectory;
407             basedir.mkdirs();
408         }
409 
410         try
411         {
412             result = executeLinkCheck( basedir );
413         }
414         catch ( LinkCheckException e )
415         {
416             throw new MojoExecutionException( "LinkCheckException: " + e.getMessage(), e );
417         }
418     }
419 
420     // ----------------------------------------------------------------------
421     // Protected methods
422     // ----------------------------------------------------------------------
423 
424     /** {@inheritDoc} */
425     protected String getOutputDirectory()
426     {
427         return outputDirectory.getAbsolutePath();
428     }
429 
430     /** {@inheritDoc} */
431     protected MavenProject getProject()
432     {
433         return project;
434     }
435 
436     /** {@inheritDoc} */
437     protected Renderer getSiteRenderer()
438     {
439         return siteRenderer;
440     }
441 
442     /** {@inheritDoc} */
443     protected void executeReport( Locale locale )
444         throws MavenReportException
445     {
446         if ( result == null )
447         {
448             getLog().debug( "Calling execute()" );
449 
450             try
451             {
452                 this.execute();
453             }
454             catch ( MojoExecutionException e )
455             {
456                 throw new MavenReportException( "MojoExecutionException: " + e.getMessage(), e );
457             }
458         }
459 
460         if ( result != null )
461         {
462             generateReport( locale, result );
463             // free memory
464             result = null;
465         }
466     }
467 
468     // ----------------------------------------------------------------------
469     // Private methods
470     // ----------------------------------------------------------------------
471 
472     /**
473      * Execute the <code>Linkcheck</code> tool.
474      *
475      * @param basedir not null
476      * @throws LinkCheckException if any
477      */
478     private LinkcheckModel executeLinkCheck( File basedir )
479         throws LinkCheckException
480     {
481         // Wrap linkcheck
482         linkCheck.setOnline( !offline );
483         linkCheck.setBasedir( basedir );
484         linkCheck.setBaseURL( baseURL );
485         linkCheck.setReportOutput( linkcheckOutput );
486         linkCheck.setLinkCheckCache( linkcheckCache );
487         linkCheck.setExcludedLinks( excludedLinks );
488         linkCheck.setExcludedPages( getExcludedPages() );
489         linkCheck.setExcludedHttpStatusErrors( excludedHttpStatusErrors );
490         linkCheck.setExcludedHttpStatusWarnings( excludedHttpStatusWarnings );
491         linkCheck.setEncoding( ( StringUtils.isNotEmpty( encoding ) ? encoding : WriterFactory.UTF_8 ) );
492 
493         HttpBean bean = new HttpBean();
494         bean.setMethod( httpMethod );
495         bean.setFollowRedirects( httpFollowRedirect );
496         bean.setTimeout( timeout );
497         if ( httpClientParameters != null )
498         {
499             bean.setHttpClientParameters( httpClientParameters );
500         }
501 
502         Proxy proxy = settings.getActiveProxy();
503         if ( proxy != null )
504         {
505             bean.setProxyHost( proxy.getHost() );
506             bean.setProxyPort( proxy.getPort() );
507             bean.setProxyUser( proxy.getUsername() );
508             bean.setProxyPassword( proxy.getPassword() );
509         }
510         linkCheck.setHttp( bean );
511 
512         return linkCheck.execute();
513     }
514 
515     /**
516      * @return the excludedPages defined by the user and also this report.
517      */
518     private String[] getExcludedPages()
519     {
520         List pagesToExclude =
521             ( excludedPages != null ? new ArrayList( Arrays.asList( excludedPages ) ) : new ArrayList() );
522 
523         // Exclude this report
524         pagesToExclude.add( getOutputName() + ".html" );
525 
526         return (String[]) pagesToExclude.toArray( new String[0] );
527     }
528 
529     /**
530      * Invoke Maven for the <code>site</code> phase for a temporary Maven project using
531      * <code>tmpReportingOutputDirectory</code> as <code>${project.reporting.outputDirectory}</code>.
532      * This is a workaround to be sure that all site files have been correctly generated.
533      * <br/>
534      * <b>Note 1</b>: the Maven Home should be defined in the <code>maven.home</code> Java system property
535      * or defined in <code>M2_HOME</code> system env variables.
536      * <b>Note 2</be>: we can't use <code>siteOutputDirectory</code> param from site plugin because some plugins
537      * <code>${project.reporting.outputDirectory}</code> in there conf.
538      *
539      * @param tmpReportingOutputDirectory not null
540      * @throws IOException if any
541      */
542     private void invokeSite( File tmpReportingOutputDirectory )
543         throws IOException
544     {
545         String mavenHome = getMavenHome();
546         if ( StringUtils.isEmpty( mavenHome ) )
547         {
548             if ( getLog().isErrorEnabled() )
549             {
550                 String msg =
551                     "Could NOT invoke Maven because no Maven Home is defined. You need to have set the M2_HOME "
552                         + "system env variable or a 'maven.home' Java system properties.";
553                 getLog().error( msg );
554             }
555             return;
556         }
557 
558         // invoker site parameters
559         List goals = Collections.singletonList( "site" );
560         Properties properties = new Properties();
561         properties.put( "linkcheck.skip", "true" ); // to stop recursion
562 
563         File invokerLog =
564             FileUtils
565                      .createTempFile( "invoker-site-plugin", ".txt", new File( project.getBuild().getDirectory() ) );
566 
567         // clone project and set a new reporting output dir
568         MavenProject clone;
569         try
570         {
571             clone = (MavenProject) project.clone();
572         }
573         catch ( CloneNotSupportedException e )
574         {
575             IOException ioe = new IOException( "CloneNotSupportedException: " + e.getMessage() );
576             ioe.setStackTrace( e.getStackTrace() );
577             throw ioe;
578         }
579 
580         // MLINKCHECK-1
581         if ( clone.getOriginalModel().getReporting() == null )
582         {
583             clone.getOriginalModel().setReporting( new Reporting() );
584         }
585 
586         clone.getOriginalModel().getReporting().setOutputDirectory( tmpReportingOutputDirectory.getAbsolutePath() );
587 
588         // create the original model as tmp pom file for the invoker
589         File tmpProjectFile = FileUtils.createTempFile( "pom", ".xml", project.getBasedir() );
590         Writer writer = null;
591         try
592         {
593             writer = WriterFactory.newXmlWriter( tmpProjectFile );
594             clone.writeOriginalModel( writer );
595         }
596         finally
597         {
598             IOUtil.close( writer );
599         }
600 
601         // invoke it
602         try
603         {
604             invoke( tmpProjectFile, invokerLog, mavenHome, goals, properties );
605         }
606         finally
607         {
608             if ( !getLog().isDebugEnabled() )
609             {
610                 tmpProjectFile.delete();
611             }
612         }
613     }
614 
615     /**
616      * @param projectFile not null, should be in the ${project.basedir}
617      * @param invokerLog not null
618      * @param mavenHome not null
619      * @param goals the list of goals
620      * @param properties the properties for the invoker
621      */
622     private void invoke( File projectFile, File invokerLog, String mavenHome, List goals, Properties properties )
623     {
624         Invoker invoker = new DefaultInvoker();
625         invoker.setMavenHome( new File( mavenHome ) );
626         invoker.setLocalRepositoryDirectory( new File( localRepository.getBasedir() ) );
627 
628         InvocationRequest request = new DefaultInvocationRequest();
629         request.setBaseDirectory( projectFile.getParentFile() );
630         request.setPomFile( projectFile );
631         request.setDebug( getLog().isDebugEnabled() );
632         request.setGoals( goals );
633         request.setProperties( properties );
634         File javaHome = getJavaHome();
635         if ( javaHome != null )
636         {
637             request.setJavaHome( javaHome );
638         }
639 
640         InvocationResult invocationResult;
641         try
642         {
643             if ( getLog().isDebugEnabled() )
644             {
645                 getLog().debug( "Invoking Maven for the goals: " + goals + " with properties=" + properties );
646             }
647             invocationResult = invoke( invoker, request, invokerLog, goals, properties, null );
648         }
649         catch ( MavenInvocationException e )
650         {
651             if ( getLog().isDebugEnabled() )
652             {
653                 getLog().error( "MavenInvocationException: " + e.getMessage(), e );
654             }
655             getLog().error( "Error when invoking Maven, consult the invoker log." );
656             return;
657         }
658 
659         String invokerLogContent = null;
660         Reader reader = null;
661         try
662         {
663             reader = ReaderFactory.newReader( invokerLog, "UTF-8" );
664             invokerLogContent = IOUtil.toString( reader );
665         }
666         catch ( IOException e )
667         {
668             String msg = "IOException: " + e.getMessage();
669             if ( getLog().isDebugEnabled() )
670             {
671                 getLog().error( msg, e );
672             }
673             else
674             {
675                 getLog().error( msg );
676             }
677         }
678         finally
679         {
680             IOUtil.close( reader );
681         }
682 
683         if ( invokerLogContent != null
684             && invokerLogContent.indexOf( "Error occurred during initialization of VM" ) != -1 )
685         {
686             getLog().info( "Error occurred during initialization of VM, try to use an empty MAVEN_OPTS." );
687 
688             if ( getLog().isDebugEnabled() )
689             {
690                 getLog().debug( "Reinvoking Maven for the goals: " + goals + " with an empty MAVEN_OPTS" );
691             }
692 
693             try
694             {
695                 invocationResult = invoke( invoker, request, invokerLog, goals, properties, "" );
696             }
697             catch ( MavenInvocationException e )
698             {
699                 if ( getLog().isDebugEnabled() )
700                 {
701                     getLog().error( "MavenInvocationException: " + e.getMessage(), e );
702                 }
703                 getLog().error( "Error when reinvoking Maven, consult the invoker log." );
704                 return;
705             }
706         }
707 
708         if ( invocationResult.getExitCode() != 0 )
709         {
710             if ( getLog().isErrorEnabled() )
711             {
712                 getLog().error(
713                                 "Error when invoking Maven, consult the invoker log file: "
714                                     + invokerLog.getAbsolutePath() );
715             }
716         }
717     }
718 
719     /**
720      * @param invoker not null
721      * @param request not null
722      * @param invokerLog not null
723      * @param goals the list of goals
724      * @param properties the properties for the invoker
725      * @param mavenOpts could be null
726      * @return the invocation result
727      * @throws MavenInvocationException if any
728      */
729     private InvocationResult invoke( Invoker invoker, InvocationRequest request, File invokerLog, List goals,
730                                      Properties properties, String mavenOpts )
731         throws MavenInvocationException
732     {
733         PrintStream ps;
734         OutputStream os = null;
735         if ( invokerLog != null )
736         {
737             if ( getLog().isDebugEnabled() )
738             {
739                 getLog().debug( "Using " + invokerLog.getAbsolutePath() + " to log the invoker" );
740             }
741 
742             try
743             {
744                 if ( !invokerLog.exists() )
745                 {
746                     invokerLog.getParentFile().mkdirs();
747                 }
748                 os = new FileOutputStream( invokerLog );
749                 ps = new PrintStream( os, true, "UTF-8" );
750             }
751             catch ( FileNotFoundException e )
752             {
753                 if ( getLog().isErrorEnabled() )
754                 {
755                     getLog().error(
756                                     "FileNotFoundException: " + e.getMessage()
757                                         + ". Using System.out to log the invoker." );
758                 }
759                 ps = System.out;
760             }
761             catch ( UnsupportedEncodingException e )
762             {
763                 if ( getLog().isErrorEnabled() )
764                 {
765                     getLog().error(
766                                     "UnsupportedEncodingException: " + e.getMessage()
767                                         + ". Using System.out to log the invoker." );
768                 }
769                 ps = System.out;
770             }
771         }
772         else
773         {
774             getLog().debug( "Using System.out to log the invoker." );
775 
776             ps = System.out;
777         }
778 
779         if ( mavenOpts != null )
780         {
781             request.setMavenOpts( mavenOpts );
782         }
783 
784         InvocationOutputHandler outputHandler = new PrintStreamHandler( ps, false );
785         request.setOutputHandler( outputHandler );
786 
787         outputHandler.consumeLine( "Invoking Maven for the goals: " + goals + " with properties=" + properties );
788         outputHandler.consumeLine( "" );
789         outputHandler.consumeLine( "M2_HOME=" + getMavenHome() );
790         outputHandler.consumeLine( "MAVEN_OPTS=" + getMavenOpts() );
791         outputHandler.consumeLine( "JAVA_HOME=" + getJavaHome() );
792         outputHandler.consumeLine( "JAVA_OPTS=" + getJavaOpts() );
793         outputHandler.consumeLine( "" );
794 
795         try
796         {
797             return invoker.execute( request );
798         }
799         finally
800         {
801             IOUtil.close( os );
802             ps = null;
803         }
804     }
805 
806     /**
807      * @return the Maven home defined in the <code>maven.home</code> system property or defined
808      * in <code>M2_HOME</code> system env variables or null if never setted.
809      * @see #invoke(Invoker, InvocationRequest, File, List, Properties, String)
810      */
811     private String getMavenHome()
812     {
813         String mavenHome = System.getProperty( "maven.home" );
814         if ( mavenHome == null )
815         {
816             try
817             {
818                 mavenHome = CommandLineUtils.getSystemEnvVars().getProperty( "M2_HOME" );
819             }
820             catch ( IOException e )
821             {
822                 String msg = "IOException: " + e.getMessage();
823                 if ( getLog().isDebugEnabled() )
824                 {
825                     getLog().error( msg, e );
826                 }
827                 else
828                 {
829                     getLog().error( msg );
830                 }
831             }
832         }
833 
834         File m2Home = new File( mavenHome );
835         if ( !m2Home.exists() )
836         {
837             if ( getLog().isErrorEnabled() )
838             {
839                 getLog().error(
840                                 "Cannot find Maven application directory. Either specify \'maven.home\' "
841                                     + "system property, or M2_HOME environment variable." );
842             }
843         }
844 
845         return mavenHome;
846     }
847 
848     /**
849      * @return the <code>MAVEN_OPTS</code> env variable value or null if not setted.
850      * @see #invoke(Invoker, InvocationRequest, File, List, Properties, String)
851      */
852     private String getMavenOpts()
853     {
854         String mavenOpts = null;
855         try
856         {
857             mavenOpts = CommandLineUtils.getSystemEnvVars().getProperty( "MAVEN_OPTS" );
858         }
859         catch ( IOException e )
860         {
861             String msg = "IOException: " + e.getMessage();
862             if ( getLog().isDebugEnabled() )
863             {
864                 getLog().error( msg, e );
865             }
866             else
867             {
868                 getLog().error( msg );
869             }
870         }
871 
872         return mavenOpts;
873     }
874 
875     /**
876      * @return the <code>JAVA_HOME</code> from System.getProperty( "java.home" )
877      * By default, <code>System.getProperty( "java.home" ) = JRE_HOME</code> and <code>JRE_HOME</code>
878      * should be in the <code>JDK_HOME</code> or null if not setted.
879      * @see #invoke(Invoker, InvocationRequest, File, List, Properties, String)
880      */
881     private File getJavaHome()
882     {
883         File javaHome;
884         if ( SystemUtils.IS_OS_MAC_OSX )
885         {
886             javaHome = SystemUtils.getJavaHome();
887         }
888         else
889         {
890             javaHome = new File( SystemUtils.getJavaHome(), ".." );
891         }
892 
893         if ( javaHome == null || !javaHome.exists() )
894         {
895             try
896             {
897                 javaHome = new File( CommandLineUtils.getSystemEnvVars().getProperty( "JAVA_HOME" ) );
898             }
899             catch ( IOException e )
900             {
901                 String msg = "IOException: " + e.getMessage();
902                 if ( getLog().isDebugEnabled() )
903                 {
904                     getLog().error( msg, e );
905                 }
906                 else
907                 {
908                     getLog().error( msg );
909                 }
910             }
911         }
912 
913         if ( javaHome == null || !javaHome.exists() )
914         {
915             if ( getLog().isErrorEnabled() )
916             {
917                 getLog().error(
918                                 "Cannot find Java application directory. Either specify \'java.home\' "
919                                     + "system property, or JAVA_HOME environment variable." );
920             }
921         }
922 
923         return javaHome;
924     }
925 
926     /**
927      * @return the <code>JAVA_OPTS</code> env variable value or null if not setted.
928      * @see #invoke(Invoker, InvocationRequest, File, List, Properties, String)
929      */
930     private String getJavaOpts()
931     {
932         String javaOpts = null;
933         try
934         {
935             javaOpts = CommandLineUtils.getSystemEnvVars().getProperty( "JAVA_OPTS" );
936         }
937         catch ( IOException e )
938         {
939             String msg = "IOException: " + e.getMessage();
940             if ( getLog().isDebugEnabled() )
941             {
942                 getLog().error( msg, e );
943             }
944             else
945             {
946                 getLog().error( msg );
947             }
948         }
949 
950         return javaOpts;
951     }
952 
953     // ----------------------------------------------------------------------
954     // Linkcheck report
955     // ----------------------------------------------------------------------
956 
957     private void generateReport( Locale locale, LinkcheckModel linkcheckModel )
958     {
959         getSink().head();
960         getSink().title();
961         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.title" ) );
962         getSink().title_();
963         getSink().head_();
964 
965         getSink().body();
966 
967         if ( linkcheckModel == null )
968         {
969             getSink().section1();
970             getSink().sectionTitle1();
971             getSink().text( getName( locale ) );
972             getSink().sectionTitle1_();
973 
974             getSink().paragraph();
975             getSink().rawText( i18n.getString( "linkcheck-report", locale, "report.linkcheck.empty" ) );
976             getSink().paragraph_();
977 
978             getSink().section1_();
979 
980             getSink().body_();
981             getSink().flush();
982             getSink().close();
983 
984             return;
985         }
986 
987         // Overview
988         getSink().section1();
989         getSink().sectionTitle1();
990         getSink().text( getName( locale ) );
991         getSink().sectionTitle1_();
992 
993         getSink().paragraph();
994         getSink().rawText( i18n.getString( "linkcheck-report", locale, "report.linkcheck.overview" ) );
995         getSink().paragraph_();
996 
997         getSink().section1_();
998 
999         // Statistics
1000         generateSummarySection( locale, linkcheckModel );
1001 
1002         if ( linkcheckModel.getFiles().size() > 0 )
1003         {
1004             // Details
1005             generateDetailsSection( locale, linkcheckModel );
1006         }
1007 
1008         getSink().body_();
1009         getSink().flush();
1010         getSink().close();
1011 
1012         closeReport();
1013     }
1014 
1015     private void generateSummarySection( Locale locale, LinkcheckModel linkcheckModel )
1016     {
1017         // Calculus
1018         List linkcheckFiles = linkcheckModel.getFiles();
1019 
1020         int totalFiles = linkcheckFiles.size();
1021 
1022         int totalLinks = 0;
1023         int totalValidLinks = 0;
1024         int totalErrorLinks = 0;
1025         int totalWarningLinks = 0;
1026         for ( Iterator it = linkcheckFiles.iterator(); it.hasNext(); )
1027         {
1028             LinkcheckFile linkcheckFile = (LinkcheckFile) it.next();
1029 
1030             totalLinks += linkcheckFile.getNumberOfLinks();
1031             totalValidLinks += linkcheckFile.getNumberOfLinks( LinkcheckFileResult.VALID_LEVEL );
1032             totalErrorLinks += linkcheckFile.getNumberOfLinks( LinkcheckFileResult.ERROR_LEVEL );
1033             totalWarningLinks += linkcheckFile.getNumberOfLinks( LinkcheckFileResult.WARNING_LEVEL );
1034         }
1035 
1036         getSink().section1();
1037         getSink().sectionTitle1();
1038         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary" ) );
1039         getSink().sectionTitle1_();
1040 
1041         // Summary of the analysis parameters
1042         getSink().paragraph();
1043         getSink().rawText( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.overview1" ) );
1044         getSink().paragraph_();
1045 
1046         getSink().table();
1047 
1048         getSink().tableRow();
1049         getSink().tableHeaderCell();
1050         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.parameter" ) );
1051         getSink().tableHeaderCell_();
1052         getSink().tableHeaderCell();
1053         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.value" ) );
1054         getSink().tableHeaderCell_();
1055         getSink().tableRow_();
1056 
1057         getSink().tableRow();
1058         getSink().tableCell();
1059         getSink().rawText(
1060                            i18n.getString( "linkcheck-report", locale,
1061                                            "report.linkcheck.summary.table.httpFollowRedirect" ) );
1062         getSink().tableCell_();
1063         getSink().tableCell();
1064         getSink().text( String.valueOf( httpFollowRedirect ) );
1065         getSink().tableCell_();
1066         getSink().tableRow_();
1067 
1068         getSink().tableRow();
1069         getSink().tableCell();
1070         getSink()
1071                  .rawText(
1072                            i18n
1073                                .getString( "linkcheck-report", locale, "report.linkcheck.summary.table.httpMethod" ) );
1074         getSink().tableCell_();
1075         getSink().tableCell();
1076         if ( StringUtils.isEmpty( httpMethod ) )
1077         {
1078             getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.none" ) );
1079         }
1080         else
1081         {
1082             getSink().text( httpMethod );
1083         }
1084         getSink().tableCell_();
1085         getSink().tableRow_();
1086 
1087         getSink().tableRow();
1088         getSink().tableCell();
1089         getSink().rawText(
1090                            i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.offline" ) );
1091         getSink().tableCell_();
1092         getSink().tableCell();
1093         getSink().text( String.valueOf( offline ) );
1094         getSink().tableCell_();
1095         getSink().tableRow_();
1096 
1097         getSink().tableRow();
1098         getSink().tableCell();
1099         getSink().rawText(
1100                            i18n.getString( "linkcheck-report", locale,
1101                                            "report.linkcheck.summary.table.excludedPages" ) );
1102         getSink().tableCell_();
1103         getSink().tableCell();
1104         if ( getExcludedPages() == null || getExcludedPages().length == 0 )
1105         {
1106             getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.none" ) );
1107         }
1108         else
1109         {
1110             getSink().text( StringUtils.join( getExcludedPages(), "," ) );
1111         }
1112         getSink().tableCell_();
1113         getSink().tableRow_();
1114 
1115         getSink().tableRow();
1116         getSink().tableCell();
1117         getSink().rawText(
1118                            i18n.getString( "linkcheck-report", locale,
1119                                            "report.linkcheck.summary.table.excludedLinks" ) );
1120         getSink().tableCell_();
1121         getSink().tableCell();
1122         if ( excludedLinks == null || excludedLinks.length == 0 )
1123         {
1124             getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.none" ) );
1125         }
1126         else
1127         {
1128             getSink().text( StringUtils.join( excludedLinks, "," ) );
1129         }
1130         getSink().tableCell_();
1131         getSink().tableRow_();
1132 
1133         getSink().tableRow();
1134         getSink().tableCell();
1135         getSink().rawText(
1136                            i18n.getString( "linkcheck-report", locale,
1137                                            "report.linkcheck.summary.table.excludedHttpStatusErrors" ) );
1138         getSink().tableCell_();
1139         getSink().tableCell();
1140         if ( excludedHttpStatusErrors == null || excludedHttpStatusErrors.length == 0 )
1141         {
1142             getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.none" ) );
1143         }
1144         else
1145         {
1146             getSink().text( toString( excludedHttpStatusErrors ) );
1147         }
1148         getSink().tableCell_();
1149         getSink().tableRow_();
1150 
1151         getSink().tableRow();
1152         getSink().tableCell();
1153         getSink().rawText(
1154                            i18n.getString( "linkcheck-report", locale,
1155                                            "report.linkcheck.summary.table.excludedHttpStatusWarnings" ) );
1156         getSink().tableCell_();
1157         getSink().tableCell();
1158         if ( excludedHttpStatusWarnings == null || excludedHttpStatusWarnings.length == 0 )
1159         {
1160             getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.table.none" ) );
1161         }
1162         else
1163         {
1164             getSink().text( toString( excludedHttpStatusWarnings ) );
1165         }
1166         getSink().tableCell_();
1167         getSink().tableRow_();
1168 
1169         getSink().table_();
1170 
1171         // Summary of the checked files
1172         getSink().paragraph();
1173         getSink().rawText( i18n.getString( "linkcheck-report", locale, "report.linkcheck.summary.overview2" ) );
1174         getSink().paragraph_();
1175 
1176         getSink().table();
1177 
1178         // Header
1179         generateTableHeader( locale, false );
1180 
1181         // Content
1182         getSink().tableRow();
1183 
1184         getSink().tableCell();
1185         getSink().bold();
1186         getSink().text( totalFiles + "" );
1187         getSink().bold_();
1188         getSink().tableCell_();
1189         getSink().tableCell();
1190         getSink().bold();
1191         getSink().text( totalLinks + "" );
1192         getSink().bold_();
1193         getSink().tableCell_();
1194         getSink().tableCell();
1195         getSink().bold();
1196         getSink().text( String.valueOf( totalValidLinks ) );
1197         getSink().bold_();
1198         getSink().tableCell_();
1199         getSink().tableCell();
1200         getSink().bold();
1201         getSink().text( String.valueOf( totalWarningLinks ) );
1202         getSink().bold_();
1203         getSink().tableCell_();
1204         getSink().tableCell();
1205         getSink().bold();
1206         getSink().text( String.valueOf( totalErrorLinks ) );
1207         getSink().bold_();
1208         getSink().tableCell_();
1209 
1210         getSink().tableRow_();
1211 
1212         getSink().table_();
1213 
1214         getSink().section1_();
1215     }
1216 
1217     private void generateDetailsSection( Locale locale, LinkcheckModel linkcheckModel )
1218     {
1219         getSink().section1();
1220         getSink().sectionTitle1();
1221         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.detail" ) );
1222         getSink().sectionTitle1_();
1223 
1224         getSink().paragraph();
1225         getSink().rawText( i18n.getString( "linkcheck-report", locale, "report.linkcheck.detail.overview" ) );
1226         getSink().paragraph_();
1227 
1228         getSink().table();
1229 
1230         // Header
1231         generateTableHeader( locale, true );
1232 
1233         // Content
1234         List linkcheckFiles = linkcheckModel.getFiles();
1235         for ( Iterator it = linkcheckFiles.iterator(); it.hasNext(); )
1236         {
1237             LinkcheckFile linkcheckFile = (LinkcheckFile) it.next();
1238 
1239             getSink().tableRow();
1240 
1241             getSink().tableCell();
1242             if ( linkcheckFile.getUnsuccessful() == 0 )
1243             {
1244                 iconValid( locale );
1245             }
1246             else
1247             {
1248                 iconError( locale );
1249             }
1250             getSink().tableCell_();
1251 
1252             // tableCell( createLinkPatternedText( linkcheckFile.getRelativePath(), "./"
1253             // + linkcheckFile.getRelativePath() ) );
1254             getSink().tableCell();
1255             getSink().link( linkcheckFile.getRelativePath() );
1256             getSink().text( linkcheckFile.getRelativePath() );
1257             getSink().link_();
1258             getSink().tableCell_();
1259             getSink().tableCell();
1260             getSink().text( String.valueOf( linkcheckFile.getNumberOfLinks() ) );
1261             getSink().tableCell_();
1262             getSink().tableCell();
1263             getSink().text( String.valueOf( linkcheckFile.getNumberOfLinks( LinkcheckFileResult.VALID_LEVEL ) ) );
1264             getSink().tableCell_();
1265             getSink().tableCell();
1266             getSink().text( String.valueOf( linkcheckFile.getNumberOfLinks( LinkcheckFileResult.WARNING_LEVEL ) ) );
1267             getSink().tableCell_();
1268             getSink().tableCell();
1269             getSink().text( String.valueOf( linkcheckFile.getNumberOfLinks( LinkcheckFileResult.ERROR_LEVEL ) ) );
1270             getSink().tableCell_();
1271 
1272             getSink().tableRow_();
1273 
1274             // Detail error
1275             if ( linkcheckFile.getUnsuccessful() != 0 )
1276             {
1277                 getSink().tableRow();
1278 
1279                 getSink().tableCell();
1280                 getSink().text( "" );
1281                 getSink().tableCell_();
1282 
1283                 // TODO it is due to DOXIA-78
1284                 getSink().rawText( "<td colspan=\"5\">" );
1285 
1286                 getSink().table();
1287 
1288                 for ( Iterator it2 = linkcheckFile.getResults().iterator(); it2.hasNext(); )
1289                 {
1290                     LinkcheckFileResult linkcheckFileResult = (LinkcheckFileResult) it2.next();
1291 
1292                     if ( linkcheckFileResult.getStatusLevel() == LinkcheckFileResult.VALID_LEVEL )
1293                     {
1294                         continue;
1295                     }
1296 
1297                     getSink().tableRow();
1298 
1299                     getSink().tableCell();
1300                     if ( linkcheckFileResult.getStatusLevel() == LinkcheckFileResult.WARNING_LEVEL )
1301                     {
1302                         iconWarning( locale );
1303                     }
1304                     else if ( linkcheckFileResult.getStatusLevel() == LinkcheckFileResult.ERROR_LEVEL )
1305                     {
1306                         iconError( locale );
1307                     }
1308                     getSink().tableCell_();
1309 
1310                     getSink().tableCell();
1311                     getSink().italic();
1312                     if ( linkcheckFileResult.getTarget().startsWith( "#" ) )
1313                     {
1314                         getSink().link( linkcheckFile.getRelativePath() + linkcheckFileResult.getTarget() );
1315                     }
1316                     else if ( linkcheckFileResult.getTarget().startsWith( "." ) )
1317                     {
1318                         // We need to calculate a correct absolute path here, because target is a relative path
1319                         String absolutePath = FilenameUtils.getFullPath( linkcheckFile.getRelativePath() )
1320                             + linkcheckFileResult.getTarget();
1321                         String normalizedPath = FilenameUtils.normalize( absolutePath );
1322                         if ( normalizedPath == null )
1323                         {
1324                             normalizedPath = absolutePath;
1325                         }
1326                         getSink().link( normalizedPath );
1327                     }
1328                     else
1329                     {
1330                         getSink().link( linkcheckFileResult.getTarget() );
1331                     }
1332                     // Show the link as it was written to make it easy for
1333                     // the author to find it in the source document
1334                     getSink().text( linkcheckFileResult.getTarget() );
1335                     getSink().link_();
1336                     getSink().text( ": " );
1337                     getSink().text( linkcheckFileResult.getErrorMessage() );
1338                     getSink().italic_();
1339                     getSink().tableCell_();
1340 
1341                     getSink().tableRow_();
1342                 }
1343 
1344                 getSink().table_();
1345 
1346                 getSink().tableCell_();
1347 
1348                 getSink().tableRow_();
1349             }
1350         }
1351 
1352         getSink().table_();
1353 
1354         getSink().section1_();
1355     }
1356 
1357     private void generateTableHeader( Locale locale, boolean detail )
1358     {
1359         getSink().tableRow();
1360         if ( detail )
1361         {
1362             getSink().rawText( "<th rowspan=\"2\">" );
1363             getSink().text( "" );
1364             getSink().tableHeaderCell_();
1365         }
1366         getSink().rawText( "<th rowspan=\"2\">" );
1367         getSink().text(
1368                         detail ? i18n.getString( "linkcheck-report", locale,
1369                                                  "report.linkcheck.detail.table.documents" )
1370                                         : i18n.getString( "linkcheck-report", locale,
1371                                                           "report.linkcheck.summary.table.documents" ) );
1372         getSink().tableHeaderCell_();
1373         // TODO it is due to DOXIA-78
1374         getSink().rawText( "<th colspan=\"4\" align=\"center\">" );
1375         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.table.links" ) );
1376         getSink().tableHeaderCell_();
1377         getSink().tableRow_();
1378 
1379         getSink().tableRow();
1380         getSink().tableHeaderCell();
1381         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.table.totalLinks" ) );
1382         getSink().tableHeaderCell_();
1383         getSink().tableHeaderCell();
1384         iconValid( locale );
1385         getSink().tableHeaderCell_();
1386         getSink().tableHeaderCell();
1387         iconWarning( locale );
1388         getSink().tableHeaderCell_();
1389         getSink().tableHeaderCell();
1390         iconError( locale );
1391         getSink().tableHeaderCell_();
1392         getSink().tableRow_();
1393     }
1394 
1395     private void iconError( Locale locale )
1396     {
1397         getSink().figure();
1398         getSink().figureCaption();
1399         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.icon.error" ) );
1400         getSink().figureCaption_();
1401         // should be defined in skins
1402         getSink().figureGraphics( "images/icon_error_sml.gif" );
1403         getSink().figure_();
1404     }
1405 
1406     private void iconValid( Locale locale )
1407     {
1408         getSink().figure();
1409         getSink().figureCaption();
1410         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.icon.valid" ) );
1411         getSink().figureCaption_();
1412         // should be defined in skins
1413         getSink().figureGraphics( "images/icon_success_sml.gif" );
1414         getSink().figure_();
1415     }
1416 
1417     private void iconWarning( Locale locale )
1418     {
1419         getSink().figure();
1420         getSink().figureCaption();
1421         getSink().text( i18n.getString( "linkcheck-report", locale, "report.linkcheck.icon.warning" ) );
1422         getSink().figureCaption_();
1423         // should be defined in skins
1424         getSink().figureGraphics( "images/icon_warning_sml.gif" );
1425         getSink().figure_();
1426     }
1427 
1428     // ----------------------------------------------------------------------
1429     // static methods
1430     // ----------------------------------------------------------------------
1431 
1432     /**
1433      * Similar to {@link Arrays#toString(int[])} in 1.5.
1434      *
1435      * @param a not null
1436      * @return the array comma separated.
1437      */
1438     private static String toString( int[] a )
1439     {
1440         if ( a == null || a.length == 0 )
1441         {
1442             return "";
1443         }
1444 
1445         StringBuffer buf = new StringBuffer();
1446         buf.append( a[0] );
1447 
1448         for ( int i = 1; i < a.length; i++ )
1449         {
1450             buf.append( ", " );
1451             buf.append( a[i] );
1452         }
1453 
1454         return buf.toString();
1455     }
1456 }