View Javadoc
1   package org.apache.maven.plugin.docck;
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.commons.httpclient.Credentials;
23  import org.apache.commons.httpclient.HttpClient;
24  import org.apache.commons.httpclient.HttpException;
25  import org.apache.commons.httpclient.UsernamePasswordCredentials;
26  import org.apache.commons.httpclient.auth.AuthScope;
27  import org.apache.commons.httpclient.methods.HeadMethod;
28  import org.apache.commons.httpclient.params.HttpMethodParams;
29  import org.apache.maven.model.IssueManagement;
30  import org.apache.maven.model.License;
31  import org.apache.maven.model.Organization;
32  import org.apache.maven.model.Prerequisites;
33  import org.apache.maven.model.Scm;
34  import org.apache.maven.plugin.AbstractMojo;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugin.docck.reports.DocumentationReport;
38  import org.apache.maven.plugin.docck.reports.DocumentationReporter;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.settings.Proxy;
42  import org.apache.maven.settings.Settings;
43  import org.apache.maven.shared.model.fileset.FileSet;
44  import org.apache.maven.shared.model.fileset.util.FileSetManager;
45  import org.codehaus.plexus.util.IOUtil;
46  import org.codehaus.plexus.util.StringUtils;
47  
48  import java.io.File;
49  import java.io.FileWriter;
50  import java.io.IOException;
51  import java.net.MalformedURLException;
52  import java.net.URL;
53  import java.util.ArrayList;
54  import java.util.LinkedHashMap;
55  import java.util.List;
56  import java.util.Map;
57  
58  /**
59   * Performs the heavy lifting for documentation checks. This is designed to be
60   * reused for other types of projects, too.
61   *
62   * @author jdcasey
63   */
64  public abstract class AbstractCheckDocumentationMojo
65      extends AbstractMojo
66  {
67      private static final int HTTP_STATUS_200 = 200;
68  
69      /**
70       */
71      @Parameter( property = "reactorProjects", readonly = true, required = true )
72      private List<MavenProject> reactorProjects;
73  
74      /**
75       * An optional location where the results will be written to. If this is
76       * not specified the results will be written to the console.
77       */
78      @Parameter( property = "output" )
79      private File output;
80  
81      /**
82       * Directory where the site source for the project is located.
83       *
84       * @todo should be determined programmatically
85       */
86      @Parameter( property = "siteDirectory", defaultValue = "src/site" )
87      protected String siteDirectory;
88  
89      /**
90       * Sets whether this plugin is running in offline or online mode. Also
91       * useful when you don't want to verify http URLs.
92       */
93      @Parameter( property = "settings.offline" )
94      private boolean offline;
95  
96      /**
97       * The current user system settings for use in Maven.
98       */
99      @Parameter( defaultValue = "${settings}", readonly = true, required = true )
100     private Settings settings;
101 
102     private HttpClient httpClient;
103 
104     private FileSetManager fileSetManager = new FileSetManager();
105 
106     private List<String> validUrls = new ArrayList<String>();
107 
108     protected AbstractCheckDocumentationMojo()
109     {
110         String httpUserAgent = "maven-docck-plugin/1.x" + " (Java " + System.getProperty( "java.version" ) + "; "
111                 + System.getProperty( "os.name" ) + " " + System.getProperty( "os.version" ) + ")";
112 
113         httpClient = new HttpClient();
114 
115         final int connectionTimeout = 5000;
116         httpClient.getHttpConnectionManager().getParams().setConnectionTimeout( connectionTimeout );
117         httpClient.getParams().setParameter( HttpMethodParams.USER_AGENT, httpUserAgent );
118     }
119 
120     protected List<MavenProject> getReactorProjects()
121     {
122         return reactorProjects;
123     }
124 
125     public void execute()
126         throws MojoExecutionException, MojoFailureException
127     {
128         setupProxy();
129 
130         if ( output != null )
131         {
132             getLog().info( "Writing documentation check results to: " + output );
133         }
134 
135         Map<MavenProject, DocumentationReporter> reporters = new LinkedHashMap<MavenProject, DocumentationReporter>();
136         boolean hasErrors = false;
137 
138         for ( MavenProject project : reactorProjects )
139         {
140             if ( approveProjectPackaging( project.getPackaging() ) )
141             {
142                 getLog().info( "Checking project: " + project.getName() );
143 
144                 DocumentationReporter reporter = new DocumentationReporter();
145 
146                 checkProject( project, reporter );
147 
148                 if ( !hasErrors && reporter.hasErrors() )
149                 {
150                     hasErrors = true;
151                 }
152 
153                 reporters.put( project, reporter );
154             }
155             else
156             {
157                 getLog().info( "Skipping unsupported project: " + project.getName() );
158             }
159         }
160 
161         String messages;
162 
163         messages = buildErrorMessages( reporters );
164 
165         if ( !hasErrors )
166         {
167             messages += "No documentation errors were found.";
168         }
169 
170         try
171         {
172             writeMessages( messages, hasErrors );
173         }
174         catch ( IOException e )
175         {
176             throw new MojoExecutionException( "Error writing results to output file: " + output );
177         }
178 
179         if ( hasErrors )
180         {
181             String logLocation;
182             if ( output == null )
183             {
184                 logLocation = "Please see the console output above for more information.";
185             }
186             else
187             {
188                 logLocation = "Please see \'" + output + "\' for more information.";
189             }
190 
191             throw new MojoFailureException( "Documentation problems were found. " + logLocation );
192         }
193     }
194 
195     /**
196      * Setup proxy access if needed.
197      */
198     private void setupProxy()
199     {
200         Proxy settingsProxy = settings.getActiveProxy();
201 
202         if ( settingsProxy != null )
203         {
204             String proxyUsername = settingsProxy.getUsername();
205 
206             String proxyPassword = settingsProxy.getPassword();
207 
208             String proxyHost = settingsProxy.getHost();
209 
210             int proxyPort = settingsProxy.getPort();
211 
212             if ( StringUtils.isNotEmpty( proxyHost ) )
213             {
214                 httpClient.getHostConfiguration().setProxy( proxyHost, proxyPort );
215 
216                 getLog().info( "Using proxy [" + proxyHost + "] at port [" + proxyPort + "]." );
217 
218                 if ( StringUtils.isNotEmpty( proxyUsername ) )
219                 {
220                     getLog().info( "Using proxy user [" + proxyUsername + "]." );
221 
222                     Credentials creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword );
223 
224                     httpClient.getState().setProxyCredentials( new AuthScope( proxyHost, proxyPort ), creds );
225                     httpClient.getParams().setAuthenticationPreemptive( true );
226                 }
227             }
228         }
229     }
230 
231     private String buildErrorMessages( Map<MavenProject, DocumentationReporter> reporters )
232     {
233         String messages = "";
234         StringBuilder buffer = new StringBuilder();
235 
236         for ( Map.Entry<MavenProject, DocumentationReporter> entry : reporters.entrySet() )
237         {
238             MavenProject project = entry.getKey();
239             DocumentationReporter reporter = entry.getValue();
240 
241             if ( !reporter.getMessages().isEmpty() )
242             {
243                 buffer.append( "\no " ).append( project.getName() );
244                 buffer.append( " (" );
245                 final int numberOfErrors = reporter.getMessagesByType( DocumentationReport.TYPE_ERROR ).size();
246                 buffer.append( numberOfErrors ).append( " error" ).append( numberOfErrors == 1 ? "" : "s" );
247                 buffer.append( ", " );
248                 final int numberOfWarnings = reporter.getMessagesByType( DocumentationReport.TYPE_WARN ).size();
249                 buffer.append( numberOfWarnings ).append( " warning" ).append( numberOfWarnings == 1 ? "" : "s" );
250                 buffer.append( ")" );
251                 buffer.append( "\n" );
252 
253                 for ( String error : reporter.getMessages() )
254                 {
255                     buffer.append( "  " ).append( error ).append( "\n" );
256                 }
257             }
258         }
259 
260         if ( buffer.length() > 0 )
261         {
262             messages = "The following documentation problems were found:\n" + buffer.toString();
263         }
264 
265         return messages;
266     }
267 
268     protected abstract boolean approveProjectPackaging( String packaging );
269 
270     /**
271      * Writes the text in messages either to a file or to the console.
272      *
273      * @param messages The message text
274      * @param hasErrors If there were any documentation errors
275      * @throws IOException
276      */
277     private void writeMessages( String messages, boolean hasErrors )
278         throws IOException
279     {
280         if ( output != null )
281         {
282             FileWriter writer = null;
283 
284             try
285             {
286                 writer = new FileWriter( output );
287                 writer.write( messages );
288                 writer.flush();
289             }
290             finally
291             {
292                 IOUtil.close( writer );
293             }
294         }
295         else
296         {
297             if ( hasErrors )
298             {
299                 getLog().error( messages );
300             }
301             else
302             {
303                 getLog().info( messages );
304             }
305         }
306     }
307 
308     private void checkProject( MavenProject project, DocumentationReporter reporter )
309     {
310         checkPomRequirements( project, reporter );
311 
312         checkPackagingSpecificDocumentation( project, reporter );
313     }
314 
315     private void checkPomRequirements( MavenProject project, DocumentationReporter reporter )
316     {
317         checkProjectLicenses( project, reporter );
318 
319         if ( StringUtils.isEmpty( project.getName() ) )
320         {
321             reporter.error( "pom.xml is missing the <name> tag." );
322         }
323 
324         if ( StringUtils.isEmpty( project.getDescription() ) )
325         {
326             reporter.error( "pom.xml is missing the <description> tag." );
327         }
328 
329         if ( StringUtils.isEmpty( project.getUrl() ) )
330         {
331             reporter.error( "pom.xml is missing the <url> tag." );
332         }
333         else
334         {
335             checkURL( project.getUrl(), "project site", reporter );
336         }
337 
338         if ( project.getIssueManagement() == null )
339         {
340             reporter.error( "pom.xml is missing the <issueManagement> tag." );
341         }
342         else
343         {
344             IssueManagement issueMngt = project.getIssueManagement();
345             if ( StringUtils.isEmpty( issueMngt.getUrl() ) )
346             {
347                 reporter.error( "pom.xml is missing the <url> tag in <issueManagement>." );
348             }
349             else
350             {
351                 checkURL( issueMngt.getUrl(), "Issue Management", reporter );
352             }
353         }
354 
355         if ( project.getPrerequisites() == null )
356         {
357             reporter.error( "pom.xml is missing the <prerequisites> tag." );
358         }
359         else
360         {
361             Prerequisites prereq = project.getPrerequisites();
362             if ( StringUtils.isEmpty( prereq.getMaven() ) )
363             {
364                 reporter.error( "pom.xml is missing the <prerequisites>/<maven> tag." );
365             }
366         }
367 
368         if ( StringUtils.isEmpty( project.getInceptionYear() ) )
369         {
370             reporter.error( "pom.xml is missing the <inceptionYear> tag." );
371         }
372 
373         if ( project.getMailingLists().size() == 0 )
374         {
375             reporter.warn( "pom.xml has no <mailingLists>/<mailingList> specified." );
376         }
377 
378         if ( project.getScm() == null )
379         {
380             reporter.warn( "pom.xml is missing the <scm> tag." );
381         }
382         else
383         {
384             Scm scm = project.getScm();
385             if ( StringUtils.isEmpty( scm.getConnection() ) && StringUtils.isEmpty( scm.getDeveloperConnection() )
386                 && StringUtils.isEmpty( scm.getUrl() ) )
387             {
388                 reporter.warn( "pom.xml is missing the child tags under the <scm> tag." );
389             }
390             else if ( scm.getUrl() != null )
391             {
392                 checkURL( scm.getUrl(), "scm", reporter );
393             }
394         }
395 
396         if ( project.getOrganization() == null )
397         {
398             reporter.error( "pom.xml is missing the <organization> tag." );
399         }
400         else
401         {
402             Organization org = project.getOrganization();
403             if ( StringUtils.isEmpty( org.getName() ) )
404             {
405                 reporter.error( "pom.xml is missing the <organization>/<name> tag." );
406             }
407             else if ( org.getUrl() != null )
408             {
409                 checkURL( org.getUrl(), org.getName() + " site", reporter );
410             }
411         }
412     }
413 
414     private void checkProjectLicenses( MavenProject project, DocumentationReporter reporter )
415     {
416         @SuppressWarnings( "unchecked" )
417         List<License> licenses = project.getLicenses();
418 
419         if ( licenses == null || licenses.isEmpty() )
420         {
421             reporter.error( "pom.xml has no <licenses>/<license> specified." );
422         }
423         else
424         {
425             for ( License license : licenses )
426             {
427                 if ( StringUtils.isEmpty( license.getName() ) )
428                 {
429                     reporter.error( "pom.xml is missing the <licenses>/<license>/<name> tag." );
430                 }
431                 else
432                 {
433                     String url = license.getUrl();
434                     if ( StringUtils.isEmpty( url ) )
435                     {
436                         reporter.error( "pom.xml is missing the <licenses>/<license>/<url> tag for the license \'"
437                             + license.getName() + "\'." );
438                     }
439                     else
440                     {
441                         checkURL( url, "license \'" + license.getName() + "\'", reporter );
442                     }
443                 }
444             }
445         }
446     }
447 
448     private String getURLProtocol( String url )
449         throws MalformedURLException
450     {
451         URL licenseUrl = new URL( url );
452         String protocol = licenseUrl.getProtocol();
453 
454         if ( protocol != null )
455         {
456             protocol = protocol.toLowerCase();
457         }
458 
459         return protocol;
460     }
461 
462     private void checkURL( String url, String description, DocumentationReporter reporter )
463     {
464         try
465         {
466             String protocol = getURLProtocol( url );
467 
468             if ( protocol.startsWith( "http" ) )
469             {
470                 if ( offline )
471                 {
472                     reporter.warn( "Cannot verify " + description + " in offline mode with URL: \'" + url + "\'." );
473                 }
474                 else if ( !validUrls.contains( url ) )
475                 {
476                     HeadMethod headMethod = new HeadMethod( url );
477                     headMethod.setFollowRedirects( true );
478                     headMethod.setDoAuthentication( false );
479 
480                     try
481                     {
482                         getLog().debug( "Verifying http url: " + url );
483                         if ( httpClient.executeMethod( headMethod ) != HTTP_STATUS_200 )
484                         {
485                             reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'." );
486                         }
487                         else
488                         {
489                             validUrls.add( url );
490                         }
491                     }
492                     catch ( HttpException e )
493                     {
494                         reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'.\nError: "
495                             + e.getMessage() );
496                     }
497                     catch ( IOException e )
498                     {
499                         reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'.\nError: "
500                             + e.getMessage() );
501                     }
502                     finally
503                     {
504                         headMethod.releaseConnection();
505                     }
506                 }
507             }
508             else
509             {
510                 reporter.warn( "Non-HTTP " + description + " URL not verified." );
511             }
512         }
513         catch ( MalformedURLException e )
514         {
515             reporter.warn( "The " + description + " appears to have an invalid URL \'" + url + "\'."
516                 + " Message: \'" + e.getMessage() + "\'. Trying to access it as a file instead." );
517 
518             checkFile( url, description, reporter );
519         }
520     }
521 
522     private void checkFile( String url, String description, DocumentationReporter reporter )
523     {
524         File licenseFile = new File( url );
525         if ( !licenseFile.exists() )
526         {
527             reporter.error( "The " + description + " in file \'" + licenseFile.getPath() + "\' does not exist." );
528         }
529     }
530 
531     protected abstract void checkPackagingSpecificDocumentation( MavenProject project, DocumentationReporter reporter );
532 
533     protected boolean findFiles( File siteDirectory, String pattern )
534     {
535         FileSet fs = new FileSet();
536         fs.setDirectory( siteDirectory.getAbsolutePath() );
537         fs.setFollowSymlinks( false );
538 
539         fs.addInclude( "apt/" + pattern + ".apt" );
540         fs.addInclude( "apt/" + pattern + ".apt.vm" );
541         fs.addInclude( "xdoc/" + pattern + ".xml" );
542         fs.addInclude( "xdoc/" + pattern + ".xml.vm" );
543         fs.addInclude( "fml/" + pattern + ".fml" );
544         fs.addInclude( "fml/" + pattern + ".fml.vm" );
545         fs.addInclude( "resources/" + pattern + ".html" );
546         fs.addInclude( "resources/" + pattern + ".html.vm" );
547 
548         String[] includedFiles = fileSetManager.getIncludedFiles( fs );
549 
550         return includedFiles != null && includedFiles.length > 0;
551     }
552 }