package org.apache.maven.plugin.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.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.ResourceBundle; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.reporting.MavenReportException; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.util.WriterFactory; import net.sourceforge.pmd.cpd.CPD; import net.sourceforge.pmd.cpd.CPDConfiguration; import net.sourceforge.pmd.cpd.CSVRenderer; import net.sourceforge.pmd.cpd.EcmascriptLanguage; import net.sourceforge.pmd.cpd.JSPLanguage; import net.sourceforge.pmd.cpd.JavaLanguage; import net.sourceforge.pmd.cpd.JavaTokenizer; import net.sourceforge.pmd.cpd.Language; import net.sourceforge.pmd.cpd.LanguageFactory; import net.sourceforge.pmd.cpd.Match; import net.sourceforge.pmd.cpd.Renderer; import net.sourceforge.pmd.cpd.XMLRenderer; /** * Creates a report for PMD's CPD tool. See <a * href="http://pmd.sourceforge.net/cpd.html">http://pmd.sourceforge.net/cpd.html</a> for more detail. * * @author Mike Perham * @version $Id$ * @since 2.0 */ @Mojo( name = "cpd", threadSafe = true ) public class CpdReport extends AbstractPmdReport { /** * The programming language to be analyzed by CPD. Valid values are currently <code>java</code>, * <code>javascript</code> or <code>jsp</code>. * * @since 3.5 */ @Parameter( defaultValue = "java" ) private String language; /** * The minimum number of tokens that need to be duplicated before it causes a violation. */ @Parameter( property = "minimumTokens", defaultValue = "100" ) private int minimumTokens; /** * Skip the CPD report generation. Most useful on the command line via "-Dcpd.skip=true". * * @since 2.1 */ @Parameter( property = "cpd.skip", defaultValue = "false" ) private boolean skip; /** * If true, CPD ignores literal value differences when evaluating a duplicate block. This means that * <code>foo=42;</code> and <code>foo=43;</code> will be seen as equivalent. You may want to run PMD with this * option off to start with and then switch it on to see what it turns up. * * @since 2.5 */ @Parameter( property = "cpd.ignoreLiterals", defaultValue = "false" ) private boolean ignoreLiterals; /** * Similar to <code>ignoreLiterals</code> but for identifiers; i.e., variable names, methods names, and so forth. * * @since 2.5 */ @Parameter( property = "cpd.ignoreIdentifiers", defaultValue = "false" ) private boolean ignoreIdentifiers; /** The CPD instance used to analyze the files. Will itself collect the duplicated code matches. */ private CPD cpd; /** Helper to exclude duplications from the result. */ private final ExcludeDuplicationsFromFile excludeDuplicationsFromFile = new ExcludeDuplicationsFromFile(); /** * {@inheritDoc} */ public String getName( Locale locale ) { return getBundle( locale ).getString( "report.cpd.name" ); } /** * {@inheritDoc} */ public String getDescription( Locale locale ) { return getBundle( locale ).getString( "report.cpd.description" ); } /** * {@inheritDoc} */ @Override public void executeReport( Locale locale ) throws MavenReportException { try { execute( locale ); } finally { if ( getSink() != null ) { getSink().close(); } } } private void execute( Locale locale ) throws MavenReportException { if ( !skip && canGenerateReport() ) { ClassLoader origLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() ); generateReport( locale ); if ( !isHtml() && !isXml() ) { writeNonHtml( cpd ); } } finally { Thread.currentThread().setContextClassLoader( origLoader ); } } } @Override public boolean canGenerateReport() { if ( skip ) { return false; } boolean result = super.canGenerateReport(); if ( result ) { try { executeCpdWithClassloader(); if ( skipEmptyReport ) { result = cpd.getMatches().hasNext(); if ( result ) { getLog().debug( "Skipping report since skipEmptyReport is true and there are no CPD issues." ); } } } catch ( MavenReportException e ) { throw new RuntimeException( e ); } } return result; } private void executeCpdWithClassloader() throws MavenReportException { ClassLoader origLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader( this.getClass().getClassLoader() ); executeCpd(); } finally { Thread.currentThread().setContextClassLoader( origLoader ); } } private void executeCpd() throws MavenReportException { if ( cpd != null ) { // CPD has already been run getLog().debug( "CPD has already been run - skipping redundant execution." ); return; } Properties p = new Properties(); if ( ignoreLiterals ) { p.setProperty( JavaTokenizer.IGNORE_LITERALS, "true" ); } if ( ignoreIdentifiers ) { p.setProperty( JavaTokenizer.IGNORE_IDENTIFIERS, "true" ); } try { if ( filesToProcess == null ) { filesToProcess = getFilesToProcess(); } try { excludeDuplicationsFromFile.loadExcludeFromFailuresData( excludeFromFailureFile ); } catch ( MojoExecutionException e ) { throw new MavenReportException( "Error loading exclusions", e ); } String encoding = determineEncoding( !filesToProcess.isEmpty() ); Language cpdLanguage; if ( "java".equals ( language ) || null == language ) { cpdLanguage = new JavaLanguage( p ); } else if ( "javascript".equals( language ) ) { cpdLanguage = new EcmascriptLanguage(); } else if ( "jsp".equals( language ) ) { cpdLanguage = new JSPLanguage(); } else { cpdLanguage = LanguageFactory.createLanguage( language, p ); } CPDConfiguration cpdConfiguration = new CPDConfiguration(); cpdConfiguration.setMinimumTileSize( minimumTokens ); cpdConfiguration.setLanguage( cpdLanguage ); cpdConfiguration.setSourceEncoding( encoding ); cpd = new CPD( cpdConfiguration ); for ( File file : filesToProcess.keySet() ) { cpd.add( file ); } } catch ( UnsupportedEncodingException e ) { throw new MavenReportException( "Encoding '" + getSourceEncoding() + "' is not supported.", e ); } catch ( IOException e ) { throw new MavenReportException( e.getMessage(), e ); } getLog().debug( "Executing CPD..." ); cpd.go(); getLog().debug( "CPD finished." ); // if format is XML, we need to output it even if the file list is empty or we have no duplications // so the "check" goals can check for violations if ( isXml() ) { writeNonHtml( cpd ); } } private Iterator<Match> filterMatches( Iterator<Match> matches ) { getLog().debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions() + " configured exclusions." ); List<Match> filteredMatches = new ArrayList<>(); int excludedDuplications = 0; while ( matches.hasNext() ) { Match match = matches.next(); if ( excludeDuplicationsFromFile.isExcludedFromFailure( match ) ) { excludedDuplications++; } else { filteredMatches.add( match ); } } getLog().debug( "Excluded " + excludedDuplications + " duplications." ); return filteredMatches.iterator(); } private void generateReport( Locale locale ) { CpdReportGenerator gen = new CpdReportGenerator( getSink(), filesToProcess, getBundle( locale ), aggregate ); Iterator<Match> matches = cpd.getMatches(); gen.generate( filterMatches( matches ) ); } private String determineEncoding( boolean showWarn ) throws UnsupportedEncodingException { String encoding = WriterFactory.FILE_ENCODING; if ( StringUtils.isNotEmpty( getSourceEncoding() ) ) { encoding = getSourceEncoding(); // test encoding as CPD will convert exception into a RuntimeException WriterFactory.newWriter( new ByteArrayOutputStream(), encoding ); } else if ( showWarn ) { getLog().warn( "File encoding has not been set, using platform encoding " + WriterFactory.FILE_ENCODING + ", i.e. build is platform dependent!" ); encoding = WriterFactory.FILE_ENCODING; } return encoding; } void writeNonHtml( CPD cpd ) throws MavenReportException { Renderer r = createRenderer(); if ( r == null ) { return; } String buffer = r.render( filterMatches( cpd.getMatches() ) ); File targetFile = new File( targetDirectory, "cpd." + format ); targetDirectory.mkdirs(); try ( Writer writer = new OutputStreamWriter( new FileOutputStream( targetFile ), getOutputEncoding() ) ) { writer.write( buffer ); if ( includeXmlInSite ) { File siteDir = getReportOutputDirectory(); siteDir.mkdirs(); FileUtils.copyFile( targetFile, new File( siteDir, "cpd." + format ) ); } } catch ( IOException ioe ) { throw new MavenReportException( ioe.getMessage(), ioe ); } } /** * {@inheritDoc} */ public String getOutputName() { return "cpd"; } private static ResourceBundle getBundle( Locale locale ) { return ResourceBundle.getBundle( "cpd-report", locale, CpdReport.class.getClassLoader() ); } /** * Create and return the correct renderer for the output type. * * @return the renderer based on the configured output * @throws org.apache.maven.reporting.MavenReportException if no renderer found for the output type */ public Renderer createRenderer() throws MavenReportException { Renderer renderer = null; if ( "xml".equals( format ) ) { renderer = new XMLRenderer( getOutputEncoding() ); } else if ( "csv".equals( format ) ) { renderer = new CSVRenderer(); } else if ( !"".equals( format ) && !"none".equals( format ) ) { try { renderer = (Renderer) Class.forName( format ).newInstance(); } catch ( Exception e ) { throw new MavenReportException( "Can't find CPD custom format " + format + ": " + e.getClass().getName(), e ); } } return renderer; } }