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