1 package org.apache.maven.plugin.docck;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
68
69
70
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
84
85
86 @Parameter( property = "output" )
87 private File output;
88
89
90
91
92 @Parameter( property = "siteDirectory", defaultValue = "src/site" )
93 protected String siteDirectory;
94
95
96
97
98
99 @Parameter( property = "settings.offline" )
100 private boolean offline;
101
102
103
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
205
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
283
284
285
286
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 }