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 org.apache.maven.artifact.repository.ArtifactRepository;
23  import org.apache.maven.doxia.linkcheck.HttpBean;
24  import org.apache.maven.doxia.linkcheck.LinkCheck;
25  import org.apache.maven.doxia.linkcheck.LinkCheckException;
26  import org.apache.maven.doxia.linkcheck.model.LinkcheckModel;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugins.annotations.Component;
29  import org.apache.maven.plugins.annotations.Mojo;
30  import org.apache.maven.plugins.annotations.Parameter;
31  import org.apache.maven.reporting.AbstractMavenReport;
32  import org.apache.maven.reporting.MavenReportException;
33  import org.apache.maven.settings.Proxy;
34  import org.apache.maven.settings.Settings;
35  import org.codehaus.plexus.i18n.I18N;
36  import org.codehaus.plexus.util.FileUtils;
37  import org.codehaus.plexus.util.ReaderFactory;
38  import org.codehaus.plexus.util.StringUtils;
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.net.URL;
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.List;
46  import java.util.Locale;
47  import java.util.Properties;
48  
49  /**
50   * Generates a <code>Linkcheck</code> report.
51   *
52   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
53   * @version $Id: LinkcheckReport.java 1628568 2014-09-30 22:19:10Z hboutemy $
54   * @since 1.0
55   */
56  @Mojo( name = "linkcheck" )
57  public class LinkcheckReport
58      extends AbstractMavenReport
59  {
60      // ----------------------------------------------------------------------
61      // Report Components
62      // ----------------------------------------------------------------------
63  
64      /**
65       * Internationalization.
66       */
67      @Component
68      private I18N i18n;
69  
70      /**
71       * LinkCheck component.
72       */
73      @Component
74      private LinkCheck linkCheck;
75  
76      // ----------------------------------------------------------------------
77      // Report Parameters
78      // ----------------------------------------------------------------------
79  
80      /**
81       * Local Repository.
82       */
83      @Parameter( defaultValue = "${localRepository}", required = true, readonly = true )
84      private ArtifactRepository localRepository;
85  
86      /**
87       * The Maven Settings.
88       */
89      @Parameter( defaultValue = "${settings}", readonly = true, required = true )
90      private Settings settings;
91  
92      // ----------------------------------------------------------------------
93      // Linkcheck parameters
94      // ----------------------------------------------------------------------
95  
96      /**
97       * Whether we are offline or not.
98       */
99      @Parameter( property = "linkcheck.offline", defaultValue = "${settings.offline}", required = true )
100     private boolean offline;
101 
102     /**
103      * If online, the HTTP method should automatically follow HTTP redirects,
104      * <tt>false</tt> otherwise.
105      *
106      * @parameter default-value="true"
107      */
108     @Parameter( defaultValue = "true" )
109     private boolean httpFollowRedirect;
110 
111     /**
112      * The location of the Linkcheck cache file.
113      */
114     @Parameter( defaultValue = "${project.build.directory}/linkcheck/linkcheck.cache", required = true )
115     protected File linkcheckCache;
116 
117     /**
118      * The location of the Linkcheck report file.
119      */
120     @Parameter( defaultValue = "${project.build.directory}/linkcheck/linkcheck.xml", required = true )
121     protected File linkcheckOutput;
122 
123     /**
124      * The HTTP method to use. Currently supported are "GET" and "HEAD".
125      * <dl>
126      * <dt>HTTP GET</dt>
127      * <dd>
128      * The HTTP GET method is defined in section 9.3 of
129      * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
130      * The GET method means retrieve whatever information (in the form of an
131      * entity) is identified by the Request-URI.
132      * </dd>
133      * <dt>HTTP HEAD</dt>
134      * <dd>
135      * The HTTP HEAD method is defined in section 9.4 of
136      * <a href="http://www.ietf.org/rfc/rfc2616.txt">RFC2616</a>:
137      * The HEAD method is identical to GET except that the server MUST NOT
138      * return a message-body in the response.
139      * </dd>
140      * </dl>
141      */
142     @Parameter( defaultValue = "head", required = true )
143     private String httpMethod;
144 
145     /**
146      * The list of HTTP errors to ignored, like <code>404</code>.
147      *
148      * @see {@link org.apache.commons.httpclient.HttpStatus} for all defined values.
149      */
150     @Parameter
151     private Integer[] excludedHttpStatusErrors;
152 
153     /**
154      * The list of HTTP warnings to ignored, like <code>301</code>.
155      *
156      * @see {@link org.apache.commons.httpclient.HttpStatus} for all defined values.
157      */
158     @Parameter
159     private Integer[] excludedHttpStatusWarnings;
160 
161     /**
162      * A list of pages to exclude.
163      * <br/>
164      * <b>Note</b>:
165      * <br/>
166      * <ul>
167      * <li>This report, i.e. <code>linkcheck.html</code>, is always excluded.</li>
168      * <li>May contain Ant-style wildcards and double wildcards, e.g. <code>apidocs/**</code>, etc.</li>
169      * </ul>
170      */
171     @Parameter
172     private String[] excludedPages;
173 
174     /**
175      * The list of links to exclude.
176      * <br/>
177      * <b>Note</b>: Patterns like <code>&#42;&#42;/dummy/&#42;</code> are allowed for excludedLink.
178      */
179     @Parameter
180     private String[] excludedLinks;
181 
182     /**
183      * The file encoding to use when Linkcheck reads the source files. If the property
184      * <code>project.build.sourceEncoding</code> is not set, the platform default encoding is used.
185      */
186     @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
187     private String encoding;
188 
189     /**
190      * The extra HttpClient parameters to be used when fetching links. For instance:
191      * <pre>
192      * &lt;httpClientParameters&gt;
193      * &nbsp;&lt;property&gt;
194      * &nbsp;&nbsp;&lt;name&gt;http.protocol.max-redirects&lt;/name&gt;
195      * &nbsp;&nbsp;&lt;value&gt;10&lt;/value&gt;
196      * &nbsp;&lt;/property&gt;
197      * &lt;/httpClientParameters&gt;
198      * </pre>
199      * See <a href="http://hc.apache.org/httpclient-3.x/preference-api.html">HttpClient preference page</a>
200      */
201     @Parameter( property = "httpClientParameters" )
202     private Properties httpClientParameters;
203 
204     /**
205      * Set the timeout to be used when fetching links. A value of zero means the timeout is not used.
206      */
207     @Parameter( property = "timeout", defaultValue = "2000" )
208     private int timeout;
209 
210     /**
211      * <code>true</code> to skip the report execution, <code>false</code> otherwise.
212      * The purpose is to prevent infinite call when {@link #forceSite} is enable.
213      */
214     @Parameter( property = "linkcheck.skip", defaultValue = "false" )
215     private boolean skip;
216 
217     /**
218      * <code>true</code> to force the site generation, <code>false</code> otherwise.
219      * Using this parameter ensures that all documents have been correctly generated.
220      */
221     @Parameter( property = "linkcheck.forceSite", defaultValue = "true" )
222     private boolean forceSite;
223 
224     /**
225      * The base URL to use for absolute links (eg <code>/index.html</code>) in the site.
226      */
227     @Parameter( property = "linkcheck.baseURL", defaultValue = "${project.url}" )
228     private String baseURL;
229 
230     // ----------------------------------------------------------------------
231     // Instance fields
232     // ----------------------------------------------------------------------
233 
234     /** Result of the linkcheck in {@link #execute()} */
235     private LinkcheckModel result;
236 
237     protected static final String ICON_SUCCESS = "images/icon_success_sml.gif";
238     protected static final String ICON_WARNING = "images/icon_warning_sml.gif";
239     protected static final String ICON_INFO = "images/icon_info_sml.gif";
240     protected static final String ICON_ERROR = "images/icon_error_sml.gif";
241     private static final String pluginResourcesBase = "org/apache/maven/plugin/linkcheck";
242     private static final String resourceNames[] = { ICON_SUCCESS, ICON_WARNING, ICON_INFO, ICON_ERROR };
243 
244     // ----------------------------------------------------------------------
245     // Public methods
246     // ----------------------------------------------------------------------
247 
248     /** {@inheritDoc} */
249     public String getDescription( Locale locale )
250     {
251         return i18n.getString( "linkcheck-report", locale, "report.linkcheck.description" );
252     }
253 
254     /** {@inheritDoc} */
255     public String getName( Locale locale )
256     {
257         return i18n.getString( "linkcheck-report", locale, "report.linkcheck.name" );
258     }
259 
260     /** {@inheritDoc} */
261     public String getOutputName()
262     {
263         return "linkcheck";
264     }
265 
266     /** {@inheritDoc} */
267     public boolean canGenerateReport()
268     {
269         return !skip;
270     }
271 
272     /** {@inheritDoc} */
273     public void execute()
274         throws MojoExecutionException
275     {
276         if ( !canGenerateReport() )
277         {
278             return;
279         }
280 
281         checkEncoding();
282 
283         try
284         {
285             result = executeLinkCheck( getBasedir() );
286         }
287         catch ( LinkCheckException e )
288         {
289             throw new MojoExecutionException( "LinkCheckException: " + e.getMessage(), e );
290         }
291     }
292 
293     // ----------------------------------------------------------------------
294     // Protected methods
295     // ----------------------------------------------------------------------
296 
297     /** {@inheritDoc} */
298     protected void executeReport( Locale locale )
299         throws MavenReportException
300     {
301         if ( result == null )
302         {
303             getLog().debug( "Calling execute()" );
304 
305             try
306             {
307                 this.execute();
308             }
309             catch ( MojoExecutionException e )
310             {
311                 throw new MavenReportException( "MojoExecutionException: " + e.getMessage(), e );
312             }
313         }
314 
315         if ( result != null )
316         {
317             generateReport( locale, result );
318             // free memory
319             result = null;
320         }
321     }
322 
323     // ----------------------------------------------------------------------
324     // Private methods
325     // ----------------------------------------------------------------------
326 
327     private void checkEncoding()
328     {
329         if ( StringUtils.isEmpty( encoding ) )
330         {
331             if ( getLog().isWarnEnabled() )
332             {
333                 getLog().warn( "File encoding has not been set, using platform encoding "
334                     + ReaderFactory.FILE_ENCODING + ", i.e. build is platform dependent!" );
335             }
336 
337             encoding = ReaderFactory.FILE_ENCODING;
338         }
339     }
340 
341     private File getBasedir()
342         throws MojoExecutionException
343     {
344         File basedir;
345 
346         if ( forceSite )
347         {
348             basedir = new File( linkcheckOutput.getParentFile(), "tmpsite" );
349             basedir.mkdirs();
350 
351             List documents = null;
352             try
353             {
354                 documents = FileUtils.getFiles( basedir, "**/*.html", null );
355             }
356             catch ( IOException e )
357             {
358                 getLog().error( "IOException: " + e.getMessage() );
359                 getLog().debug( e );
360             }
361 
362             // if the site was not already generated, invoke it
363             if ( documents == null || ( documents != null && documents.size() == 0 ) )
364             {
365                 getLog().info( "Invoking the maven-site-plugin to ensure that all files are generated..." );
366 
367                 try
368                 {
369                     SiteInvoker invoker = new SiteInvoker( localRepository, getLog() );
370                     invoker.invokeSite( project, basedir );
371                 }
372                 catch ( IOException e )
373                 {
374                     throw new MojoExecutionException( "IOException: " + e.getMessage(), e );
375                 }
376             }
377         }
378         else
379         {
380             getLog().warn( "The number of documents analyzed by Linkcheck could differ from the actual "
381                                    + "number of documents!" );
382 
383             basedir = outputDirectory;
384             basedir.mkdirs();
385         }
386 
387         return basedir;
388     }
389 
390     /**
391      * Execute the <code>Linkcheck</code> tool.
392      *
393      * @param basedir not null
394      * @throws LinkCheckException if any
395      */
396     private LinkcheckModel executeLinkCheck( File basedir )
397         throws LinkCheckException
398     {
399         // Wrap linkcheck
400         linkCheck.setOnline( !offline );
401         linkCheck.setBasedir( basedir );
402         linkCheck.setBaseURL( baseURL );
403         linkCheck.setReportOutput( linkcheckOutput );
404         linkCheck.setLinkCheckCache( linkcheckCache );
405         linkCheck.setExcludedLinks( excludedLinks );
406         linkCheck.setExcludedPages( getExcludedPages() );
407         linkCheck.setExcludedHttpStatusErrors( asIntArray( excludedHttpStatusErrors ) );
408         linkCheck.setExcludedHttpStatusWarnings( asIntArray( excludedHttpStatusWarnings ) );
409         linkCheck.setEncoding( ( StringUtils.isNotEmpty( encoding ) ? encoding : ReaderFactory.UTF_8 ) );
410 
411         HttpBean bean = new HttpBean();
412         bean.setMethod( httpMethod );
413         bean.setFollowRedirects( httpFollowRedirect );
414         bean.setTimeout( timeout );
415         if ( httpClientParameters != null )
416         {
417             bean.setHttpClientParameters( httpClientParameters );
418         }
419 
420         Proxy proxy = settings.getActiveProxy();
421         if ( proxy != null )
422         {
423             bean.setProxyHost( proxy.getHost() );
424             bean.setProxyPort( proxy.getPort() );
425             bean.setProxyUser( proxy.getUsername() );
426             bean.setProxyPassword( proxy.getPassword() );
427         }
428         linkCheck.setHttp( bean );
429 
430         return linkCheck.execute();
431     }
432 
433     /**
434      * @return the excludedPages defined by the user and also this report.
435      */
436     private String[] getExcludedPages()
437     {
438         List pagesToExclude =
439             ( excludedPages != null ? new ArrayList( Arrays.asList( excludedPages ) ) : new ArrayList() );
440 
441         // Exclude this report
442         pagesToExclude.add( getOutputName() + ".html" );
443 
444         return (String[]) pagesToExclude.toArray(new String[pagesToExclude.size()]);
445     }
446 
447     // ----------------------------------------------------------------------
448     // Linkcheck report
449     // ----------------------------------------------------------------------
450 
451     private void generateReport( Locale locale, LinkcheckModel linkcheckModel )
452     {
453         LinkcheckReportGenerator reportGenerator = new LinkcheckReportGenerator( i18n );
454 
455         reportGenerator.setExcludedHttpStatusErrors( excludedHttpStatusErrors );
456         reportGenerator.setExcludedHttpStatusWarnings( excludedHttpStatusWarnings );
457         reportGenerator.setExcludedLinks( excludedLinks );
458         reportGenerator.setExcludedPages( excludedPages );
459         reportGenerator.setHttpFollowRedirect( httpFollowRedirect );
460         reportGenerator.setHttpMethod( httpMethod );
461         reportGenerator.setOffline( offline );
462 
463         reportGenerator.generateReport( locale, linkcheckModel, getSink() );
464         closeReport();
465 
466         // Copy the images
467         copyStaticResources();
468     }
469 
470     private void copyStaticResources()
471     {
472         try
473         {
474             getLog().debug( "Copying static linkcheck resources." );
475             for (String resourceName : resourceNames) {
476                 URL url = this.getClass().getClassLoader().getResource(pluginResourcesBase + "/" + resourceName);
477                 FileUtils.copyURLToFile(url, new File(getReportOutputDirectory(), resourceName));
478             }
479         }
480         catch ( IOException e )
481         {
482             getLog().error( "Unable to copy icons for linkcheck report." );
483             getLog().debug( e );
484         }
485     }
486 
487     private static int[] asIntArray( Integer[] array )
488     {
489         if ( array == null )
490         {
491             return null;
492         }
493 
494         int[] newArray = new int[array.length];
495 
496         for ( int i = 0; i < array.length; i++ )
497         {
498             newArray[i] = array[i];
499         }
500 
501         return newArray;
502     }
503 }