/*
* Sonar Sonargraph Plugin
* Copyright (C) 2009, 2010, 2011 hello2morrow GmbH
* mailto: info AT hello2morrow DOT com
*
* Licensed 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.
*/
package com.hello2morrow.sonarplugin;
import com.hello2morrow.sonarplugin.xsd.ReportContext;
import com.hello2morrow.sonarplugin.xsd.XsdArchitectureViolation;
import com.hello2morrow.sonarplugin.xsd.XsdAttribute;
import com.hello2morrow.sonarplugin.xsd.XsdAttributeCategory;
import com.hello2morrow.sonarplugin.xsd.XsdAttributeRoot;
import com.hello2morrow.sonarplugin.xsd.XsdBuildUnits;
import com.hello2morrow.sonarplugin.xsd.XsdCycleGroup;
import com.hello2morrow.sonarplugin.xsd.XsdCycleGroups;
import com.hello2morrow.sonarplugin.xsd.XsdCyclePath;
import com.hello2morrow.sonarplugin.xsd.XsdPosition;
import com.hello2morrow.sonarplugin.xsd.XsdTask;
import com.hello2morrow.sonarplugin.xsd.XsdTasks;
import com.hello2morrow.sonarplugin.xsd.XsdTypeRelation;
import com.hello2morrow.sonarplugin.xsd.XsdViolations;
import com.hello2morrow.sonarplugin.xsd.XsdWarning;
import com.hello2morrow.sonarplugin.xsd.XsdWarnings;
import com.hello2morrow.sonarplugin.xsd.XsdWarningsByAttribute;
import com.hello2morrow.sonarplugin.xsd.XsdWarningsByAttributeGroup;
import org.apache.commons.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.Sensor;
import org.sonar.api.batch.SensorContext;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.Metric;
import org.sonar.api.resources.JavaFile;
import org.sonar.api.resources.JavaPackage;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
import org.sonar.api.rules.RulePriority;
import org.sonar.api.rules.Violation;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class SonargraphSensor implements Sensor
{
private static final Logger LOG = LoggerFactory.getLogger(SonargraphSensor.class);
private static final String REPORT_DIR = "sonargraph-sonar-plugin";
private static final String REPORT_NAME = "sonargraph-sonar-report.xml";
private static final String ACD = "AverageComponentDependency";
private static final String NCCD = "NormalizedCumulativeComponentDependency";
private static final String INTERNAL_PACKAGES = "NumberOfInternalNamespaces";
private static final String INSTRUCTIONS = "NumberOfInstructions";
private static final String UNASSIGNED_TYPES = "NumberOfNotAssignedTypes";
private static final String VIOLATING_DEPENDENCIES = "NumberOfViolations";
private static final String VIOLATING_TYPES = "NumberOfViolatingTypes";
private static final String TYPE_DEPENDENCIES = "OverallNumberOfTypeDependencies";
private static final String JAVA_FILES = "NumberOfSourceFiles";
private static final String IGNORED_VIOLATIONS = "NumberOfIgnoredViolations";
private static final String IGNORED_WARNINGS = "NumberOfIgnoredWarnings";
private static final String TASKS = "NumberOfTasks";
private static final String ALL_WARNINGS = "NumberOfWarnings";
private static final String CYCLE_WARNINGS = "NumberOfCyclicWarnings";
private static final String THRESHOLD_WARNINGS = "NumberOfMetricWarnings";
private static final String WORKSPACE_WARNINGS = "NumberOfWorkspaceWarnings";
private static final String DUPLICATE_WARNINGS = "NumberOfDuplicateCodeBlocksWarnings";
private static final String EROSION_REFS = "StructuralErosionReferenceLevel";
private static final String EROSION_TYPES = "StructuralErosionTypeLevel";
private static final String INTERNAL_TYPES = "NumberOfInternalTypes";
private static final String STUCTURAL_DEBT_INDEX = "StructuralDebtIndex";
private final Map<String, Number> buildUnitMetrics = new HashMap<String, Number>();
private SensorContext sensorContext;
private final RuleFinder ruleFinder;
private double indexCost = SonargraphPluginBase.COST_PER_INDEX_POINT_DEFAULT;
protected static ReportContext readSonargraphReport(String fileName, String packaging)
{
ReportContext result = null;
InputStream input = null;
ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader();
try
{
Thread.currentThread().setContextClassLoader(SonargraphSensor.class.getClassLoader());
JAXBContext context = JAXBContext.newInstance("com.hello2morrow.sonarplugin.xsd");
Unmarshaller u = context.createUnmarshaller();
input = new FileInputStream(fileName);
result = (ReportContext) u.unmarshal(input);
}
catch (JAXBException e)
{
LOG.error("JAXB Problem in " + fileName, e);
}
catch (FileNotFoundException e)
{
if (!packaging.equalsIgnoreCase("pom"))
{
LOG.warn("Cannot open Sonargraph report: " + fileName + ".");
LOG.warn(" Did you run the maven sonargraph goal before with the POM option <prepareForSonar>true</prepareForSonar> " + "or with the commandline option -Dsonargraph.prepareForSonar=true?");
LOG.warn(" Is the project part of the Sonargraph architecture description?");
LOG.warn(" Did you set the 'aggregate' to true (must be false)?");
}
}
finally
{
Thread.currentThread().setContextClassLoader(defaultClassLoader);
if (input != null)
{
try
{
input.close();
}
catch (IOException e)
{
LOG.error("Cannot close " + fileName, e);
}
}
}
return result;
}
public SonargraphSensor(RuleFinder ruleFinder)
{
this.ruleFinder = ruleFinder;
if (ruleFinder == null)
{
LOG.warn("No RulesManager provided to sensor");
}
}
public boolean shouldExecuteOnProject(Project project)
{
return true;
}
private void readAttributes(XsdAttributeRoot root)
{
buildUnitMetrics.clear();
for (XsdAttributeCategory cat : root.getAttributeCategory())
{
for (XsdAttribute attr : cat.getAttribute())
{
String attrName = attr.getStandardName();
String value = attr.getValue();
try
{
if (value.contains("."))
{
buildUnitMetrics.put(attrName, SonargraphPluginBase.FLOAT_FORMAT.parse(value));
}
else
{
buildUnitMetrics.put(attrName, SonargraphPluginBase.INTEGER_FORMAT.parse(value));
}
}
catch (ParseException e)
{
// Ignore this value
}
}
}
}
private boolean hasBuildUnitMetric(String key)
{
return buildUnitMetrics.get(key) != null;
}
private double getBuildUnitMetric(String key)
{
Number num = buildUnitMetrics.get(key);
if (num == null)
{
LOG.error("Cannot find metric <" + key + "> in generated report");
return 0.0;
}
return num.doubleValue();
}
private Measure saveMeasure(String key, Metric metric, int precision)
{
double value = getBuildUnitMetric(key);
return saveMeasure(metric, value, precision);
}
private Measure saveMeasure(Metric metric, double value, int precision)
{
Measure m = new Measure(metric, value, precision);
sensorContext.saveMeasure(m);
return m;
}
private String getAttribute(List<XsdAttribute> map, String name)
{
String value = null;
for (XsdAttribute attr : map)
{
if (attr.getName().equals(name))
{
value = attr.getValue();
break;
}
}
return value;
}
private void analyseCycleGroups(ReportContext report, Number internalPackages, String buildUnitName)
{
XsdCycleGroups cycleGroups = report.getCycleGroups();
double cyclicity = 0;
double biggestCycleGroupSize = 0;
double cyclicPackages = 0;
for (XsdCycleGroup group : cycleGroups.getCycleGroup())
{
if (group.getNamedElementGroup().equals("Physical package") && getBuildUnitName(group).equals(buildUnitName))
{
int groupSize = group.getCyclePath().size();
cyclicPackages += groupSize;
cyclicity += groupSize * groupSize;
if (groupSize > biggestCycleGroupSize)
{
biggestCycleGroupSize = groupSize;
}
handlePackageCycleGroup(group);
}
}
saveMeasure(SonargraphMetrics.BIGGEST_CYCLE_GROUP, biggestCycleGroupSize, 0);
saveMeasure(SonargraphMetrics.CYCLICITY, cyclicity, 0);
saveMeasure(SonargraphMetrics.CYCLIC_PACKAGES, cyclicPackages, 0);
double relativeCyclicity = 100.0 * Math.sqrt(cyclicity) / internalPackages.doubleValue();
double relativeCyclicPackages = 100.0 * cyclicPackages / internalPackages.doubleValue();
saveMeasure(SonargraphMetrics.RELATIVE_CYCLICITY, relativeCyclicity, 1);
saveMeasure(SonargraphMetrics.INTERNAL_PACKAGES, internalPackages.doubleValue(), 0);
saveMeasure(SonargraphMetrics.CYCLIC_PACKAGES_PERCENT, relativeCyclicPackages, 1);
}
@SuppressWarnings("unchecked")
private void saveViolation(Rule rule, RulePriority priority, String fqName, int line, String msg)
{
Resource javaFile = sensorContext.getResource(new JavaFile(fqName));
if (javaFile == null)
{
LOG.error("Cannot obtain resource " + fqName);
}
else
{
Violation v = Violation.create(rule, javaFile);
v.setMessage(msg);
v.setLineId(line);
if (priority != null)
{
v.setSeverity(priority);
}
sensorContext.saveViolation(v);
}
}
private String getBuildUnitName(XsdCycleGroup group)
{
if (group.getParent().equals("(Default Build Unit)"))
{
return group.getElementScope();
}
return group.getParent();
}
private String getBuildUnitName(String fqName)
{
String buName = "<UNKNOWN>";
if (fqName != null)
{
int colonPos = fqName.indexOf("::");
if (colonPos != -1)
{
buName = fqName.substring(colonPos + 2);
if (buName.equals("(Default Build Unit)"))
{
// Compatibility with old SonarJ versions
buName = fqName.substring(0, colonPos);
}
}
}
return buName;
}
private static String relativeFileNameToFqName(String fileName)
{
int lastDot = fileName.lastIndexOf('.');
return fileName.substring(0, lastDot).replace('/', '.');
}
@SuppressWarnings("unchecked")
private void handlePackageCycleGroup(XsdCycleGroup group)
{
Rule rule = ruleFinder.findByKey(SonargraphPluginBase.PLUGIN_KEY, SonargraphPluginBase.CYCLE_GROUP_RULE_KEY);
if (rule != null)
{
for (XsdCyclePath pathElement : group.getCyclePath())
{
String fqName = pathElement.getParent();
Resource<JavaPackage> javaPackage = sensorContext.getResource(new JavaPackage(fqName));
if (javaPackage == null)
{
LOG.error("Cannot obtain resource " + fqName);
}
else
{
Violation v = Violation.create(rule, javaPackage);
v.setMessage("Package participates in a cycle group");
v.setLineId(1);
sensorContext.saveViolation(v);
}
}
}
}
private int handleArchitectureViolations(XsdViolations violations, String buildUnitName)
{
Rule rule = ruleFinder.findByKey(SonargraphPluginBase.PLUGIN_KEY, SonargraphPluginBase.ARCH_RULE_KEY);
int count = 0;
for (XsdArchitectureViolation violation : violations.getArchitectureViolations())
{
String toName = getAttribute(violation.getArchitectureViolation().getAttribute(), "To");
String toElemType = getAttribute(violation.getArchitectureViolation().getAttribute(), "To element type").toLowerCase();
String target = toElemType + ' ' + toName;
for (XsdTypeRelation rel : violation.getTypeRelation())
{
String toType = getAttribute(rel.getAttribute(), "To");
String msg = "Type " + toType + " from " + target + " must not be used from here";
String bu = getAttribute(rel.getAttribute(), "From build unit");
bu = getBuildUnitName(bu);
if (bu.equals(buildUnitName))
{
for (XsdPosition pos : rel.getPosition())
{
if (rule != null)
{
String relFileName = pos.getFile();
if (relFileName != null)
{
String fqName = relativeFileNameToFqName(relFileName);
saveViolation(rule, null, fqName, Integer.valueOf(pos.getLine()), msg);
}
}
count++;
}
}
}
}
if (rule == null)
{
LOG.error("Sonargraph architecture rule not found");
}
return count;
}
private String getRuleKey(String attributeGroup)
{
if (attributeGroup.equals("Duplicate code"))
{
return SonargraphPluginBase.DUPLICATE_RULE_KEY;
}
if (attributeGroup.equals("Workspace"))
{
return SonargraphPluginBase.WORKSPACE_RULE_KEY;
}
if (attributeGroup.equals("Threshold"))
{
return SonargraphPluginBase.THRESHOLD_RULE_KEY;
}
return null;
}
private void handleWarnings(XsdWarnings warnings, String buildUnitName)
{
for (XsdWarningsByAttributeGroup warningGroup : warnings.getWarningsByAttributeGroup())
{
String key = getRuleKey(warningGroup.getAttributeGroup());
if (key == null)
{
continue;
}
Rule rule = ruleFinder.findByKey(SonargraphPluginBase.PLUGIN_KEY, key);
if (rule == null)
{
LOG.error("Sonargraph threshold rule not found");
continue;
}
for (XsdWarningsByAttribute warningByAttribute : warningGroup.getWarningsByAttribute())
{
String attrName = warningByAttribute.getAttributeName();
for (XsdWarning warning : warningByAttribute.getWarning())
{
String msg = attrName + "=" + getAttribute(warning.getAttribute(), "Attribute value");
String bu = getAttribute(warning.getAttribute(), "Build unit");
bu = getBuildUnitName(bu);
if (bu.equals(buildUnitName))
{
if (warning.getPosition().size() > 0)
{
for (XsdPosition pos : warning.getPosition())
{
String relFileName = pos.getFile();
if (relFileName != null)
{
String fqName = relativeFileNameToFqName(relFileName);
saveViolation(rule, null, fqName, Integer.valueOf(pos.getLine()), msg);
}
}
}
else
{
String elemType = getAttribute(warning.getAttribute(), "Element type");
if (elemType.equals("Class file") || elemType.equals("Source file"))
{
// Attach a violation at line 1
String fileName = getAttribute(warning.getAttribute(), "Element");
String fqName = fileName.substring(0, fileName.lastIndexOf('.')).replace('/', '.');
saveViolation(rule, null, fqName, 1, msg);
}
}
}
}
}
}
}
private String handleDescription(String descr)
{
if (descr.startsWith("Fix warning"))
{
// TODO: handle ascending metrics correctly (99% are descending)
return "Reduce" + descr.substring(descr.indexOf(':') + 1).toLowerCase();
}
if (descr.startsWith("Cut type"))
{
String toType = descr.substring(descr.indexOf("to "));
return "Cut dependency " + toType;
}
if (descr.startsWith("Move type"))
{
String to = descr.substring(descr.indexOf("to "));
return "Move " + to;
}
return descr;
}
private int handleTasks(XsdTasks tasks, String buildUnitName)
{
Map<String, RulePriority> priorityMap = new HashMap<String, RulePriority>();
Rule rule = ruleFinder.findByKey(SonargraphPluginBase.PLUGIN_KEY, SonargraphPluginBase.TASK_RULE_KEY);
int count = 0;
if (rule == null)
{
LOG.error("Sonargraph task rule not found");
return 0;
}
priorityMap.put("Low", RulePriority.INFO);
priorityMap.put("Medium", RulePriority.MINOR);
priorityMap.put("High", RulePriority.MAJOR);
for (XsdTask task : tasks.getTask())
{
String bu = getAttribute(task.getAttribute(), "Build unit");
bu = getBuildUnitName(bu);
if (bu.equals(buildUnitName))
{
String priority = getAttribute(task.getAttribute(), "Priority");
String description = getAttribute(task.getAttribute(), "Description");
String assignedTo = getAttribute(task.getAttribute(), "Assigned to");
description = handleDescription(description); // This should not
// be needed,
// but the
// current
// description
// sucks
int index = description.indexOf(" package");
if (index > 0 && index < 8)
{
// Package refactorings won't get markers - this would
// create to many non relevant markers
count++;
}
else
{
if (assignedTo != null)
{
assignedTo = '[' + assignedTo.trim() + ']';
if (assignedTo.length() > 2)
{
description += ' ' + assignedTo;
}
}
for (XsdPosition pos : task.getPosition())
{
String relFileName = pos.getFile();
if (relFileName != null)
{
String fqName = relativeFileNameToFqName(relFileName);
int line = Integer.valueOf(pos.getLine());
if (line == 0)
{
line = 1;
}
saveViolation(rule, priorityMap.get(priority), fqName, line, description);
}
count++;
}
}
}
}
return count;
}
private void addArchitectureMeasures(ReportContext report, String buildUnitName)
{
double types = saveMeasure(INTERNAL_TYPES, SonargraphMetrics.INTERNAL_TYPES, 0).getValue();
Measure unassignedTypes = saveMeasure(UNASSIGNED_TYPES, SonargraphMetrics.UNASSIGNED_TYPES, 0);
Measure violatingTypes = saveMeasure(VIOLATING_TYPES, SonargraphMetrics.VIOLATING_TYPES, 0);
saveMeasure(VIOLATING_DEPENDENCIES, SonargraphMetrics.VIOLATING_DEPENDENCIES, 0);
saveMeasure(TASKS, SonargraphMetrics.TASKS, 0);
if (hasBuildUnitMetric(THRESHOLD_WARNINGS))
{
saveMeasure(THRESHOLD_WARNINGS, SonargraphMetrics.THRESHOLD_WARNINGS, 0);
}
saveMeasure(WORKSPACE_WARNINGS, SonargraphMetrics.WORKSPACE_WARNINGS, 0);
saveMeasure(IGNORED_VIOLATIONS, SonargraphMetrics.IGNORED_VIOLATONS, 0);
saveMeasure(IGNORED_WARNINGS, SonargraphMetrics.IGNORED_WARNINGS, 0);
if (hasBuildUnitMetric(DUPLICATE_WARNINGS))
{
saveMeasure(DUPLICATE_WARNINGS, SonargraphMetrics.DUPLICATE_WARNINGS, 0);
}
saveMeasure(CYCLE_WARNINGS, SonargraphMetrics.CYCLE_WARNINGS, 0);
saveMeasure(ALL_WARNINGS, SonargraphMetrics.ALL_WARNINGS, 0);
assert types >= 1.0 : "Project must not be empty !";
double violatingTypesPercent = 100.0 * violatingTypes.getValue() / types;
double unassignedTypesPercent = 100.0 * unassignedTypes.getValue() / types;
saveMeasure(SonargraphMetrics.VIOLATING_TYPES_PERCENT, violatingTypesPercent, 1);
saveMeasure(SonargraphMetrics.UNASSIGNED_TYPES_PERCENT, unassignedTypesPercent, 1);
XsdViolations violations = report.getViolations();
double violatingRefs = 0;
double taskRefs = 0;
if (ruleFinder != null)
{
violatingRefs = handleArchitectureViolations(violations, buildUnitName);
handleWarnings(report.getWarnings(), buildUnitName);
taskRefs = handleTasks(report.getTasks(), buildUnitName);
}
saveMeasure(SonargraphMetrics.ARCHITECTURE_VIOLATIONS, violatingRefs, 0);
saveMeasure(SonargraphMetrics.TASK_REFS, taskRefs, 0);
}
private void analyse(IProject project, XsdAttributeRoot xsdBuildUnit, String buildUnitName, ReportContext report)
{
LOG.info("Adding measures for " + project.getName());
readAttributes(xsdBuildUnit);
Number internalPackages = getBuildUnitMetric(INTERNAL_PACKAGES);
if (internalPackages.intValue() == 0)
{
LOG.warn("No packages found in project " + project.getName());
return;
}
saveMeasure(ACD, SonargraphMetrics.ACD, 1);
saveMeasure(NCCD, SonargraphMetrics.NCCD, 1);
saveMeasure(INSTRUCTIONS, SonargraphMetrics.INSTRUCTIONS, 0);
saveMeasure(JAVA_FILES, SonargraphMetrics.JAVA_FILES, 0);
saveMeasure(TYPE_DEPENDENCIES, SonargraphMetrics.TYPE_DEPENDENCIES, 0);
saveMeasure(EROSION_REFS, SonargraphMetrics.EROSION_REFS, 0);
saveMeasure(EROSION_TYPES, SonargraphMetrics.EROSION_TYPES, 0);
Number structuralDebtIndex = saveMeasure(STUCTURAL_DEBT_INDEX, SonargraphMetrics.EROSION_INDEX, 0).getValue();
if (indexCost > 0)
{
double structuralDebtCost = structuralDebtIndex.doubleValue() * indexCost;
saveMeasure(SonargraphMetrics.EROSION_COST, structuralDebtCost, 0);
}
analyseCycleGroups(report, internalPackages, buildUnitName);
if (hasBuildUnitMetric(UNASSIGNED_TYPES))
{
LOG.info("Adding architecture measures for " + project.getName());
addArchitectureMeasures(report, buildUnitName);
}
AlertDecorator.setAlertLevels(new SensorProjectContext(sensorContext));
}
void analyse(IProject project, SensorContext sensorContext, ReportContext report)
{
this.sensorContext = sensorContext;
Configuration configuration = project.getConfiguration();
this.indexCost = configuration.getDouble(SonargraphPluginBase.COST_PER_INDEX_POINT, SonargraphPluginBase.COST_PER_INDEX_POINT_DEFAULT);
XsdBuildUnits buildUnits = report.getBuildUnits();
List<XsdAttributeRoot> buildUnitList = buildUnits.getBuildUnit();
if (buildUnitList.size() == 1)
{
XsdAttributeRoot sonarBuildUnit = buildUnitList.get(0);
String buName = getBuildUnitName(sonarBuildUnit.getName());
analyse(project, sonarBuildUnit, buName, report);
}
else if (buildUnitList.size() > 1)
{
boolean foundMatchingBU = false;
for (XsdAttributeRoot sonarBuildUnit : buildUnitList)
{
String buName = getBuildUnitName(sonarBuildUnit.getName());
if (buildUnitMatchesAnalyzedProject(buName, project))
{
analyse(project, sonarBuildUnit, buName, report);
foundMatchingBU = true;
break;
}
}
if (!foundMatchingBU)
{
LOG.warn("Project " + project.getName() + " could not be mapped to a build unit. The project will not be analyzed. Check the build unit configuration of your Sonargraph system.");
}
}
else
{
LOG.error("No build units found in report file!");
}
}
private boolean buildUnitMatchesAnalyzedProject(String buName, IProject project)
{
final String artifactId = project.getArtifactId();
final String groupId = project.getGroupId();
final String longName = artifactId + "[" + groupId + "]";
final String longName2 = groupId + ':' + artifactId;
return buName.equals(artifactId) || buName.equals(longName) || buName.equals(longName2) || (buName.startsWith("...") && longName2.endsWith(buName.substring(2)));
}
public void analyse(Project project, SensorContext sensorContext)
{
LOG.info("------------------------------------------------------------------------");
LOG.info("Execute sonar-sonargraph-plugin for " + project.getName());
LOG.info("------------------------------------------------------------------------");
ReportContext report = readSonargraphReport(getReportFileName(project), project.getPackaging());
if (report != null)
{
analyse(new ProjectDelegate(project), sensorContext, report);
}
}
public String getReportFileName(Project project)
{
return project.getFileSystem().getBuildDir().getPath() + '/' + REPORT_DIR + '/' + REPORT_NAME;
}
}