blob: a0943631aff146c10c346c14f8413afebafe67e3 [file] [log] [blame]
package org.apache.maven.plugins.pmd;
/*
* 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 java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.pmd.model.ProcessingError;
import org.apache.maven.plugins.pmd.model.Violation;
import org.codehaus.plexus.util.StringUtils;
import net.sourceforge.pmd.RulePriority;
/**
* Render the PMD violations into Doxia events.
*
* @author Brett Porter
* @version $Id$
*/
public class PmdReportGenerator
{
private Log log;
private Sink sink;
private String currentFilename;
private ResourceBundle bundle;
private Set<Violation> violations = new HashSet<>();
private List<ProcessingError> processingErrors = new ArrayList<>();
private boolean aggregate;
private boolean renderRuleViolationPriority;
private boolean renderViolationsByPriority;
private Map<File, PmdFileInfo> files;
// private List<Metric> metrics = new ArrayList<Metric>();
public PmdReportGenerator( Log log, Sink sink, ResourceBundle bundle, boolean aggregate )
{
this.log = log;
this.sink = sink;
this.bundle = bundle;
this.aggregate = aggregate;
}
private String getTitle()
{
return bundle.getString( "report.pmd.title" );
}
public void setViolations( Collection<Violation> violations )
{
this.violations = new HashSet<>( violations );
}
public List<Violation> getViolations()
{
return new ArrayList<>( violations );
}
public void setProcessingErrors( Collection<ProcessingError> errors )
{
this.processingErrors = new ArrayList<>( errors );
}
public List<ProcessingError> getProcessingErrors()
{
return processingErrors;
}
// public List<Metric> getMetrics()
// {
// return metrics;
// }
//
// public void setMetrics( List<Metric> metrics )
// {
// this.metrics = metrics;
// }
private String shortenFilename( String filename, PmdFileInfo fileInfo )
{
String result = filename;
if ( fileInfo != null && fileInfo.getSourceDirectory() != null )
{
result = StringUtils.substring( result, fileInfo.getSourceDirectory().getAbsolutePath().length() + 1 );
}
return StringUtils.replace( result, "\\", "/" );
}
private String makeFileSectionName( String filename, PmdFileInfo fileInfo )
{
if ( aggregate && fileInfo != null && fileInfo.getProject() != null )
{
return fileInfo.getProject().getName() + " - " + filename;
}
return filename;
}
private PmdFileInfo determineFileInfo( String filename )
throws IOException
{
File canonicalFilename = new File( filename ).getCanonicalFile();
PmdFileInfo fileInfo = files.get( canonicalFilename );
if ( fileInfo == null )
{
log.warn( "Couldn't determine PmdFileInfo for file " + filename + " (canonical: " + canonicalFilename
+ "). XRef links won't be available." );
}
return fileInfo;
}
private void startFileSection( int level, String currentFilename, PmdFileInfo fileInfo )
{
sink.section( level, null );
sink.sectionTitle( level, null );
// prepare the filename
this.currentFilename = shortenFilename( currentFilename, fileInfo );
sink.text( makeFileSectionName( this.currentFilename, fileInfo ) );
sink.sectionTitle_( level );
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.pmd.column.rule" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.pmd.column.violation" ) );
sink.tableHeaderCell_();
if ( this.renderRuleViolationPriority )
{
sink.tableHeaderCell();
sink.text( bundle.getString( "report.pmd.column.priority" ) );
sink.tableHeaderCell_();
}
sink.tableHeaderCell();
sink.text( bundle.getString( "report.pmd.column.line" ) );
sink.tableHeaderCell_();
sink.tableRow_();
}
private void endFileSection( int level )
{
sink.table_();
sink.section_( level );
}
private void addRuleName( Violation ruleViolation )
{
boolean hasUrl = StringUtils.isNotBlank( ruleViolation.getExternalInfoUrl() );
if ( hasUrl )
{
sink.link( ruleViolation.getExternalInfoUrl() );
}
sink.text( ruleViolation.getRule() );
if ( hasUrl )
{
sink.link_();
}
}
private void processSingleRuleViolation( Violation ruleViolation, PmdFileInfo fileInfo )
{
sink.tableRow();
sink.tableCell();
addRuleName( ruleViolation );
sink.tableCell_();
sink.tableCell();
sink.text( ruleViolation.getText() );
sink.tableCell_();
if ( this.renderRuleViolationPriority )
{
sink.tableCell();
sink.text( String.valueOf( RulePriority.valueOf( ruleViolation.getPriority() ).getPriority() ) );
sink.tableCell_();
}
sink.tableCell();
int beginLine = ruleViolation.getBeginline();
outputLineLink( beginLine, fileInfo );
int endLine = ruleViolation.getEndline();
if ( endLine != beginLine )
{
sink.text( "&#x2013;" ); // \u2013 is a medium long dash character
outputLineLink( endLine, fileInfo );
}
sink.tableCell_();
sink.tableRow_();
}
// PMD might run the analysis multi-threaded, so the violations might be reported
// out of order. We sort them here by filename and line number before writing them to
// the report.
private void renderViolations()
throws IOException
{
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.pmd.files" ) );
sink.sectionTitle1_();
// TODO files summary
List<Violation> violations2 = new ArrayList<>( violations );
renderViolationsTable( 2, violations2 );
sink.section1_();
}
private void renderViolationsByPriority() throws IOException
{
if ( !renderViolationsByPriority )
{
return;
}
boolean oldPriorityColumn = this.renderRuleViolationPriority;
this.renderRuleViolationPriority = false;
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.pmd.violationsByPriority" ) );
sink.sectionTitle1_();
Map<RulePriority, List<Violation>> violationsByPriority = new HashMap<>();
for ( Violation violation : violations )
{
RulePriority priority = RulePriority.valueOf( violation.getPriority() );
List<Violation> violationSegment = violationsByPriority.get( priority );
if ( violationSegment == null )
{
violationSegment = new ArrayList<>();
violationsByPriority.put( priority, violationSegment );
}
violationSegment.add( violation );
}
for ( RulePriority priority : RulePriority.values() )
{
List<Violation> violationsWithPriority = violationsByPriority.get( priority );
if ( violationsWithPriority == null || violationsWithPriority.isEmpty() )
{
continue;
}
sink.section2();
sink.sectionTitle2();
sink.text( bundle.getString( "report.pmd.priority" ) + " " + priority.getPriority() );
sink.sectionTitle2_();
renderViolationsTable( 3, violationsWithPriority );
sink.section2_();
}
if ( violations.isEmpty() )
{
sink.paragraph();
sink.text( bundle.getString( "report.pmd.noProblems" ) );
sink.paragraph_();
}
sink.section1_();
this.renderRuleViolationPriority = oldPriorityColumn;
}
private void renderViolationsTable( int level, List<Violation> violationSegment )
throws IOException
{
Collections.sort( violationSegment, new Comparator<Violation>()
{
/** {@inheritDoc} */
public int compare( Violation o1, Violation o2 )
{
int filenames = o1.getFileName().compareTo( o2.getFileName() );
if ( filenames == 0 )
{
return o1.getBeginline() - o2.getBeginline();
}
else
{
return filenames;
}
}
} );
boolean fileSectionStarted = false;
String previousFilename = null;
for ( Violation ruleViolation : violationSegment )
{
String currentFn = ruleViolation.getFileName();
PmdFileInfo fileInfo = determineFileInfo( currentFn );
if ( !currentFn.equalsIgnoreCase( previousFilename ) && fileSectionStarted )
{
endFileSection( level );
fileSectionStarted = false;
}
if ( !fileSectionStarted )
{
startFileSection( level, currentFn, fileInfo );
fileSectionStarted = true;
}
processSingleRuleViolation( ruleViolation, fileInfo );
previousFilename = currentFn;
}
if ( fileSectionStarted )
{
endFileSection( level );
}
}
private void outputLineLink( int line, PmdFileInfo fileInfo )
{
String xrefLocation = null;
if ( fileInfo != null )
{
xrefLocation = fileInfo.getXrefLocation();
}
if ( xrefLocation != null )
{
sink.link( xrefLocation + "/" + currentFilename.replaceAll( "\\.java$", ".html" ) + "#L" + line );
}
sink.text( String.valueOf( line ) );
if ( xrefLocation != null )
{
sink.link_();
}
}
private void processProcessingErrors() throws IOException
{
// sort the problem by filename first, since PMD is executed multi-threaded
// and might reports the results unsorted
Collections.sort( processingErrors, new Comparator<ProcessingError>()
{
@Override
public int compare( ProcessingError e1, ProcessingError e2 )
{
return e1.getFilename().compareTo( e2.getFilename() );
}
} );
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.pmd.processingErrors.title" ) );
sink.sectionTitle1_();
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.pmd.processingErrors.column.filename" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.pmd.processingErrors.column.problem" ) );
sink.tableHeaderCell_();
sink.tableRow_();
for ( ProcessingError error : processingErrors )
{
processSingleProcessingError( error );
}
sink.table_();
sink.section1_();
}
private void processSingleProcessingError( ProcessingError error ) throws IOException
{
String filename = error.getFilename();
PmdFileInfo fileInfo = determineFileInfo( filename );
filename = makeFileSectionName( shortenFilename( filename, fileInfo ), fileInfo );
sink.tableRow();
sink.tableCell();
sink.text( filename );
sink.tableCell_();
sink.tableCell();
sink.text( error.getMsg() );
sink.verbatim( null );
sink.rawText( error.getDetail() );
sink.verbatim_();
sink.tableCell_();
sink.tableRow_();
}
public void beginDocument()
{
sink.head();
sink.title();
sink.text( getTitle() );
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.sectionTitle1();
sink.text( getTitle() );
sink.sectionTitle1_();
sink.paragraph();
sink.text( bundle.getString( "report.pmd.pmdlink" ) + " " );
sink.link( "https://pmd.github.io" );
sink.text( "PMD" );
sink.link_();
sink.text( " " + AbstractPmdReport.getPmdVersion() + "." );
sink.paragraph_();
sink.section1_();
// TODO overall summary
}
/*
* private void processMetrics() { if ( metrics.size() == 0 ) { return; } sink.section1(); sink.sectionTitle1();
* sink.text( "Metrics" ); sink.sectionTitle1_(); sink.table(); sink.tableRow(); sink.tableHeaderCell(); sink.text(
* "Name" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Count" ); sink.tableHeaderCell_();
* sink.tableHeaderCell(); sink.text( "High" ); sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Low" );
* sink.tableHeaderCell_(); sink.tableHeaderCell(); sink.text( "Average" ); sink.tableHeaderCell_();
* sink.tableRow_(); for ( Metric met : metrics ) { sink.tableRow(); sink.tableCell(); sink.text(
* met.getMetricName() ); sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getCount() ) );
* sink.tableCell_(); sink.tableCell(); sink.text( String.valueOf( met.getHighValue() ) ); sink.tableCell_();
* sink.tableCell(); sink.text( String.valueOf( met.getLowValue() ) ); sink.tableCell_(); sink.tableCell();
* sink.text( String.valueOf( met.getAverage() ) ); sink.tableCell_(); sink.tableRow_(); } sink.table_();
* sink.section1_(); }
*/
public void render()
throws IOException
{
if ( !violations.isEmpty() )
{
renderViolationsByPriority();
renderViolations();
}
else
{
sink.paragraph();
sink.text( bundle.getString( "report.pmd.noProblems" ) );
sink.paragraph_();
}
if ( !processingErrors.isEmpty() )
{
processProcessingErrors();
}
}
public void endDocument()
throws IOException
{
// The Metrics report useless with the current PMD metrics impl.
// For instance, run the coupling ruleset and you will get a boatload
// of excessive imports metrics, none of which is really any use.
// TODO Determine if we are going to just ignore metrics.
// processMetrics();
sink.body_();
sink.flush();
sink.close();
}
public void setFiles( Map<File, PmdFileInfo> files )
{
this.files = files;
}
public void setRenderRuleViolationPriority( boolean renderRuleViolationPriority )
{
this.renderRuleViolationPriority = renderRuleViolationPriority;
}
public void setRenderViolationsByPriority( boolean renderViolationsByPriority )
{
this.renderViolationsByPriority = renderViolationsByPriority;
}
}