All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.plugins.pmd.CpdReport Maven / Gradle / Ivy

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.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
 * Finding duplicated code
 * for more details.
 *
 * @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 java,
     * javascript or jsp.
     *
     * @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
     * foo=42; and foo=43; 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 ignoreLiterals 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;
        }

        setupPmdLogging();

        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 filterMatches( Iterator matches )
    {
        getLog().debug( "Filtering duplications. Using " + excludeDuplicationsFromFile.countExclusions()
            + " configured exclusions." );

        List 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 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 );
            writer.flush();

            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;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy