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