package org.codehaus.mojo.simian; import java.io.File; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import org.apache.maven.artifact.handler.ArtifactHandler; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.AbstractMavenReport; import org.apache.maven.reporting.MavenReportException; import org.codehaus.doxia.sink.Sink; import org.codehaus.doxia.site.renderer.SiteRenderer; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.StringUtils; import au.com.redhillconsulting.simian.Checker; import au.com.redhillconsulting.simian.FileLoader; import au.com.redhillconsulting.simian.Option; import au.com.redhillconsulting.simian.Options; import au.com.redhillconsulting.simian.SourceFile; /** * Implement the Simian report. * * @author Miguel Griffa * @version $Id$ * @goal simian * @description Runs the simian tool on the project sources * @todo needs to support the multiple source roots (based on pmd plugin) */ public class SimianReportMojo extends AbstractMavenReport { /** * @parameter expression="${project.build.sourceDirectory}" * @required * @readonly */ private String sourceDirectory; /** * @parameter expression="${project.build.testSourceDirectory}" * @required * @readonly */ private String testDirectory; /** * @parameter expression="${project.reporting.outputDirectory}" * @required */ private String outputDirectory; /** * @component */ private SiteRenderer siteRenderer; /** * @parameter expression="${project}" * @required * @readonly */ private MavenProject project; // Simian options /** * Matches will contain at least the specified number of lines * @parameter default-value="6" * @optional */ private int minimumThreshold = 6; /** * MyVariable and myvariable would both match * @parameter * @optional */ private boolean ignoreStrings; /** * Completely ignores all identfiers * @parameter * @optional */ private boolean ignoreIdentifiers; /** * Completely ignores variable names (field, parameter and local). Eg. int foo = 1; and int bar = 1 would both match * @parameter * @optional */ private boolean ignoreVariableNames; /** * int x = 1; and int x = 576; would both match * @parameter * @optional */ private boolean ignoreNumbers; /** * 'A', "one" and 27.8would all match * @parameter * @optional */ private boolean ignoreLiterals; /** * public, protected, static, etc. * @parameter * @optional */ private boolean ignoreModifiers; /** * Link to jxr plugin report? If this option is set to <code>true</code>, links are added to jxr plugin * @parameter default-value="true" * @optional */ private boolean linkToJxr = true; private Locale locale; /** * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale) */ public String getName( final Locale locale ) { return getBundle( locale ).getString( "report.simian.name" ); } /** * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale) */ public String getDescription( final Locale locale ) { return getBundle( locale ).getString( "report.simian.description" ); } /** * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory() */ protected String getOutputDirectory() { return outputDirectory; } /** * @see org.apache.maven.reporting.AbstractMavenReport#getProject() */ protected MavenProject getProject() { return project; } /** * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer() */ protected SiteRenderer getSiteRenderer() { return siteRenderer; } /** * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale) */ public void executeReport( final Locale locale ) throws MavenReportException { this.locale = locale; final Sink sink = getSink(); // final SimianAuditListener listener = new SimianAuditListener(); listener.setLog( getLog() ); final Options options = getOptions(); final Checker checker = new Checker( listener, options ); final FileLoader loader = new FileLoader( checker ); List files = null; try { files = getFilesToProcess( "**/*.java", null ); } catch ( IOException e ) { throw new MavenReportException( "error in getFilesToProcess", e ); } for ( Iterator i = files.iterator(); i.hasNext(); ) { final File file = (File) i.next(); try { loader.load( file ); } catch ( IOException e ) { throw new MavenReportException( "error loading " + file, e ); } } if ( checker.check() ) { getLog().debug( "Duplicate lines were found!" ); } sink.body(); summarySection( sink, listener ); sink.section1(); sink.sectionTitle1(); sink.text( getBundle( locale ).getString( "report.simian.duplications" ) ); sink.sectionTitle1_(); Record[] records = listener.getRecords(); List recordsList = Arrays.asList( records ); Collections.sort( recordsList, new RecordSizeComparator() ); Record[] blocksInfo = getDifferentBlockIds( records ); for ( int i = 0; i < blocksInfo.length; i++ ) { sink.section2(); sink.sectionTitle2(); sink.text( MessageFormat.format( getBundle( locale ).getString( "report.simian.duplications.subsection" ), new Integer[] { new Integer( blocksInfo[i].getBlockSize() ) } ) ); sink.sectionTitle2_(); sink.list(); Record[] bsRecords = getRecordsForBlockId( records, blocksInfo[i].getBlockId() ); for ( int j = 0; j < bsRecords.length; j++ ) { Record r = bsRecords[j]; sink.listItem(); if ( linkToJxr ) sink.link( getLink( r ) + "#" + r.getStartLine() ); sink.text( getSourceFilenameWithoutBasedir( r ) ); sink.text( " ( " + r.getStartLine() + " - " + r.getEndLine() + " ) " ); sink.listItem_(); } if ( linkToJxr ) sink.link_(); sink.list(); sink.section2_(); } sink.section1_(); sink.body_(); sink.close(); getLog().info( "Simian report done in " + listener.getElapsed() + " ms" ); } private Record[] getRecordsForBlockId( Record[] records, int id ) { List l = new ArrayList( records.length ); for ( int i = 0; i < records.length; i++ ) { if ( records[i].getBlockId() == id && !records[i].isVisited() ) { l.add( records[i] ); records[i].setVisited( true ); } } Collections.sort( l, new Comparator() { public int compare( Object arg0, Object arg1 ) { Record r0 = (Record) arg0; Record r1 = (Record) arg1; return r0.getSourcefile().getFilename().compareTo( r1.getSourcefile().getFilename() ); } } ); return (Record[]) l.toArray( new Record[l.size()] ); } private Record[] getDifferentBlockIds( Record[] records ) { final List added = new ArrayList(); List l = new ArrayList( records.length ); for ( int i = 0; i < records.length; i++ ) { Integer id = new Integer( records[i].getBlockId() ); if ( !added.contains( id ) ) { added.add( id ); l.add( records[i] ); } } return (Record[]) l.toArray( new Record[l.size()] ); } private void summarySection( final Sink sink, SimianAuditListener listener ) { sink.section1(); sink.sectionTitle1(); sink.text( getBundle( locale ).getString( "report.simian.summary" ) ); sink.sectionTitle1_(); sink.table(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.threshold" ) ); sink.tableCell_(); sink.tableCell(); sink.text( "" + this.minimumThreshold ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.total.duplicate.lines" ) ); sink.tableCell_(); sink.tableCell(); sink.text( "" + listener.getDuplicateLineCount() ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.total.duplicate.blocks" ) ); sink.tableCell_(); sink.tableCell(); sink.text( "" + listener.getBlockCount() ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.total.duplicate.files" ) ); sink.tableCell_(); sink.tableCell(); sink.text( "" + listener.getFileWithDuplicateCount() ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.total.processed.lines" ) ); sink.tableCell_(); sink.tableCell(); sink.text( "" + listener.getTotalSourceLines() ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.total.processed.files" ) ); sink.tableCell_(); sink.tableCell(); sink.text( "" + listener.getFileProcessedCount() ); sink.tableCell_(); sink.tableRow_(); sink.tableRow(); sink.tableCell(); sink.text( getBundle( locale ).getString( "report.simian.total.scantime" ) ); sink.tableCell_(); sink.tableCell(); sink.text( listener.getElapsed() + "ms" ); sink.tableCell_(); sink.tableRow_(); sink.table_(); sink.section1_(); } private String getLink( Record r ) { String ret = "#"; String sourceFile = r.getSourcefile().getFilename(); // check if source is 'core' or test link to jxr is different if ( ret.startsWith( "/" ) ) { ret = ret.substring( 1 ); } if ( sourceFile.startsWith( sourceDirectory ) ) { ret = "xref" + sourceFile.substring( sourceDirectory.length() ); } if ( sourceFile.startsWith( testDirectory ) ) { ret = "xref-test" + sourceFile.substring( testDirectory.length() ); } if ( ret.endsWith( ".java" ) ) { ret = ret.substring( 0, ret.length() - 5 ).concat( ".html" ); } return ret; } private String getSourceFilenameWithoutBasedir( Record r ) { final SourceFile sourcefile = r.getSourcefile(); return getSourceFilenameWithoutBasedir( sourcefile ); } private String getSourceFilenameWithoutBasedir( final SourceFile sourcefile ) { StringBuffer sb = new StringBuffer( sourcefile.getFilename() ); String path = project.getBasedir().getAbsolutePath(); if ( sb.subSequence( 0, path.length() ).equals( path ) ) sb = sb.delete( 0, path.length() ); return sb.toString(); } private void rowSeparator( final Sink sink, final int cells ) { // simple separator sink.tableRow(); for ( int i = 0; i < cells; i++ ) { sink.tableCell(); sink.text( "" ); sink.tableCell_(); } sink.tableRow_(); } private Options getOptions() { final Options options = new Options(); options.setThreshold( 6 ); options.setOption( Option.IGNORE_STRINGS, ignoreStrings ); options.setOption( Option.IGNORE_IDENTIFIERS, ignoreIdentifiers ); options.setOption( Option.IGNORE_NUMBERS, ignoreNumbers ); options.setOption( Option.IGNORE_VARIABLE_NAMES, ignoreVariableNames ); options.setOption( Option.IGNORE_LITERALS, ignoreLiterals ); options.setOption( Option.IGNORE_MODIFIERS, ignoreModifiers ); return options; } private SourceFile[] getAffectedFiles( Record[] records, Comparator comp ) { List l = new ArrayList( records.length ); for ( int i = 0; i < records.length; i++ ) { if ( !l.contains( records[i].getSourcefile() ) ) { l.add( records[i].getSourcefile() ); } } Collections.sort( l, comp ); return (SourceFile[]) l.toArray( new SourceFile[l.size()] ); } /** * @see org.apache.maven.reporting.MavenReport#getOutputName() */ public String getOutputName() { return "simian"; } private List getFilesToProcess( final String includes, final String excludes ) throws IOException { final File dir = new File( getProject().getBuild().getSourceDirectory() ); if ( !dir.exists() ) { return Collections.EMPTY_LIST; } final StringBuffer excludesStr = new StringBuffer(); if ( StringUtils.isNotEmpty( excludes ) ) { excludesStr.append( excludes ); } final String[] defaultExcludes = FileUtils.getDefaultExcludes(); for ( int i = 0; i < defaultExcludes.length; i++ ) { if ( excludesStr.length() > 0 ) { excludesStr.append( "," ); } excludesStr.append( defaultExcludes[i] ); } return FileUtils.getFiles( dir, includes, excludesStr.toString() ); } private static ResourceBundle getBundle( final Locale locale ) { return ResourceBundle.getBundle( "simian-report", locale, SimianReportMojo.class.getClassLoader() ); } /** * @see org.apache.maven.reporting.AbstractMavenReport#canGenerateReport() */ public boolean canGenerateReport() { final ArtifactHandler artifactHandler = project.getArtifact().getArtifactHandler(); return ( "java".equals( artifactHandler.getLanguage() ) ); } private static final class RecordSizeComparator implements Comparator { public int compare( Object arg0, Object arg1 ) { Record r0 = (Record) arg0; Record r1 = (Record) arg1; return new Integer( r1.getBlockSize() ).compareTo( new Integer( r0.getBlockSize() ) ); } } }