blob: 81d02f5a6fb1182dfb2703a732c855f5631a972d [file] [log] [blame]
package org.apache.maven.plugin.docck;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpHead;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.util.Timeout;
import org.apache.maven.model.IssueManagement;
import org.apache.maven.model.License;
import org.apache.maven.model.Organization;
import org.apache.maven.model.Prerequisites;
import org.apache.maven.model.Scm;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.docck.reports.DocumentationReport;
import org.apache.maven.plugin.docck.reports.DocumentationReporter;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.model.fileset.FileSet;
import org.apache.maven.shared.model.fileset.util.FileSetManager;
import org.codehaus.plexus.util.StringUtils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Performs the heavy lifting for documentation checks. This is designed to be
* reused for other types of projects, too.
*
* @author jdcasey
*/
public abstract class AbstractCheckDocumentationMojo
extends AbstractMojo
{
private static final int HTTP_STATUS_200 = 200;
/**
*/
@Parameter( property = "reactorProjects", readonly = true, required = true )
private List<MavenProject> reactorProjects;
/**
* An optional location where the results will be written to. If this is
* not specified the results will be written to the console.
*/
@Parameter( property = "output" )
private File output;
/**
* Directory where the site source for the project is located.
*
* @todo should be determined programmatically
*/
@Parameter( property = "siteDirectory", defaultValue = "src/site" )
protected String siteDirectory;
/**
* Sets whether this plugin is running in offline or online mode. Also
* useful when you don't want to verify http URLs.
*/
@Parameter( property = "settings.offline" )
private boolean offline;
/**
* The current user system settings for use in Maven.
*/
@Parameter( defaultValue = "${settings}", readonly = true, required = true )
private Settings settings;
private CloseableHttpClient httpClient;
private FileSetManager fileSetManager = new FileSetManager();
private List<String> validUrls = new ArrayList<>();
protected List<MavenProject> getReactorProjects()
{
return reactorProjects;
}
@Override
public void execute()
throws MojoExecutionException, MojoFailureException
{
String httpUserAgent = "maven-docck-plugin/1.x" + " (Java " + System.getProperty( "java.version" ) + "; "
+ System.getProperty( "os.name" ) + " " + System.getProperty( "os.version" ) + ")";
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setDefaultRequestConfig( RequestConfig.custom()
.setConnectTimeout( Timeout.ofSeconds( 5 ) )
.setResponseTimeout( Timeout.ofSeconds( 5 ) )
.setCookieSpec( StandardCookieSpec.STRICT )
.build() )
.setDefaultHeaders( List.of( new BasicHeader( HttpHeaders.USER_AGENT, httpUserAgent ) ) );
setupProxy( httpClientBuilder );
httpClient = httpClientBuilder.build();
if ( output != null )
{
getLog().info( "Writing documentation check results to: " + output );
}
Map<MavenProject, DocumentationReporter> reporters = new LinkedHashMap<>();
boolean hasErrors = false;
for ( MavenProject project : reactorProjects )
{
if ( approveProjectPackaging( project.getPackaging() ) )
{
getLog().info( "Checking project: " + project.getName() );
DocumentationReporter reporter = new DocumentationReporter();
checkProject( project, reporter );
if ( !hasErrors && reporter.hasErrors() )
{
hasErrors = true;
}
reporters.put( project, reporter );
}
else
{
getLog().info( "Skipping unsupported project: " + project.getName() );
}
}
String messages;
messages = buildErrorMessages( reporters );
if ( !hasErrors )
{
messages += "No documentation errors were found.";
}
try
{
writeMessages( messages, hasErrors );
}
catch ( IOException e )
{
throw new MojoExecutionException( "Error writing results to output file: " + output );
}
if ( hasErrors )
{
String logLocation;
if ( output == null )
{
logLocation = "Please see the console output above for more information.";
}
else
{
logLocation = "Please see \'" + output + "\' for more information.";
}
throw new MojoFailureException( "Documentation problems were found. " + logLocation );
}
}
/**
* Setup proxy access if needed.
* @param httpClientBuilder
*/
private void setupProxy( HttpClientBuilder httpClientBuilder )
{
Proxy settingsProxy = settings.getActiveProxy();
if ( settingsProxy != null )
{
String proxyUsername = settingsProxy.getUsername();
String proxyPassword = settingsProxy.getPassword();
String proxyHost = settingsProxy.getHost();
int proxyPort = settingsProxy.getPort();
if ( StringUtils.isNotEmpty( proxyHost ) )
{
httpClientBuilder.setProxy( new HttpHost( proxyHost, proxyPort ) );
getLog().info( "Using proxy [" + proxyHost + "] at port [" + proxyPort + "]." );
if ( StringUtils.isNotEmpty( proxyUsername ) )
{
getLog().info( "Using proxy user [" + proxyUsername + "]." );
BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope( proxyHost, proxyPort ),
new UsernamePasswordCredentials( proxyUsername, proxyPassword.toCharArray() ) );
httpClientBuilder.setDefaultCredentialsProvider( credsProvider );
}
}
}
}
private String buildErrorMessages( Map<MavenProject, DocumentationReporter> reporters )
{
String messages = "";
StringBuilder buffer = new StringBuilder();
for ( Map.Entry<MavenProject, DocumentationReporter> entry : reporters.entrySet() )
{
MavenProject project = entry.getKey();
DocumentationReporter reporter = entry.getValue();
if ( !reporter.getMessages().isEmpty() )
{
buffer.append( System.lineSeparator() ).append( "o " ).append( project.getName() );
buffer.append( " (" );
final int numberOfErrors = reporter.getMessagesByType( DocumentationReport.TYPE_ERROR ).size();
buffer.append( numberOfErrors ).append( " error" ).append( numberOfErrors == 1 ? "" : "s" );
buffer.append( ", " );
final int numberOfWarnings = reporter.getMessagesByType( DocumentationReport.TYPE_WARN ).size();
buffer.append( numberOfWarnings ).append( " warning" ).append( numberOfWarnings == 1 ? "" : "s" );
buffer.append( ")" );
buffer.append( System.lineSeparator() );
for ( String error : reporter.getMessages() )
{
buffer.append( " " ).append( error ).append( System.lineSeparator() );
}
}
}
if ( buffer.length() > 0 )
{
messages = "The following documentation problems were found:" + System.lineSeparator() + buffer.toString();
}
return messages;
}
protected abstract boolean approveProjectPackaging( String packaging );
/**
* Writes the text in messages either to a file or to the console.
*
* @param messages The message text
* @param hasErrors If there were any documentation errors
* @throws IOException
*/
private void writeMessages( String messages, boolean hasErrors )
throws IOException
{
if ( output != null )
{
try ( FileWriter writer = new FileWriter( output ) )
{
writer.write( messages );
}
}
else
{
if ( hasErrors )
{
getLog().error( messages );
}
else
{
getLog().info( messages );
}
}
}
private void checkProject( MavenProject project, DocumentationReporter reporter )
{
checkPomRequirements( project, reporter );
checkPackagingSpecificDocumentation( project, reporter );
}
private void checkPomRequirements( MavenProject project, DocumentationReporter reporter )
{
checkProjectLicenses( project, reporter );
if ( StringUtils.isEmpty( project.getName() ) )
{
reporter.error( "pom.xml is missing the <name> tag." );
}
if ( StringUtils.isEmpty( project.getDescription() ) )
{
reporter.error( "pom.xml is missing the <description> tag." );
}
if ( StringUtils.isEmpty( project.getUrl() ) )
{
reporter.error( "pom.xml is missing the <url> tag." );
}
else
{
checkURL( project.getUrl(), "project site", reporter );
}
if ( project.getIssueManagement() == null )
{
reporter.error( "pom.xml is missing the <issueManagement> tag." );
}
else
{
IssueManagement issueMngt = project.getIssueManagement();
if ( StringUtils.isEmpty( issueMngt.getUrl() ) )
{
reporter.error( "pom.xml is missing the <url> tag in <issueManagement>." );
}
else
{
checkURL( issueMngt.getUrl(), "Issue Management", reporter );
}
}
if ( project.getPrerequisites() == null )
{
reporter.error( "pom.xml is missing the <prerequisites> tag." );
}
else
{
Prerequisites prereq = project.getPrerequisites();
if ( StringUtils.isEmpty( prereq.getMaven() ) )
{
reporter.error( "pom.xml is missing the <prerequisites>/<maven> tag." );
}
}
if ( StringUtils.isEmpty( project.getInceptionYear() ) )
{
reporter.error( "pom.xml is missing the <inceptionYear> tag." );
}
if ( project.getMailingLists().size() == 0 )
{
reporter.warn( "pom.xml has no <mailingLists>/<mailingList> specified." );
}
if ( project.getScm() == null )
{
reporter.warn( "pom.xml is missing the <scm> tag." );
}
else
{
Scm scm = project.getScm();
if ( StringUtils.isEmpty( scm.getConnection() ) && StringUtils.isEmpty( scm.getDeveloperConnection() )
&& StringUtils.isEmpty( scm.getUrl() ) )
{
reporter.warn( "pom.xml is missing the child tags under the <scm> tag." );
}
else if ( scm.getUrl() != null )
{
checkURL( scm.getUrl(), "scm", reporter );
}
}
if ( project.getOrganization() == null )
{
reporter.error( "pom.xml is missing the <organization> tag." );
}
else
{
Organization org = project.getOrganization();
if ( StringUtils.isEmpty( org.getName() ) )
{
reporter.error( "pom.xml is missing the <organization>/<name> tag." );
}
else if ( org.getUrl() != null )
{
checkURL( org.getUrl(), org.getName() + " site", reporter );
}
}
}
private void checkProjectLicenses( MavenProject project, DocumentationReporter reporter )
{
@SuppressWarnings( "unchecked" )
List<License> licenses = project.getLicenses();
if ( licenses == null || licenses.isEmpty() )
{
reporter.error( "pom.xml has no <licenses>/<license> specified." );
}
else
{
for ( License license : licenses )
{
if ( StringUtils.isEmpty( license.getName() ) )
{
reporter.error( "pom.xml is missing the <licenses>/<license>/<name> tag." );
}
else
{
String url = license.getUrl();
if ( StringUtils.isEmpty( url ) )
{
reporter.error( "pom.xml is missing the <licenses>/<license>/<url> tag for the license \'"
+ license.getName() + "\'." );
}
else
{
checkURL( url, "license \'" + license.getName() + "\'", reporter );
}
}
}
}
}
private String getURLProtocol( String url )
throws MalformedURLException
{
URL licenseUrl = new URL( url );
String protocol = licenseUrl.getProtocol();
if ( protocol != null )
{
protocol = protocol.toLowerCase();
}
return protocol;
}
private void checkURL( String url, String description, DocumentationReporter reporter )
{
try
{
String protocol = getURLProtocol( url );
if ( protocol.startsWith( "http" ) )
{
if ( offline )
{
reporter.warn( "Cannot verify " + description + " in offline mode with URL: \'" + url + "\'." );
}
else if ( !validUrls.contains( url ) )
{
HttpHead headMethod = new HttpHead( url );
try ( CloseableHttpResponse response = httpClient.execute( headMethod ) )
{
getLog().debug( "Verifying http url: " + url );
if ( response.getCode() != HTTP_STATUS_200 )
{
reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'." );
}
else
{
validUrls.add( url );
}
}
catch ( IOException e )
{
reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'.\nError: "
+ e.getMessage() );
}
}
}
else
{
reporter.warn( "Non-HTTP " + description + " URL not verified." );
}
}
catch ( MalformedURLException e )
{
reporter.warn( "The " + description + " appears to have an invalid URL \'" + url + "\'."
+ " Message: \'" + e.getMessage() + "\'. Trying to access it as a file instead." );
checkFile( url, description, reporter );
}
}
private void checkFile( String url, String description, DocumentationReporter reporter )
{
File licenseFile = new File( url );
if ( !licenseFile.exists() )
{
reporter.error( "The " + description + " in file \'" + licenseFile.getPath() + "\' does not exist." );
}
}
protected abstract void checkPackagingSpecificDocumentation( MavenProject project, DocumentationReporter reporter );
protected boolean findFiles( File siteDirectory, String pattern )
{
FileSet fs = new FileSet();
fs.setDirectory( siteDirectory.getAbsolutePath() );
fs.setFollowSymlinks( false );
fs.addInclude( "apt/" + pattern + ".apt" );
fs.addInclude( "apt/" + pattern + ".apt.vm" );
fs.addInclude( "xdoc/" + pattern + ".xml" );
fs.addInclude( "xdoc/" + pattern + ".xml.vm" );
fs.addInclude( "fml/" + pattern + ".fml" );
fs.addInclude( "fml/" + pattern + ".fml.vm" );
fs.addInclude( "resources/" + pattern + ".html" );
fs.addInclude( "resources/" + pattern + ".html.vm" );
String[] includedFiles = fileSetManager.getIncludedFiles( fs );
return includedFiles != null && includedFiles.length > 0;
}
}