diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18e5667 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +############################## +## Java +############################## +.mtj.tmp/ +*.class +*.jar +*.war +*.ear +*.nar +hs_err_pid* + +############################## +## Maven +############################## +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +pom.xml.bak +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +############################## +## Gradle +############################## +bin/ +build/ +.gradle +.gradletasknamecache +gradle-app.setting +!gradle-wrapper.jar + +############################## +## IntelliJ +############################## +out/ +.idea/ +.idea_modules/ +*.iml +*.ipr +*.iws + +############################## +## Eclipse +############################## +.settings/ +bin/ +tmp/ +.metadata +.classpath +.project +*.tmp +*.bak +*.swp +*~.nib +local.properties +.loadpath +.factorypath + +############################## +## NetBeans +############################## +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +############################## +## Visual Studio Code +############################## +.vscode/ + +############################## +## OS X +############################## +.DS_Store diff --git a/README.md b/README.md index d5d5ae0..f33e311 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,112 @@ -# mule-sonarqube-plugin -Mule Sonarqube Plugin +# Mule Sonarqube Validation Plugin + +Sonarqube plugin: + +* Ensure the consistency of Mule Applications by a set of predefined rules focused on specific quality attributes like: _Conceptual Integrity_, _Maintainability_, _Reusability_, _Security_, _Traceability_ and _Availability_. + +**This module requires Java 8**. + +## Prerequisites + +## Install mule-validation-xpath-core dependency + +1. Download the module source code. +2. Open a terminal window and browse to module mule-validation-xpath-core folder. +3. Install this module by running `mvn install`. + +## Plugin Generation + +1. Download the module source code. +2. Open a terminal window and browse to module root folder. +3. Build the mule plugin for Mule rules running `mvn clean package sonar-packaging:sonar-plugin -Dlanguage=mule`. + +## Maven Configuration +1. In your settings file add sonar profile. + ``` + + sonar + + true + + + + + http://localhost:9000 + + + + ``` + + +## Sonar Configuration +1. Copy the generated file, mule-validation-sonarqube-plugin-{version}-mule.jar to *sonar-home*/extensions/plugins +2. Copy rules [Mule 3 Rules](https://github.com/mulesoft-consulting/mule-validation-toolkit/blob/master/mule-validation-xpath-core/src/main/resources/rules-3.xml) or [Mule 4 Rules](https://github.com/mulesoft-consulting/mule-validation-toolkit/blob/master/mule-validation-xpath-core/src/main/resources/rules-4.xml) to *sonar-home*/extensions/plugins +3. Start the server + +## Project Configuration +### Alternative 1 +1. Modify project's pom.xml file to add language and location of the source files to analysis. +``` + + ... + src + ... + +``` +2. Analyze the project executing mvn sonar:sonar + +### Alternative 2 +1. Analyze the project executing mvn sonar:sonar -Dsonar.sources=src + +## Release Notes + +#### 1.0.0 +##### Changes + - Added Line Number processing in Coverage Sensor + +#### 0.0.11 +##### Changes + - Added Rule Template. + You could create new custom rules from SonarQube Console. + +#### 0.0.10 +##### Changes + - Added Rule description. + Now you could add an extended description to the rules. The description supports HTML. + +#### 0.0.9 +##### Changes + - Added property sonar.mule.ruleset.categories. It allows to filter ruleset categories to apply in the project. + Value should be a string separated by commas. + For example run, `mvn sonar:sonar -Dsonar.mule.ruleset.categories=flows` to only apply the flows category ruleset + +#### 0.0.8 +##### Changes + - Bug Fixes + - Added Metric LOC + - Added Support for SonarQube 7.2.1 +#### 0.0.7 +##### Changes + - Added new rules + http namespace +#### 0.0.6 +##### Changes + - Added Munit Coverage and Minor Improvements +#### 0.0.5 +##### Changes + - Externalized Rules to $SONARQUBE_HOME/extensions/plugins/[rules-3.xml|rules-4.xml]. + Basic set of rules for [mule3](https://github.com/mulesoft-consulting/mule-validation-toolkit/blob/master/mule-validation-xpath-core/src/main/resources/rules-3.xml), [mule4](https://github.com/mulesoft-consulting/mule-validation-toolkit/blob/master/mule-validation-xpath-core/src/main/resources/rules-4.xml) + + **Adding new rules: if it is needed to add a new namespace you will have to added to [mule3-namespaces](https://github.com/mulesoft-consulting/mule-validation-toolkit/blob/master/mule-validation-xpath-core/src/main/resources/namespace-3.properties) or to [mule4-namespaces](https://github.com/mulesoft-consulting/mule-validation-toolkit/blob/master/mule-validation-xpath-core/src/main/resources/namespace-4.properties) and regenerate all plugins** + + **Updating existing rule: if you need to update an existing rule, you will also have to update its id number** + +#### 0.0.4 +##### Changes + - Added Measures + - Number of Flows + - Number of SubFlows + - Number of DW Transformations + - Application Complexity + +## Final Note +Enjoy and provide feedback / contribute :) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4ac51b5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + + com.mulesoft.services + mule-validation-sonarqube-plugin + 1.0.0 + jar + + http://maven.apache.org + Sonar Plugin for Mule Projects + + + + UTF-8 + 7.7 + java + + + + + junit + junit + 3.8.1 + test + + + org.sonarsource.sonarqube + sonar-plugin-api + + ${sonar.apiVersion} + + provided + + + org.jdom + jdom2 + 2.0.6 + + + com.mulesoft.services + mule-validation-xpath-core + 0.1.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.8.9 + + + + ${project.artifactId}-${project.version}-mule + + + org.sonarsource.sonar-packaging-maven-plugin + sonar-packaging-maven-plugin + 1.17 + true + + + com.mulesoft.services.tools.sonarqube.MulePlugin + MulePlugin + mulevalidationsonarqubeplugin-mule + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + UTF-8 + + + + + diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/MulePlugin.java b/src/main/java/com/mulesoft/services/tools/sonarqube/MulePlugin.java new file mode 100644 index 0000000..e7acadd --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/MulePlugin.java @@ -0,0 +1,54 @@ +package com.mulesoft.services.tools.sonarqube; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.Plugin; +import org.sonar.api.config.PropertyDefinition; +import org.sonar.api.resources.Qualifiers; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.measures.MuleFlowCount; +import com.mulesoft.services.tools.sonarqube.measures.MuleSizeRating; +import com.mulesoft.services.tools.sonarqube.measures.MuleSubFlowCount; +import com.mulesoft.services.tools.sonarqube.measures.MuleTransformationCount; +import com.mulesoft.services.tools.sonarqube.metrics.ConfigurationFilesSensor; +import com.mulesoft.services.tools.sonarqube.metrics.CoverageSensor; +import com.mulesoft.services.tools.sonarqube.metrics.MUnitSensor; +import com.mulesoft.services.tools.sonarqube.metrics.MuleMetrics; +import com.mulesoft.services.tools.sonarqube.profile.MuleQualityProfile; +import com.mulesoft.services.tools.sonarqube.rule.MuleRulesDefinition; +import com.mulesoft.services.tools.sonarqube.sensor.MuleSensor; + +public class MulePlugin implements Plugin { + + // public static final String LANGUAGE_NAME = "Mule"; + private static final String GENERAL = "General"; + + Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + public void define(Context context) { + if (logger.isDebugEnabled()) + logger.debug("Configuring Mule Plugin"); + + // Added Language + context.addExtensions(MuleLanguage.class, MuleSensor.class); + // context.addExtension(getProperties()); + + // Added Rules + context.addExtension(MuleRulesDefinition.class); + + // Added Profile + context.addExtension(MuleQualityProfile.class); + + context.addExtension(PropertyDefinition.builder(MuleLanguage.FILE_SUFFIXES_KEY) + .defaultValue(MuleLanguage.FILE_SUFFIXES_DEFAULT_VALUE).name("File Suffixes") + .description("List of suffixes for files to analyze.").subCategory(GENERAL) + .category(MuleLanguage.LANGUAGE_NAME).multiValues(true).onQualifiers(Qualifiers.PROJECT).build()); + + context.addExtensions(MuleMetrics.class, ConfigurationFilesSensor.class, MuleSizeRating.class, + MuleFlowCount.class, MuleSubFlowCount.class, MuleTransformationCount.class, CoverageSensor.class, + MUnitSensor.class); + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/filter/MuleFilePredicate.java b/src/main/java/com/mulesoft/services/tools/sonarqube/filter/MuleFilePredicate.java new file mode 100644 index 0000000..80a58dc --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/filter/MuleFilePredicate.java @@ -0,0 +1,39 @@ +package com.mulesoft.services.tools.sonarqube.filter; + +import java.io.IOException; + +import org.jdom2.Document; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.FilePredicate; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +public class MuleFilePredicate implements FilePredicate { + private final Logger logger = Loggers.get(MuleFilePredicate.class); + SAXBuilder saxBuilder = new SAXBuilder(); + String muleNamespace = "http://www.mulesoft.org/schema/mule/core"; + String fileExtension = ".xml"; + + @Override + public boolean apply(InputFile inputFile) { + if (logger.isDebugEnabled()) { + logger.debug("Executing Mule Sensor on file:" + inputFile.filename()); + } + + if (inputFile.filename().endsWith(fileExtension)) { + try { + Document document = saxBuilder.build(inputFile.inputStream()); + + String namespace = document.getRootElement().getNamespaceURI(); + if (muleNamespace.equals(namespace)) + return true; + } catch (JDOMException | IOException e) { + logger.error("Parsing document:" + inputFile.filename(), e); + } + } + return false; + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/language/MuleLanguage.java b/src/main/java/com/mulesoft/services/tools/sonarqube/language/MuleLanguage.java new file mode 100644 index 0000000..f7e5f36 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/language/MuleLanguage.java @@ -0,0 +1,41 @@ +package com.mulesoft.services.tools.sonarqube.language; + +import java.util.Arrays; + +import org.sonar.api.config.Configuration; +import org.sonar.api.resources.AbstractLanguage; + +/** + * Mule Language Definition + * + * @author franco.parma + * + */ +public class MuleLanguage extends AbstractLanguage { + + protected Configuration config = null; + + public static final String LANGUAGE_NAME = "Mule"; + public static final String LANGUAGE_KEY = "mule"; + + public static final String LANGUAGE_MULE4_KEY = "mule4"; + public static final String LANGUAGE_MULE3_KEY = "mule3"; + + public static final String FILE_SUFFIXES_KEY = "sonar.mule.file.suffixes"; + public static final String FILE_SUFFIXES_DEFAULT_VALUE = ".xml"; + + public MuleLanguage(Configuration config) { + super("mule", LANGUAGE_NAME); + this.config = config; + } + + @Override + public String[] getFileSuffixes() { + String[] suffixes = config.getStringArray(FILE_SUFFIXES_KEY); + if (suffixes.length == 0) { + suffixes = Arrays.asList(FILE_SUFFIXES_DEFAULT_VALUE.split(",")).toArray(suffixes); + } + return suffixes; + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleFlowCount.java b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleFlowCount.java new file mode 100644 index 0000000..67a9b88 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleFlowCount.java @@ -0,0 +1,39 @@ +package com.mulesoft.services.tools.sonarqube.measures; + +import org.sonar.api.ce.measure.Component; +import org.sonar.api.ce.measure.Measure; +import org.sonar.api.ce.measure.MeasureComputer; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.metrics.MuleMetrics; + +/** + * Computes the total number of Flows + * @author franco.perez + * + */ +public class MuleFlowCount implements MeasureComputer { + + private final Logger logger = Loggers.get(this.getClass()); + + @Override + public MeasureComputerDefinition define(MeasureComputerDefinitionContext defContext) { + return defContext.newDefinitionBuilder().setOutputMetrics(MuleMetrics.FLOWS.key()).build(); + } + + @Override + public void compute(MeasureComputerContext context) { + logger.info("Computing Mule Flow Size"); + + if (context.getComponent().getType() != Component.Type.FILE) { + int sumFlows = 0; + for (Measure child : context.getChildrenMeasures(MuleMetrics.FLOWS.key())) { + sumFlows += child.getIntValue(); + } + context.addMeasure(MuleMetrics.FLOWS.key(), sumFlows); + + } + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleSizeRating.java b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleSizeRating.java new file mode 100644 index 0000000..361d135 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleSizeRating.java @@ -0,0 +1,53 @@ +package com.mulesoft.services.tools.sonarqube.measures; + +import org.sonar.api.ce.measure.Measure; +import org.sonar.api.ce.measure.MeasureComputer; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.metrics.MuleMetrics; + +/** + * Computes the complex rating depending on the number of flows + * @author franco.perez + * + */ +public class MuleSizeRating implements MeasureComputer { + + private final Logger logger = Loggers.get(this.getClass()); + private static final int THRESHOLD_SIMPLE = 7; + private static final int THRESHOLD_MEDIUM = 15; + private static final int RATING_A = 1; + private static final int RATING_B = 2; + private static final int RATING_C = 3; + + @Override + public MeasureComputerDefinition define(MeasureComputerDefinitionContext def) { + return def.newDefinitionBuilder().setInputMetrics(MuleMetrics.FLOWS.key(), MuleMetrics.SUBFLOWS.key()) + .setOutputMetrics(MuleMetrics.CONFIGURATION_FILES_COMP_RATING.key()).build(); + } + + @Override + public void compute(MeasureComputerContext context) { + logger.info("Computing MuleSizeRating"); + Measure flows = context.getMeasure(MuleMetrics.FLOWS.key()); + Measure subflows = context.getMeasure(MuleMetrics.SUBFLOWS.key()); + int totalNumberOfFlows = 0; + if (flows != null) { + totalNumberOfFlows += flows.getIntValue(); + } + if (subflows != null) { + totalNumberOfFlows += subflows.getIntValue(); + } + + // rating values are currently implemented as integers in API + int rating = RATING_A; + if (totalNumberOfFlows > THRESHOLD_SIMPLE) { + rating = RATING_B; + } + if (totalNumberOfFlows > THRESHOLD_MEDIUM) { + rating = RATING_C; + } + context.addMeasure(MuleMetrics.CONFIGURATION_FILES_COMP_RATING.key(), rating); + } +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleSubFlowCount.java b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleSubFlowCount.java new file mode 100644 index 0000000..c8528a3 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleSubFlowCount.java @@ -0,0 +1,37 @@ +package com.mulesoft.services.tools.sonarqube.measures; + +import org.sonar.api.ce.measure.Component; +import org.sonar.api.ce.measure.Measure; +import org.sonar.api.ce.measure.MeasureComputer; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.metrics.MuleMetrics; + +/** + * Computes the total number of Subflows + * @author franco.perez + * + */ +public class MuleSubFlowCount implements MeasureComputer { + + private final Logger logger = Loggers.get(this.getClass()); + + @Override + public MeasureComputerDefinition define(MeasureComputerDefinitionContext defContext) { + return defContext.newDefinitionBuilder().setOutputMetrics(MuleMetrics.SUBFLOWS.key()).build(); + } + + @Override + public void compute(MeasureComputerContext context) { + logger.info("Computing Mule SubFlow Size"); + + if (context.getComponent().getType() != Component.Type.FILE) { + int sumFlows = 0; + for (Measure child : context.getChildrenMeasures(MuleMetrics.SUBFLOWS.key())) { + sumFlows += child.getIntValue(); + } + context.addMeasure(MuleMetrics.SUBFLOWS.key(), sumFlows); + } + } +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleTransformationCount.java b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleTransformationCount.java new file mode 100644 index 0000000..79b81e7 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/measures/MuleTransformationCount.java @@ -0,0 +1,39 @@ +package com.mulesoft.services.tools.sonarqube.measures; + +import org.sonar.api.ce.measure.Component; +import org.sonar.api.ce.measure.Measure; +import org.sonar.api.ce.measure.MeasureComputer; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.metrics.MuleMetrics; + +/** + * Computes the total number of transformations + * @author franco.perez + * + */ +public class MuleTransformationCount implements MeasureComputer { + private final Logger logger = Loggers.get(this.getClass()); + + @Override + public MeasureComputerDefinition define(MeasureComputerDefinitionContext defContext) { + return defContext.newDefinitionBuilder().setOutputMetrics(MuleMetrics.TRANSFORMATIONS.key()).build(); + } + + @Override + public void compute(MeasureComputerContext context) { + logger.info("Computing Mule Transformation Count"); + + if (context.getComponent().getType() != Component.Type.FILE) { + int sumFlows = 0; + for (Measure child : context.getChildrenMeasures(MuleMetrics.TRANSFORMATIONS.key())) { + sumFlows += child.getIntValue(); + } + context.addMeasure(MuleMetrics.TRANSFORMATIONS.key(), sumFlows); + + } + + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/AbstractMuleSensor.java b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/AbstractMuleSensor.java new file mode 100644 index 0000000..6162c83 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/AbstractMuleSensor.java @@ -0,0 +1,29 @@ +package com.mulesoft.services.tools.sonarqube.metrics; + +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; + +import com.mulesoft.services.tools.sonarqube.filter.MuleFilePredicate; +import com.mulesoft.services.tools.sonarqube.sensor.MuleSensor; + +public abstract class AbstractMuleSensor implements Sensor { + + SAXBuilder saxBuilder = new SAXBuilder(); + + @Override + public void execute(SensorContext context) { + + FileSystem fs = context.fileSystem(); + // Only ConfigurationFiles + Iterable files = fs.inputFiles(new MuleFilePredicate()); + for (InputFile file : files) { + process(context, file, MuleSensor.getLanguage(context)); + } + } + + protected abstract void process(SensorContext context, InputFile file, String language); + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/ConfigurationFilesSensor.java b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/ConfigurationFilesSensor.java new file mode 100644 index 0000000..3439b98 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/ConfigurationFilesSensor.java @@ -0,0 +1,78 @@ +package com.mulesoft.services.tools.sonarqube.metrics; + +import java.io.IOException; + +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.measure.Metric; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.measure.NewMeasure; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.properties.MuleProperties; +import com.mulesoft.services.xpath.XPathProcessor; + +public class ConfigurationFilesSensor extends AbstractMuleSensor { + + private final Logger logger = Loggers.get(ConfigurationFilesSensor.class); + SAXBuilder saxBuilder = new SAXBuilder(); + + private static final String METRIC_FLOW_PROPERTY = "mule.metric.flow"; + private static final String METRIC_SUBFLOW_PROPERTY = "mule.metric.subflow"; + private static final String METRIC_DW_PAYLOAD_PROPERTY = "mule.metric.dw.payload"; + private static final String METRIC_DW_VARIABLE_PROPERTY = "mule.metric.dw.variable"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Compute size of configuration file"); + } + + @Override + protected void process(SensorContext context, InputFile file, String language) { + try { + Document document = saxBuilder.build(file.inputStream()); + Element rootElement = document.getRootElement(); + XPathProcessor xpathProcessor = new XPathProcessor() + .loadNamespaces(language.equals(MuleLanguage.LANGUAGE_MULE4_KEY) ? "namespace-4.properties" + : "namespace-3.properties"); + + saveMetric(xpathProcessor, MuleMetrics.FLOWS, context, file, rootElement, + MuleProperties.getProperties(language).get(METRIC_FLOW_PROPERTY).toString()); + + saveMetric(xpathProcessor, MuleMetrics.SUBFLOWS, context, file, rootElement, + MuleProperties.getProperties(language).get(METRIC_SUBFLOW_PROPERTY).toString()); + + // Lines of code = Lines in Mule + saveMetric(file.lines(), CoreMetrics.NCLOC, context, file); + + saveMetric(xpathProcessor, MuleMetrics.TRANSFORMATIONS, context, file, rootElement, + MuleProperties.getProperties(language).get(METRIC_DW_PAYLOAD_PROPERTY).toString(), + MuleProperties.getProperties(language).get(METRIC_DW_VARIABLE_PROPERTY).toString()); + + } catch (JDOMException | IOException e) { + logger.error(e.getMessage(), e); + } + } + + private void saveMetric(XPathProcessor helper, Metric metric, SensorContext context, InputFile file, + Element rootElement, String... xpathExpressions) { + int result = 0; + for (int i = 0; i < xpathExpressions.length; i++) { + result += helper.processXPath(xpathExpressions[i], rootElement, Double.class).intValue(); + } + saveMetric(result, metric, context, file); + } + + private void saveMetric(int result, Metric metric, SensorContext context, InputFile file) { + NewMeasure metrics = context.newMeasure(); + metrics.forMetric(metric).on(file).withValue(result).save(); + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/CoverageSensor.java b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/CoverageSensor.java new file mode 100644 index 0000000..9cdcafc --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/CoverageSensor.java @@ -0,0 +1,155 @@ +package com.mulesoft.services.tools.sonarqube.metrics; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Scanner; + +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.coverage.NewCoverage; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.mulesoft.services.tools.sonarqube.filter.MuleFilePredicate; +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.properties.MuleProperties; +import com.mulesoft.services.tools.sonarqube.sensor.MuleSensor; + +public class CoverageSensor implements Sensor { + + private final Logger logger = Loggers.get(CoverageSensor.class); + private static final String MUNIT_NAME_PROPERTY = "mule.munit.properties.name"; + private static final String MUNIT_FLOWS_PROPERTY = "mule.munit.properties.flows"; + private static final String MUNIT_FILES_PROPERTY = "mule.munit.properties.files"; + private static final String MUNIT_LINES_PROPERTY = "mule.munit.properties.lines"; + private static final String MUNIT_COVERAGE_PROPERTY = "mule.munit.properties.coverage"; + private static final String MUNIT_PROCESSOR_COUNT = "mule.munit.properties.processorCount"; + private static final String MUNIT_COVERED_PROCESSOR_COUNT = "mule.munit.properties.coveredProcessorCount"; + private static final String MUNIT_LINE_NUMBER = "mule.munit.properties.lineNumber"; + private static final String MUNIT_COVERED = "mule.munit.properties.covered"; + + ObjectMapper objectMapper = new ObjectMapper(); + + private static final String MUNIT_REPORT_MULE_3 = java.io.File.separator + "target" + java.io.File.separator + + "munit-reports" + java.io.File.separator + "coverage-json" + java.io.File.separator + "report.json"; + private static final String MUNIT_REPORT_MULE_4 = java.io.File.separator + "target" + java.io.File.separator + + "site" + java.io.File.separator + "munit" + java.io.File.separator + "coverage" + java.io.File.separator + + "munit-coverage.json"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Compute the coverage of the applications"); + descriptor.onlyOnLanguage(MuleLanguage.LANGUAGE_KEY); + } + + @Override + public void execute(SensorContext context) { + File munitJsonReport = new File(context.fileSystem().baseDir() + + (MuleSensor.getLanguage(context).equals(MuleLanguage.LANGUAGE_MULE4_KEY) ? MUNIT_REPORT_MULE_4 + : MUNIT_REPORT_MULE_3)); + if (munitJsonReport.exists()) { + Map coverage = loadResults( + MuleProperties.getProperties(MuleSensor.getLanguage(context)), munitJsonReport); + FileSystem fs = context.fileSystem(); + // Only ConfigurationFiles + Iterable files = fs.inputFiles(new MuleFilePredicate()); + for (InputFile file : files) { + saveCoverage(coverage, file.filename(), context, file); + } + } + } + + private Map loadResults(Properties props, File munitJsonReport) { + Map coverageMap = null; + try (Scanner scanner = new Scanner(munitJsonReport)) { + + String jsonInput = scanner.useDelimiter("\\Z").next(); + JsonNode root = objectMapper.readTree(jsonInput); + String coverage = root.get(props.getProperty(MUNIT_COVERAGE_PROPERTY)).asText(); + if (logger.isDebugEnabled()) { + logger.debug("Total Coverage :" + coverage); + } + ArrayNode files = (ArrayNode) root.get(props.getProperty(MUNIT_FILES_PROPERTY)); + coverageMap = new HashMap(); + for (Iterator iterator = files.iterator(); iterator.hasNext();) { + JsonNode node = iterator.next(); + String name = node.get(props.getProperty(MUNIT_NAME_PROPERTY)).asText(); + FlowCoverageCounter counter = new FlowCoverageCounter(); + ArrayNode flows = (ArrayNode) node.get(props.getProperty(MUNIT_FLOWS_PROPERTY)); + for (Iterator flowsIterator = flows.iterator(); flowsIterator.hasNext();) { + JsonNode flowNode = flowsIterator.next(); + int messageProcessorCount = flowNode.get(props.getProperty(MUNIT_PROCESSOR_COUNT)).asInt(); + int coveredProcessorCount = flowNode.get(props.getProperty(MUNIT_COVERED_PROCESSOR_COUNT)).asInt(); + counter.addProcessors(messageProcessorCount); + counter.addCoveredProcessors(coveredProcessorCount); + ArrayNode lines = (ArrayNode) flowNode.get(props.getProperty(MUNIT_LINES_PROPERTY)); + if (lines != null) { + counter.setHasLineNumbers(true); + for (Iterator linesIterator = lines.iterator(); linesIterator.hasNext();) { + JsonNode linesNode = linesIterator.next(); + int lineNumber = linesNode.get(props.getProperty(MUNIT_LINE_NUMBER)).asInt(); + boolean covered = linesNode.get(props.getProperty(MUNIT_COVERED)).asBoolean(); + + if (covered) { + counter.getCoveredLines().add(lineNumber); + } else { + counter.getNotcoveredLines().add(lineNumber); + } + } + } + } + String[] fileParts = name.split(File.separator); + coverageMap.put(fileParts[fileParts.length - 1], counter); + if (logger.isDebugEnabled()) { + logger.debug("name :" + node.get("name") + " : coverage:" + node.get("coverage")); + } + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + return coverageMap; + + } + + private void saveCoverage(Map coverageMap, String fileName, SensorContext context, + InputFile file) { + + FlowCoverageCounter coverage = coverageMap.get(fileName); + if (coverage != null) { + NewCoverage newCoverage = context.newCoverage().onFile(file); + + if (coverage.hasLineNumbers()) { + for (Iterator iterator = coverage.getCoveredLines().iterator(); iterator.hasNext();) { + Integer lineNumber = iterator.next(); + newCoverage.lineHits(lineNumber, 1); + } + for (Iterator iterator = coverage.getNotcoveredLines().iterator(); iterator.hasNext();) { + Integer lineNumber = iterator.next(); + newCoverage.lineHits(lineNumber, 0); + } + newCoverage.save(); + } else { + int processors = coverage.getProcessors(); + int covered = coverage.getCoveredProcessors(); + + for (int i = 0; i < processors; i++) { + newCoverage.lineHits(i + 1, covered > 0 ? 1 : 0); + covered--; + } + + newCoverage.save(); + } + } + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/FlowCoverageCounter.java b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/FlowCoverageCounter.java new file mode 100644 index 0000000..b613ef2 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/FlowCoverageCounter.java @@ -0,0 +1,52 @@ +package com.mulesoft.services.tools.sonarqube.metrics; + +import java.util.ArrayList; + +public class FlowCoverageCounter { + + private int processors = 0; + private int coveredProcessors = 0; + private boolean hasLineNumbers = false; + private ArrayList coveredLines = new ArrayList(); + private ArrayList notcoveredLines = new ArrayList(); + + public void addProcessors(int processors) { + this.processors += processors; + } + + public void addCoveredProcessors(int processors) { + this.coveredProcessors += processors; + } + + public int getCoveredProcessors() { + return coveredProcessors; + } + + public int getProcessors() { + return processors; + } + + public void setCoveredLines(ArrayList coveredLines) { + this.coveredLines = coveredLines; + } + + public ArrayList getCoveredLines() { + return coveredLines; + } + + public ArrayList getNotcoveredLines() { + return notcoveredLines; + } + + public void setNotcoveredLines(ArrayList notcoveredLines) { + this.notcoveredLines = notcoveredLines; + } + + public void setHasLineNumbers(boolean hasLineNumbers) { + this.hasLineNumbers = hasLineNumbers; + } + + public boolean hasLineNumbers() { + return hasLineNumbers; + } +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/MUnitSensor.java b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/MUnitSensor.java new file mode 100644 index 0000000..3858648 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/MUnitSensor.java @@ -0,0 +1,58 @@ +package com.mulesoft.services.tools.sonarqube.metrics; + +import java.io.IOException; + +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.measure.Metric; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.measure.NewMeasure; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.xpath.XPathProcessor; + +public class MUnitSensor extends AbstractMuleSensor { + + private final Logger logger = Loggers.get(MUnitSensor.class); + + SAXBuilder saxBuilder = new SAXBuilder(); + + private static final String UNIT_TESTS_XPATH_EXPRESSION = "count(//mule:mule/munit:test)"; + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.name("Compute number of unit test cases"); + } + + @Override + protected void process(SensorContext context, InputFile file, String language) { + try { + Document document = saxBuilder.build(file.inputStream()); + Element rootElement = document.getRootElement(); + XPathProcessor xpathProcessor = new XPathProcessor() + .loadNamespaces(language.equals(MuleLanguage.LANGUAGE_MULE4_KEY) ? "namespace-4.properties" + : "namespace-3.properties"); + saveMetric(xpathProcessor, CoreMetrics.TESTS, context, file, rootElement, UNIT_TESTS_XPATH_EXPRESSION); + } catch (JDOMException | IOException e) { + logger.error(e.getMessage(), e); + } + } + + private void saveMetric(XPathProcessor helper, Metric metric, SensorContext context, InputFile file, + Element rootElement, String... xpathExpressions) { + int result = 0; + for (int i = 0; i < xpathExpressions.length; i++) { + result += helper.processXPath(xpathExpressions[i], rootElement, Double.class).intValue(); + } + NewMeasure metrics = context.newMeasure(); + metrics.forMetric(metric).on(file).withValue(result).save(); + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/MuleMetrics.java b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/MuleMetrics.java new file mode 100644 index 0000000..26de73e --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/metrics/MuleMetrics.java @@ -0,0 +1,41 @@ +package com.mulesoft.services.tools.sonarqube.metrics; + +import java.util.Arrays; +import java.util.List; + +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.Metrics; + +public class MuleMetrics implements Metrics { + + public static final Metric FLOWS = new Metric.Builder("mule_flows", "Flows", Metric.ValueType.INT) + .setDescription("Number of flows").setDirection(Metric.DIRECTION_WORST).setQualitative(false) + .setDomain(org.sonar.api.measures.CoreMetrics.DOMAIN_SIZE).create(); + + public static final Metric SUBFLOWS = new Metric.Builder("mule_subflows", "SubFlows", Metric.ValueType.INT) + .setDescription("Number of subflows").setDirection(Metric.DIRECTION_WORST).setQualitative(false) + .setDomain(org.sonar.api.measures.CoreMetrics.DOMAIN_SIZE).create(); + + public static final Metric TRANSFORMATIONS = new Metric.Builder("mule_dw", "DW Transformations", + Metric.ValueType.INT).setDescription("Number of subflows").setDirection(Metric.DIRECTION_WORST) + .setQualitative(false).setDomain(org.sonar.api.measures.CoreMetrics.DOMAIN_SIZE).create(); + + public static final Metric CONFIGURATION_FILES_COMP_RATING = new Metric.Builder( + "mule_configuration_complexity_rating", "Configuration File Complexity", Metric.ValueType.RATING) + .setDescription("Rating based on complexity of configuration file") + .setDirection(Metric.DIRECTION_BETTER).setQualitative(true).setDomain(CoreMetrics.DOMAIN_COMPLEXITY) + .create(); + + public static final Metric CONFIGURATION_FILES = new Metric.Builder("mule_configuration_files", + "Configuration Files", Metric.ValueType.INT).setDescription("Number of configuration files") + .setDirection(Metric.DIRECTION_WORST).setQualitative(false) + .setDomain(org.sonar.api.measures.CoreMetrics.DOMAIN_SIZE).create(); + + @SuppressWarnings("rawtypes") + @Override + public List getMetrics() { + return Arrays.asList(FLOWS, SUBFLOWS, CONFIGURATION_FILES_COMP_RATING, TRANSFORMATIONS, CONFIGURATION_FILES); + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/profile/MuleQualityProfile.java b/src/main/java/com/mulesoft/services/tools/sonarqube/profile/MuleQualityProfile.java new file mode 100644 index 0000000..7a3c30c --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/profile/MuleQualityProfile.java @@ -0,0 +1,69 @@ +package com.mulesoft.services.tools.sonarqube.profile; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.rule.MuleRulesDefinition; +import com.mulesoft.services.tools.validation.consumer.RuleFactory; +import com.mulesoft.services.tools.validation.rules.Ruleset; +import com.mulesoft.services.tools.validation.rules.Rulestore; + +/** + * Mule Quality Profile + * + * @author franco.perez + * + */ +public class MuleQualityProfile implements BuiltInQualityProfilesDefinition { + + Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + public void define(Context context) { + if (logger.isDebugEnabled()) + logger.debug("Creating MuleSoft Profiles"); + + // Mule3 + NewBuiltInQualityProfile profile3 = context.createBuiltInQualityProfile("MuleSoft Rules for Mule 3.x", + MuleLanguage.LANGUAGE_KEY); + // profile3.setDefault(true); + activeRule(profile3, "mule3-repository", "file:extensions/plugins/rules-3.xml"); + profile3.done(); + + // Mule4 + NewBuiltInQualityProfile profile4 = context.createBuiltInQualityProfile("MuleSoft Rules for Mule 4.x", + MuleLanguage.LANGUAGE_KEY); + profile4.setDefault(true); + activeRule(profile4, "mule4-repository", "file:extensions/plugins/rules-4.xml"); + profile4.done(); + } + + private void activeRule(NewBuiltInQualityProfile profile, String repositoryKey, String ruleFilename) { + try { + Rulestore rules = RuleFactory.loadRulesFromXml(ruleFilename); + List rulesetList = rules.getRuleset(); + + for (Iterator iterator = rulesetList.iterator(); iterator.hasNext();) { + Ruleset ruleset = iterator.next(); + List ruleList = ruleset.getRule(); + for (Iterator ruleIterator = ruleList + .iterator(); ruleIterator.hasNext();) { + com.mulesoft.services.tools.validation.rules.Rule rule = ruleIterator.next(); + profile.activateRule(repositoryKey, MuleRulesDefinition.getRuleKey(ruleset, rule)); + } + } + } catch (JAXBException | IOException e) { + logger.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/properties/MuleProperties.java b/src/main/java/com/mulesoft/services/tools/sonarqube/properties/MuleProperties.java new file mode 100644 index 0000000..c3fdb75 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/properties/MuleProperties.java @@ -0,0 +1,49 @@ +package com.mulesoft.services.tools.sonarqube.properties; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; + +public class MuleProperties { + + private static final String MULE_PROP = "mule.properties"; + private static final String MULE4_PROP = "mule4.properties"; + private static final String MULE3_PROP = "mule3.properties"; + + static private Logger logger = LoggerFactory.getLogger(MuleProperties.class); + + static private Map props = new HashMap(); + + private static Properties loadProp(String language, String propName) { + Properties properties = new Properties(); + try (InputStream input = MuleProperties.class.getClassLoader().getResourceAsStream(propName)) { + properties.load(input); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + props.put(language, properties); + return properties; + } + + public static Properties getProperties(String language) { + if (props.containsKey(language)) { + return props.get(language); + } else { + switch (language) { + case MuleLanguage.LANGUAGE_MULE4_KEY: + return loadProp(language, MULE4_PROP); + case MuleLanguage.LANGUAGE_MULE3_KEY: + return loadProp(language, MULE3_PROP); + default: + return loadProp(language, MULE_PROP); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/rule/MuleRulesDefinition.java b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/MuleRulesDefinition.java new file mode 100644 index 0000000..fbc44cf --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/MuleRulesDefinition.java @@ -0,0 +1,174 @@ +package com.mulesoft.services.tools.sonarqube.rule; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +import javax.xml.bind.JAXBException; + +import org.sonar.api.rule.RuleStatus; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.api.server.rule.RuleParamType; +import org.sonar.api.server.rule.RulesDefinition; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.properties.MuleProperties; +import com.mulesoft.services.tools.validation.Constants; +import com.mulesoft.services.tools.validation.Constants.Severities; +import com.mulesoft.services.tools.validation.Constants.Types; +import com.mulesoft.services.tools.validation.consumer.RuleFactory; +import com.mulesoft.services.tools.validation.rules.Ruleset; +import com.mulesoft.services.tools.validation.rules.Rulestore; + +public class MuleRulesDefinition implements RulesDefinition { + + private final Logger logger = Loggers.get(MuleRulesDefinition.class); + + public static String MULE3_REPOSITORY_KEY = "mule3-repository"; + public static String MULE4_REPOSITORY_KEY = "mule4-repository"; + public static final String RULE_TEMPLATE_KEY = "mule-rule-template"; + + public interface PARAMS { + String CATEGORY = "category"; + String SCOPE = "scope"; + String XPATH = "xpath-expression"; + } + + @Override + public void define(Context context) { + if (logger.isDebugEnabled()) + logger.debug("Defining MuleSoft Rule - Based on Mule-Validation-XPath-Core rules"); + + logger.info("Working Directory = {}", System.getProperty("user.dir")); + + createRepository(context, MULE3_REPOSITORY_KEY, MuleLanguage.LANGUAGE_KEY, "Mule3 Analyzer", + "file:extensions/plugins/rules-3.xml"); + createRepository(context, MULE4_REPOSITORY_KEY, MuleLanguage.LANGUAGE_KEY, "Mule4 Analyzer", + "file:extensions/plugins/rules-4.xml"); + + } + + private void createRepository(Context context, String repositoryKey, String language, String repositoryName, + String ruleFilename) { + NewRepository repository = context.createRepository(repositoryKey, language).setName(repositoryName); + try { + Rulestore rulestore = RuleFactory.loadRulesFromXml(ruleFilename); + List rulesetList = rulestore.getRuleset(); + + for (Iterator iterator = rulesetList.iterator(); iterator.hasNext();) { + Ruleset ruleset = iterator.next(); + logger.debug("Rule Category : " + ruleset.getCategory()); + List ruleList = ruleset.getRule(); + for (Iterator ruleIterator = ruleList + .iterator(); ruleIterator.hasNext();) { + com.mulesoft.services.tools.validation.rules.Rule rule = ruleIterator.next(); + logger.debug("Rule Id : " + rule.getId()); + addRule(repository, ruleset, rule, language); + } + + } + + } catch (JAXBException | IOException e) { + logger.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + + addRuleTemplate(repository, language); + repository.rule(RULE_TEMPLATE_KEY).setTemplate(true); + // don't forget to call done() to finalize the definition + repository.done(); + + } + + private void addRuleTemplate(NewRepository repository, String language) { + + Properties prop = (repository.key().equals(MULE3_REPOSITORY_KEY) + ? MuleProperties.getProperties(MuleLanguage.LANGUAGE_MULE3_KEY) + : MuleProperties.getProperties(MuleLanguage.LANGUAGE_MULE4_KEY)); + NewRule x1Rule = repository.createRule(RULE_TEMPLATE_KEY).setName(prop.getProperty("rule.template.name")) + .setHtmlDescription(prop.getProperty("rule.template.description")); + + x1Rule.addTags(language); + x1Rule.createParam(PARAMS.CATEGORY).setDescription(prop.getProperty("rule.template.parameter.category")) + .setType(RuleParamType.STRING); + x1Rule.createParam(PARAMS.XPATH).setDescription(prop.getProperty("rule.template.parameter.xpath")) + .setType(RuleParamType.STRING); + x1Rule.createParam(PARAMS.SCOPE).setDescription(prop.getProperty("rule.template.parameter.scope")) + .setType(RuleParamType.STRING); + } + + private void addRule(NewRepository repository, Ruleset ruleset, + com.mulesoft.services.tools.validation.rules.Rule rule, String language) { + + NewRule x1Rule = repository.createRule(MuleRulesDefinition.getRuleKey(ruleset, rule)).setName(rule.getName()) + .setHtmlDescription(rule.getDescription()).setActivatedByDefault(true) + .setHtmlDescription(rule.getDescription()).setStatus(RuleStatus.READY); + + x1Rule.setSeverity(getSeverity(rule)); + x1Rule.setType(getType(rule)); + x1Rule.addTags(language); + x1Rule.createParam(PARAMS.CATEGORY).setDefaultValue(ruleset.getCategory()).setType(RuleParamType.STRING); + x1Rule.createParam(PARAMS.XPATH).setDefaultValue(rule.getValue()).setType(RuleParamType.STRING); + if (rule.getApplies() != null) { + x1Rule.createParam(PARAMS.SCOPE).setDefaultValue(rule.getApplies()).setType(RuleParamType.STRING); + } + + } + + private String getSeverity(com.mulesoft.services.tools.validation.rules.Rule rule) { + Severities sev = Severities.valueOf(rule.getSeverity()); + String severity; + switch (sev) { + case BLOCKER: + severity = Severity.BLOCKER; + break; + case CRITICAL: + severity = Severity.CRITICAL; + break; + case MAJOR: + severity = Severity.MAJOR; + break; + case MINOR: + severity = Severity.MINOR; + break; + case INFO: + severity = Severity.INFO; + break; + default: + severity = Severity.INFO; + break; + } + return severity; + } + + private RuleType getType(com.mulesoft.services.tools.validation.rules.Rule rule) { + Types types = Types + .valueOf((rule.getType() != null ? rule.getType().toUpperCase() : Constants.Type.BUG.toUpperCase())); + RuleType type; + switch (types) { + case BUG: + type = RuleType.BUG; + break; + case CODE_SMELL: + type = RuleType.CODE_SMELL; + break; + case VULNERABILITY: + type = RuleType.VULNERABILITY; + break; + default: + type = RuleType.BUG; + break; + } + return type; + } + + public static String getRuleKey(Ruleset ruleset, com.mulesoft.services.tools.validation.rules.Rule rule) { + return ruleset.getCategory() + "." + rule.getId(); + + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ApplicationStrategyScope.java b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ApplicationStrategyScope.java new file mode 100644 index 0000000..86f4645 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ApplicationStrategyScope.java @@ -0,0 +1,70 @@ +package com.mulesoft.services.tools.sonarqube.rule.scope; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.rule.MuleRulesDefinition; +import com.mulesoft.services.xpath.XPathProcessor; + +public class ApplicationStrategyScope implements ScopeStrategy { + private final Logger logger = Loggers.get(ApplicationStrategyScope.class); + SAXBuilder saxBuilder = new SAXBuilder(); + Set valids = new HashSet(); + + @Override + public void validate(XPathProcessor xpathValidator, Map> issues, SensorContext context, + InputFile t, ActiveRule rule) { + try { + Document document = saxBuilder.build(t.inputStream()); + Element rootElement = document.getRootElement(); + String ruleId = rule.ruleKey().toString(); + boolean valid = xpathValidator.processXPath(rule.param(MuleRulesDefinition.PARAMS.XPATH).trim(), + rootElement, Boolean.class).booleanValue(); + logger.info("Validation Result: " + valid + " : File: " + t.filename() + " :Rule:" + rule.ruleKey()); + if (!valid && !valids.contains(ruleId) && !issues.containsKey(rule.ruleKey())) { + NewIssue newIssue = context.newIssue().forRule(rule.ruleKey()); + NewIssueLocation primaryLocation = newIssue.newLocation().on(t); + newIssue.at(primaryLocation); + addIssue(issues, rule, newIssue); + } else { + if (valid && !valids.contains(ruleId)) { + valids.add(ruleId); + issues.remove(rule.ruleKey()); + } + } + + } catch (JDOMException | IOException e) { + logger.error(e.getMessage(), e); + } + } + + private void addIssue(Map> issues, ActiveRule rule, NewIssue issue) { + + if (issues.containsKey(rule.ruleKey())) { + issues.get(rule.ruleKey()).add(issue); + } else { + List issuesList = new ArrayList(); + issuesList.add(issue); + issues.put(rule.ruleKey(), issuesList); + } + + } + +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/FileStrategyScope.java b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/FileStrategyScope.java new file mode 100644 index 0000000..89428ac --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/FileStrategyScope.java @@ -0,0 +1,60 @@ +package com.mulesoft.services.tools.sonarqube.rule.scope; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.batch.sensor.issue.NewIssueLocation; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.rule.MuleRulesDefinition; +import com.mulesoft.services.xpath.XPathProcessor; + +public class FileStrategyScope implements ScopeStrategy { + private final Logger logger = Loggers.get(FileStrategyScope.class); + SAXBuilder saxBuilder = new SAXBuilder(); + + @Override + public void validate(XPathProcessor xpathValidator, Map> issues, SensorContext context, + InputFile t, ActiveRule rule) { + try { + Document document = saxBuilder.build(t.inputStream()); + Element rootElement = document.getRootElement(); + boolean valid = xpathValidator.processXPath(rule.param(MuleRulesDefinition.PARAMS.XPATH).trim(), + rootElement, Boolean.class).booleanValue(); + logger.info("Validation Result: " + valid + " : File: " + t.filename() + " :Rule:" + rule.ruleKey()); + if (!valid) { + NewIssue newIssue = context.newIssue().forRule(rule.ruleKey()); + NewIssueLocation primaryLocation = newIssue.newLocation().on(t); + newIssue.at(primaryLocation); + addIssue(issues, rule, newIssue); + } + + } catch (JDOMException | IOException e) { + logger.error(e.getMessage(), e); + } + } + + private void addIssue(Map> issues, ActiveRule rule, NewIssue issue) { + + if (issues.containsKey(rule.ruleKey())) { + issues.get(rule.ruleKey()).add(issue); + } else { + List issuesList = new ArrayList(); + issuesList.add(issue); + issues.put(rule.ruleKey(), issuesList); + } + + } +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ScopeFactory.java b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ScopeFactory.java new file mode 100644 index 0000000..aa07a82 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ScopeFactory.java @@ -0,0 +1,32 @@ +package com.mulesoft.services.tools.sonarqube.rule.scope; + +import java.util.HashMap; +import java.util.Map; + +import com.mulesoft.services.tools.validation.Constants; + +public class ScopeFactory { + + private static ScopeFactory instance = null; + + private Map strategies = new HashMap(); + + public static ScopeFactory getInstance() { + if (instance == null) { + instance = new ScopeFactory(); + } + return instance; + } + + public ScopeFactory() { + strategies.put(ScopeStrategy.FILE, new FileStrategyScope()); + strategies.put(ScopeStrategy.APPLICATION, new ApplicationStrategyScope()); + } + + public ScopeStrategy getStrategy(String scope) { + if (Constants.Applicability.APPLICATION.equals(scope)) { + return strategies.get(ScopeStrategy.APPLICATION); + } + return strategies.get(ScopeStrategy.FILE); + } +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ScopeStrategy.java b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ScopeStrategy.java new file mode 100644 index 0000000..b20ef72 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/rule/scope/ScopeStrategy.java @@ -0,0 +1,20 @@ +package com.mulesoft.services.tools.sonarqube.rule.scope; + +import java.util.List; +import java.util.Map; + +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.rule.RuleKey; + +import com.mulesoft.services.xpath.XPathProcessor; + +public interface ScopeStrategy { + + String FILE = "file"; + String APPLICATION = "application"; + + public void validate(XPathProcessor xpathValidator, Map> issues, SensorContext context, InputFile t, ActiveRule rule); +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/sensor/MuleSensor.java b/src/main/java/com/mulesoft/services/tools/sonarqube/sensor/MuleSensor.java new file mode 100644 index 0000000..b8de4d6 --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/sensor/MuleSensor.java @@ -0,0 +1,67 @@ +package com.mulesoft.services.tools.sonarqube.sensor; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.jdom2.input.SAXBuilder; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.filter.MuleFilePredicate; +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.rule.MuleRulesDefinition; + +/** + * Mule Sensor Iterates over all mule files and applies the corresponding rules + * + * @author franco.perez + * + */ +public class MuleSensor implements Sensor { + + private final Logger logger = Loggers.get(MuleSensor.class); + + SAXBuilder saxBuilder = new SAXBuilder(); + + @Override + public void describe(SensorDescriptor descriptor) { + descriptor.onlyOnLanguage(MuleLanguage.LANGUAGE_KEY); + descriptor.createIssuesForRuleRepositories(MuleRulesDefinition.MULE3_REPOSITORY_KEY, + MuleRulesDefinition.MULE4_REPOSITORY_KEY); + } + + @Override + public void execute(SensorContext context) { + if (logger.isDebugEnabled()) { + logger.debug("Executing Mule Sensor"); + } + + FileSystem fs = context.fileSystem(); + + Map> issues = new HashMap>(); + fs.inputFiles(new MuleFilePredicate()).forEach(new SonarRuleConsumer(getLanguage(context), context, issues)); + + // Iterate and save all the issues + for (Iterator> newIssueListIterator = issues.values().iterator(); newIssueListIterator + .hasNext();) { + List newIssueList = newIssueListIterator.next(); + for (Iterator newIssueIterator = newIssueList.iterator(); newIssueIterator.hasNext();) { + NewIssue newIssue = newIssueIterator.next(); + newIssue.save(); + } + } + } + + public static String getLanguage(SensorContext context) { + boolean mule4 = context.activeRules().findByRepository(MuleRulesDefinition.MULE3_REPOSITORY_KEY).isEmpty(); + return mule4 ? MuleLanguage.LANGUAGE_MULE4_KEY : MuleLanguage.LANGUAGE_MULE3_KEY; + } +} diff --git a/src/main/java/com/mulesoft/services/tools/sonarqube/sensor/SonarRuleConsumer.java b/src/main/java/com/mulesoft/services/tools/sonarqube/sensor/SonarRuleConsumer.java new file mode 100644 index 0000000..ae69a1f --- /dev/null +++ b/src/main/java/com/mulesoft/services/tools/sonarqube/sensor/SonarRuleConsumer.java @@ -0,0 +1,98 @@ +package com.mulesoft.services.tools.sonarqube.sensor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.batch.rule.ActiveRule; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.issue.NewIssue; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +import com.mulesoft.services.tools.sonarqube.language.MuleLanguage; +import com.mulesoft.services.tools.sonarqube.properties.MuleProperties; +import com.mulesoft.services.tools.sonarqube.rule.MuleRulesDefinition; +import com.mulesoft.services.tools.sonarqube.rule.scope.ScopeFactory; +import com.mulesoft.services.xpath.XPathProcessor; + +/** + * Consumer to validate an InputFile against all the rules + * + * @author franco.parma + * + */ +public class SonarRuleConsumer implements Consumer { + + private final Logger logger = Loggers.get(SonarRuleConsumer.class); + SensorContext context; + Map> issues; + String language; + XPathProcessor xpathProcessor; + List filteredCategories = new ArrayList(); + + private static final String FILTER_RULESET_PROPERTY = "sonar.property.ruleset.categories"; + + public SonarRuleConsumer(String language, SensorContext context, Map> issues) { + this.language = language; + this.context = context; + this.issues = issues; + xpathProcessor = new XPathProcessor().loadNamespaces( + language.equals(MuleLanguage.LANGUAGE_MULE4_KEY) ? "namespace-4.properties" : "namespace-3.properties"); + + String categories = (String) MuleProperties.getProperties(MuleLanguage.LANGUAGE_KEY) + .get(FILTER_RULESET_PROPERTY); + Optional categoriesVariable = this.context.config().get(categories); + if (categoriesVariable.isPresent()) { + filteredCategories = Arrays.asList(categoriesVariable.get().split(",")); + } + } + + @Override + public void accept(InputFile t) { + if (logger.isDebugEnabled()) { + logger.debug("Validating file:" + t.filename()); + } + + Collection activeRules = this.context.activeRules().findByLanguage(MuleLanguage.LANGUAGE_KEY); + + Iterator iterator = rulesIterator(activeRules); + + while (iterator.hasNext()) { + ActiveRule rule = iterator.next(); + if (logger.isDebugEnabled()) { + logger.debug("Validating rule:" + rule.internalKey()); + } + String appliesTo = rule.param(MuleRulesDefinition.PARAMS.SCOPE); + + ScopeFactory.getInstance().getStrategy(appliesTo).validate(xpathProcessor, issues, context, t, rule); + } + } + + private Iterator rulesIterator(Collection activeRules) { + Iterator iterator; + if (!filteredCategories.isEmpty()) { + iterator = activeRules.stream().filter(new CategoryPredicate()).iterator(); + } else { + iterator = activeRules.iterator(); + } + return iterator; + } + + class CategoryPredicate implements Predicate { + @Override + public boolean test(ActiveRule t) { + String ruleCategory = t.param(MuleRulesDefinition.PARAMS.CATEGORY); + return filteredCategories.contains(ruleCategory); + } + } + +} diff --git a/src/main/resources/com/mulesoft/services/tools/sonarqube/profile/profile-mule.xml b/src/main/resources/com/mulesoft/services/tools/sonarqube/profile/profile-mule.xml new file mode 100755 index 0000000..d93bec3 --- /dev/null +++ b/src/main/resources/com/mulesoft/services/tools/sonarqube/profile/profile-mule.xml @@ -0,0 +1,4 @@ + + MuleSoft Profile + mule + \ No newline at end of file diff --git a/src/main/resources/mule.properties b/src/main/resources/mule.properties new file mode 100644 index 0000000..d37e7f3 --- /dev/null +++ b/src/main/resources/mule.properties @@ -0,0 +1,3 @@ +## CONFIGURATION +sonar.property.language=sonar.mule.language +sonar.property.ruleset.categories=sonar.mule.ruleset.categories diff --git a/src/main/resources/mule3.properties b/src/main/resources/mule3.properties new file mode 100644 index 0000000..5dab45f --- /dev/null +++ b/src/main/resources/mule3.properties @@ -0,0 +1,28 @@ +## PLUGIN +mule.munit.properties.name=name +mule.munit.properties.flows=flows +mule.munit.properties.files=files +mule.munit.properties.coverage=coverage +mule.munit.properties.processorCount=messageProcessorCount +mule.munit.properties.coveredProcessorCount=coveredMessageProcessorCount +mule.munit.properties.lines=lines +mule.munit.properties.lineNumber=lineNumber +mule.munit.properties.covered=covered + +## METRICS + +## CONFIGURATION +sonar.property.ruleset.categories=sonar.mule.ruleset.categories + +## Labels +rule.template.name=Track issues on Mule 3 +rule.template.description=Rule Template for mule 3 +rule.template.parameter.scope=The scope of the rules. It should either file or application +rule.template.parameter.xpath=XPath expression to be evaluated +rule.template.parameter.category=Category of the rule + + +mule.metric.dw.payload=count(//dw:transform-message/dw:set-payload) +mule.metric.dw.variable=count(//dw:transform-message/dw:set-variable) +mule.metric.flow=count(//mule:mule/mule:flow) +mule.metric.subflow=count(//mule:mule/mule:sub-flow) \ No newline at end of file diff --git a/src/main/resources/mule4.properties b/src/main/resources/mule4.properties new file mode 100644 index 0000000..3b76f62 --- /dev/null +++ b/src/main/resources/mule4.properties @@ -0,0 +1,28 @@ +## PLUGIN +mule.munit.properties.name=name +mule.munit.properties.flows=flows +mule.munit.properties.files=files +mule.munit.properties.coverage=coverage +mule.munit.properties.processorCount=messageProcessorCount +mule.munit.properties.coveredProcessorCount=coveredProcessorCount +mule.munit.properties.lines=lines +mule.munit.properties.lineNumber=lineNumber +mule.munit.properties.covered=covered + +## METRICS + +## CONFIGURATION +sonar.property.ruleset.categories=sonar.mule.ruleset.categories + +## Labels +rule.template.name=Track issues on Mule 4 +rule.template.description=Rule Template for mule 4 +rule.template.parameter.scope=The scope of the rules. It should either file or application +rule.template.parameter.xpath=XPath expression to be evaluated +rule.template.parameter.category=Category of the rule + + +mule.metric.dw.payload=count(//ee:transform/ee:message/ee:set-payload) +mule.metric.dw.variable=count(//ee:transform/ee:variables/ee:set-variable) +mule.metric.flow=count(//mule:mule/mule:flow) +mule.metric.subflow=count(//mule:mule/mule:sub-flow) diff --git a/src/test/java/com/mulesoft/services/tools/sonarqube/AppTest.java b/src/test/java/com/mulesoft/services/tools/sonarqube/AppTest.java new file mode 100644 index 0000000..25e96a6 --- /dev/null +++ b/src/test/java/com/mulesoft/services/tools/sonarqube/AppTest.java @@ -0,0 +1,38 @@ +package com.mulesoft.services.tools.sonarqube; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +}