/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.phoenix.report;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.jcoderz.commons.ArgumentMalformedException;
import org.jcoderz.commons.types.Date;
import org.jcoderz.commons.util.Assert;
import org.jcoderz.commons.util.FileUtils;
import org.jcoderz.commons.util.IoUtil;
import org.jcoderz.commons.util.LoggingUtils;
import org.jcoderz.commons.util.ObjectUtil;
import org.jcoderz.commons.util.StringUtil;
import org.jcoderz.phoenix.report.jaxb.Item;
import org.jcoderz.phoenix.report.jaxb.ObjectFactory;
import org.jcoderz.phoenix.report.jaxb.Report;
/**
* Provides merging and filtering of various jcoderz-report.xml files.
* It combines parts of the functions from ReportNormalizer and XmlMergeAntTask.
*
* @author Michael Rumpf
*/
public class ReportMerger
{
/** The Constant CLASSNAME. */
private static final String CLASSNAME = ReportNormalizer.class.getName();
/** The Constant logger. */
private static final Logger logger = Logger.getLogger(CLASSNAME);
/** The length of an unique part of a c&p finding message. */
private static final int CPD_UNIQUE_STRING_LENGTH
= "Copied and pasted code. 341 equal".length();
/** The log level. */
private Level mLogLevel;
/** The out file. */
private File mOutFile = null;
/** The reports. */
private final List<File> mReports = new ArrayList<File>();
/** The filters. */
private final List<File> mFilters = new ArrayList<File>();
/** The old Report. */
private File mOldReport;
/** The old Report. */
private final Date mReportDate = Date.now();
/**
* Merge input reports.
* @throws JAXBException if a xml handling error occurs.
* @throws FileNotFoundException in case of an IO issue.
*/
public void merge ()
throws JAXBException, FileNotFoundException
{
logger.log(Level.FINE, "Merging jcoderz-report.xml files...");
// merge the reports
final Report mergedReport = new ObjectFactory().createReport();
for (final File reportFile : mReports)
{
logger.log(Level.FINE, "Report: " + reportFile);
try
{
final Report report = (Report) new ObjectFactory()
.createUnmarshaller().unmarshal(reportFile);
mergedReport.getFile().addAll(report.getFile());
}
catch (JAXBException ex)
{
// TODO: ADD ISSUE AS system ITEM TO THE REPORT
ex.printStackTrace();
}
}
writeResult(mergedReport, mOutFile);
}
/**
* Filters the report XML file using the JDK XSL processor.
* @throws TransformerException if the transformation fails.
* @throws IOException if an io operation fails.
*/
public void filter () throws TransformerException, IOException
{
logger.log(Level.FINE, "Filtering jcoderz-report.xml files...");
for (final File filterFile : mFilters)
{
logger.log(Level.FINE, "Filter: " + filterFile);
final TransformerFactory tFactory
= TransformerFactory.newInstance();
final Transformer transformer
= tFactory.newTransformer(new StreamSource(filterFile));
final File tempOutputFile
= new File(mOutFile.getCanonicalPath() + ".tmp");
FileUtils.createNewFile(tempOutputFile);
final FileOutputStream out = new FileOutputStream(tempOutputFile);
transformer.transform(new StreamSource(mOutFile),
new StreamResult(out));
IoUtil.close(out);
FileUtils.copyFile(tempOutputFile, mOutFile);
FileUtils.delete(tempOutputFile);
}
}
/**
* Searches for new findings based on the old jcReport and increases the
* severity of such findings to NEW.
*/
public void flagNewFindings ()
{
logger.log(Level.FINE, "Searching for NEW findings...");
try
{
final Report currentReport
= (Report) new ObjectFactory().createUnmarshaller().unmarshal(
mOutFile);
final Report oldReport
= (Report) new ObjectFactory().createUnmarshaller().unmarshal(
mOldReport);
for (org.jcoderz.phoenix.report.jaxb.File newFile
: (List<org.jcoderz.phoenix.report.jaxb.File>)
currentReport.getFile())
{
final org.jcoderz.phoenix.report.jaxb.File oldFile
= findFile(newFile, oldReport);
if (oldFile != null)
{
findNewFindings(newFile, oldFile);
}
else
{
flaggAllAsNew(newFile.getItem());
}
}
writeResult(currentReport, mOutFile);
}
catch (Exception ex)
{
logger.log(Level.WARNING,
"Failed to flagNewFindings. Cause " + ex.getMessage(), ex);
}
}
private void findNewFindings (org.jcoderz.phoenix.report.jaxb.File newFile,
org.jcoderz.phoenix.report.jaxb.File oldFile)
{
final List<Item> newFindings
= new ArrayList<Item>((List<Item>) newFile.getItem());
final List<Item> oldFindings
= new ArrayList<Item>((List<Item>) oldFile.getItem());
filterLowSeverity(newFindings);
filterLowSeverity(oldFindings);
filterFullMatches(newFindings, oldFindings);
filterPartialMatches(newFindings, oldFindings);
// the rest...
flaggAllAsNew(newFindings);
for (Item item : oldFindings)
{
addAsOld(newFile.getItem(), item);
}
}
private void flaggAllAsNew (final List<Item> newFindings)
{
for (Item item : newFindings)
{
if (item.getSeverity().getPenalty() > 0
&& item.getSeverity() != Severity.COVERAGE)
{
flagAsNew(item);
}
}
}
private void addAsOld (List<Item> newFindings, Item item)
{
if (item.getSeverity().getPenalty() > 0
&& item.getSeverity() != Severity.COVERAGE)
{
item.setSeverity(Severity.OK);
item.unsetNew();
item.setOld(true);
newFindings.add(item);
}
}
private void filterFullMatches (final List<Item> newFindings,
final List<Item> oldFindings)
{
// Filter 100% matches:
final Iterator<Item> newIterator = newFindings.iterator();
while (newIterator.hasNext())
{
final Item newItem = newIterator.next();
final Iterator<Item> oldIterator = oldFindings.iterator();
while (oldIterator.hasNext())
{
final Item oldItem = oldIterator.next();
if (isSameFinding(newItem, oldItem))
{
newItem.setSince(oldItem.getSince());
newIterator.remove();
oldIterator.remove();
break;
}
}
}
}
/* private */ static boolean isSameFinding (Item newItem, Item oldItem)
{
final boolean result;
if (oldItem.getFindingType().equals(newItem.getFindingType()))
{
if (oldItem.getOrigin().equals(Origin.CPD))
{
// Fuzzy compare CPD Findings
// see also http://www.jcoderz.org/fawkez/ticket/71
result = oldItem.getLine() == newItem.getLine()
&& oldItem.getMessage().regionMatches(
0, newItem.getMessage(), 0, CPD_UNIQUE_STRING_LENGTH);
}
else
{
result = oldItem.getLine() == newItem.getLine()
&& oldItem.getColumn() == newItem.getColumn()
&& oldItem.getMessage().equals(newItem.getMessage())
&& oldItem.getCounter() <= newItem.getCounter();
}
}
else
{
result = false;
}
return result;
}
private void filterPartialMatches (final List<Item> newFindings,
final List<Item> oldFindings)
{
// Filter matches that 'moved' within the file.
// There is for sure a better algorithm possible..
final Iterator<Item> newIterator = newFindings.iterator();
while (newIterator.hasNext())
{
final Item newItem = newIterator.next();
final Iterator<Item> oldIterator = oldFindings.iterator();
while (oldIterator.hasNext())
{
final Item oldItem = oldIterator.next();
if (isPartialSameFinding(newItem, oldItem))
{
newItem.setSince(oldItem.getSince());
newIterator.remove();
oldIterator.remove();
break;
}
}
}
}
private boolean isPartialSameFinding (Item newItem, Item oldItem)
{
final boolean result;
if (oldItem.getFindingType().equals(newItem.getFindingType()))
{
if (oldItem.getOrigin().equals(Origin.CPD))
{
// Fuzzy compare CPD Findings
// see also http://www.jcoderz.org/fawkez/ticket/71
// The or is by intention due to resistant findings
// reported as new frequently.
result = oldItem.getLine() == newItem.getLine()
|| oldItem.getMessage().regionMatches(
0, newItem.getMessage(), 0, CPD_UNIQUE_STRING_LENGTH);
}
else
{
result = oldItem.getMessage().equals(newItem.getMessage())
&& oldItem.getCounter() <= newItem.getCounter();
}
}
else
{
result = false;
}
return result;
}
private void filterLowSeverity (final List<Item> newFindings)
{
final Iterator<Item> i = newFindings.iterator();
while (i.hasNext())
{
final Item item = i.next();
if (item.getSeverity().getPenalty() == 0
|| item.getSeverity() == Severity.COVERAGE)
{
i.remove();
}
}
}
private void flagAsNew (Item item)
{
item.unsetOld();
item.setNew(true);
item.setSince(mReportDate);
}
// This could be done faster, might be restructure the data first for
// faster lookup.
private org.jcoderz.phoenix.report.jaxb.File findFile (
org.jcoderz.phoenix.report.jaxb.File newFile, Report oldReport)
{
final String className = newFile.getClassname();
final String packageName = newFile.getPackage();
final String fileName = newFile.getName();
org.jcoderz.phoenix.report.jaxb.File result = null;
for (org.jcoderz.phoenix.report.jaxb.File file
: (List<org.jcoderz.phoenix.report.jaxb.File>) oldReport.getFile())
{
if (ObjectUtil.equals(file.getName(), fileName)
|| (!StringUtil.isEmptyOrNull(className)
&& packageName != null
&& ObjectUtil.equals(file.getClassname(), className)
&& ObjectUtil.equals(file.getPackage(), packageName)))
{
result = file;
break;
}
}
return result;
}
/**
* Parses the arguments.
*
* @param args the args
*/
private void parseArguments (String[] args)
{
try
{
for (int i = 0; i < args.length; )
{
logger.fine("Parsing argument '" + args[i] + "' = '"
+ args[i + 1] + "'");
if ("-jcreport".equals(args[i]))
{
addReport(new File(args[i + 1]));
}
else if ("-filter".equals(args[i]))
{
addFilter(new File(args[i + 1]));
}
else if ("-old".equals(args[i]))
{
setOldFile(new File(args[i + 1]));
}
else if ("-loglevel".equals(args[i]))
{
setLogLevel(Level.parse(args[i + 1]));
}
else if ("-out".equals(args[i]))
{
setOutFile(new File(args[i + 1]));
}
else
{
throw new IllegalArgumentException(
"Invalid argument '" + args[i] + "'");
}
++i;
++i;
}
}
catch (IndexOutOfBoundsException e)
{
final IllegalArgumentException ex = new IllegalArgumentException(
"Missing value for " + args[args.length - 1]);
ex.initCause(e);
throw ex;
}
catch (IOException e)
{
final IllegalArgumentException ex = new IllegalArgumentException(
"Wrong out folder " + args[args.length - 1]);
ex.initCause(e);
throw ex;
}
}
/**
* The main method.
*
* @param args the arguments
* @throws Exception in case of a technical issue.
*/
public static void main (String[] args)
throws Exception
{
final ReportMerger rm = new ReportMerger();
rm.parseArguments(args);
rm.merge();
rm.filter();
}
/**
* Adds the report.
* @param report the report
*/
public void addReport (File report)
{
mReports.add(report);
}
/**
* Adds the filter.
* @param filter the filter
*/
public void addFilter (File filter)
{
mFilters.add(filter);
}
/**
* Gets the log level.
*
* @return the log level
*/
public Level getLogLevel ()
{
return mLogLevel;
}
/**
* Sets the log level.
*
* @param logLevel the new log level
*/
public void setLogLevel (Level logLevel)
{
mLogLevel = logLevel;
LoggingUtils.setGlobalHandlerLogLevel(mLogLevel);
logger.fine("Setting log level: " + mLogLevel);
logger.setLevel(mLogLevel);
}
/**
* Gets the out file.
*
* @return the out file
*/
public File getOutFile ()
{
return mOutFile;
}
/**
* Set the old report to compare with.
* @param file old report file.
* @throws IOException if the file name conversion fails
*/
public void setOldFile (File file)
throws IOException
{
Assert.notNull(file, "file");
if (mOldReport != null)
{
throw new ArgumentMalformedException("old", file,
"Old Report File has already set to '" + mOldReport + "'.");
}
mOldReport = file.getCanonicalFile();
}
/**
* Sets the out file.
*
* @param outFile the new out file
*
* @throws IOException Signals that an I/O exception has occurred.
*/
public void setOutFile (File outFile)
throws IOException
{
if (mOutFile != null)
{
throw new ArgumentMalformedException("outFile", outFile,
"Out File already set to '" + mOutFile + "'.");
}
mOutFile = outFile;
if (mOutFile.isDirectory())
{
FileUtils.mkdirs(mOutFile);
mOutFile = new File(mOutFile,
ReportNormalizer.JCODERZ_REPORT_XML).getCanonicalFile();
}
else
{
mOutFile = mOutFile.getCanonicalFile();
}
}
private void writeResult (final Report mergedReport, File outFile)
throws JAXBException, PropertyException, FileNotFoundException
{
// create the file
final JAXBContext mJaxbContext
= JAXBContext.newInstance("org.jcoderz.phoenix.report.jaxb",
this.getClass().getClassLoader());
final Marshaller marshaller = mJaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,
Boolean.TRUE);
final FileOutputStream out = new FileOutputStream(outFile);
try
{
marshaller.marshal(mergedReport, out);
}
finally
{
IoUtil.close(out);
}
}
}