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