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.commons.httpclient.Credentials; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.httpclient.params.HttpMethodParams; 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.IOUtil; 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 HttpClient httpClient; private FileSetManager fileSetManager = new FileSetManager(); private List<String> validUrls = new ArrayList<String>(); protected AbstractCheckDocumentationMojo() { String httpUserAgent = "maven-docck-plugin/1.x" + " (Java " + System.getProperty( "java.version" ) + "; " + System.getProperty( "os.name" ) + " " + System.getProperty( "os.version" ) + ")"; httpClient = new HttpClient(); final int connectionTimeout = 5000; httpClient.getHttpConnectionManager().getParams().setConnectionTimeout( connectionTimeout ); httpClient.getParams().setParameter( HttpMethodParams.USER_AGENT, httpUserAgent ); } protected List<MavenProject> getReactorProjects() { return reactorProjects; } public void execute() throws MojoExecutionException, MojoFailureException { setupProxy(); if ( output != null ) { getLog().info( "Writing documentation check results to: " + output ); } Map<MavenProject, DocumentationReporter> reporters = new LinkedHashMap<MavenProject, DocumentationReporter>(); 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. */ private void setupProxy() { 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 ) ) { httpClient.getHostConfiguration().setProxy( proxyHost, proxyPort ); getLog().info( "Using proxy [" + proxyHost + "] at port [" + proxyPort + "]." ); if ( StringUtils.isNotEmpty( proxyUsername ) ) { getLog().info( "Using proxy user [" + proxyUsername + "]." ); Credentials creds = new UsernamePasswordCredentials( proxyUsername, proxyPassword ); httpClient.getState().setProxyCredentials( new AuthScope( proxyHost, proxyPort ), creds ); httpClient.getParams().setAuthenticationPreemptive( true ); } } } } 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( "\no " ).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( "\n" ); for ( String error : reporter.getMessages() ) { buffer.append( " " ).append( error ).append( "\n" ); } } } if ( buffer.length() > 0 ) { messages = "The following documentation problems were found:\n" + 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 ) { FileWriter writer = null; try { writer = new FileWriter( output ); writer.write( messages ); writer.close(); writer = null; } finally { IOUtil.close( writer ); } } 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 ) ) { HeadMethod headMethod = new HeadMethod( url ); headMethod.setFollowRedirects( true ); headMethod.setDoAuthentication( false ); try { getLog().debug( "Verifying http url: " + url ); if ( httpClient.executeMethod( headMethod ) != HTTP_STATUS_200 ) { reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'." ); } else { validUrls.add( url ); } } catch ( HttpException e ) { reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'.\nError: " + e.getMessage() ); } catch ( IOException e ) { reporter.error( "Cannot reach " + description + " with URL: \'" + url + "\'.\nError: " + e.getMessage() ); } finally { headMethod.releaseConnection(); } } } 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; } }