Tuesday, November 24, 2020

Sample log4j properties

 Below is the sample " log4j.conf " property file

----------------------------------------------------------------------------------------------------------

#Root logger option
log4j.rootLogger=WARN, file, stdout

#library log files
log4j.logger.org.apache=ERROR
log4j.logger.com.nexa.ogt.light.configbridge=ERROR
log4j.logger.com.nexa.ogt.light=DEBUG
log4j.logger.org.eclipse.jgit=ERROR

# Direct log messages to a log file
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=./logs/logger.log
#log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.append=false
#log4j.appender.file.MaxBackupIndex=10
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %m%n

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-5p %m%n

Create Sonar Plugin - Scanner Basic - create Extractor

 Flow of our plugin is to 

  • Analyse the files using a third party app/jar which generate a xml/json violation/issues report.
  • read the xml/json report and save details as new issues along with respective Input files


  • Here we need  to bundle the third party app/jar along with out plugin jar.
  • Plugin jar will be installed to  SonarQube. (copy to "sonarqube-x.x\extensions\plugins" )  folder
  • Invoke scanner. (  "sonar-scanner" comand )
    • Plugin will be downloaded and start analysing and generate details inside work folder. 
    • Work folder will be flushed each time runs and everything will be deleted.
    • Therefore we need to copy the details to the work folder.
  • We need to do this manually.
  • Logic is handled using Extractor - extract files from plugin jar and copy back to work folder


-----------------------------------------------------------------------------------------------------------------------

package com.plugin.sonar.util;

import com.plugin.sonar.exception.FooPluginException;
import org.sonar.api.batch.InstantiationStrategy;
import org.sonar.api.batch.ScannerSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import static com.plugin.sonar.util.FooConstants.*;

@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
@ScannerSide()
public class Extractor {
private static final Logger LOG = Loggers.get(Extractor.class);

private static String exportResource(String resourceName, String folder) throws FooPluginException {
try (InputStream stream = Extractor.class.getResourceAsStream(resourceName)) {//note that each / is a directory down in the "jar tree" been the jar the root of the tree
if (stream == null) {
throw new FooPluginException("Cannot get resource \"" + resourceName + "\" from Jar file.");
}
createResource(resourceName, folder, stream);
} catch (Exception ex) {
LOG.debug(ex.getLocalizedMessage());
}

LOG.info("folder =" + folder + " resourceName=" + resourceName);
return folder + resourceName;
}

private static void createResource(String resourceName, String folder, InputStream stream) {
LOG.debug("resourceName=" + resourceName + "folder=" + folder + "stream=" + stream);
int readBytes;
byte[] buffer = new byte[4096];
try (OutputStream resStreamOut = new FileOutputStream(folder + resourceName)) {
while ((readBytes = stream.read(buffer)) > 0) {
resStreamOut.write(buffer, 0, readBytes);
}
} catch (Exception ex) {
LOG.debug(ex.getLocalizedMessage());
}
}

public void extractPlsql(String workDir) throws FooPluginException {
LOG.info("Calling::extractPlsql ");
exportResource(FooSQL_JAR, workDir);
exportResource(LOGIN_CONFIGURATION, workDir);
exportResource(INIT_PROPERTIES, workDir);
exportResource(FooSQL_SONAR_RULES, workDir);
exportResource(RULE_ENGINE_PROPERTIES, workDir);
exportResource(RULES, workDir);
}

}

Create Sonar Plugin - Scanner Basic - Convert Issues xml to Java

The flow of the plugin is
  • Call third party jar - command line execution of the jar
  • generate violation/issues xml
  • convert xml to java
  • for each file , get the Input file and set the violations and save as a new issue - create inside context.


Sample of violation report

---------------------------------------------------------------------------------------------------------------------

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<violations>
<file name="C:\sample\sample-project\src\main\java\sample.sql">
<violation>
<message>Avoid multiple declarations of labels and variables.</message>
<line>7</line>
<column>25</column>
<type>Tech-Defects</type>
<severity>1</severity>
<rule id="1" name="Avoid multiple declarations of labels and variables."/>
</violation>
</violations>


----------------------------------------------------------------------------------------------------------------------- 

we need to have a java file structure to map with xml tags. basically 3
  1. Violations
  2. File
  3. Violation - there can be multiples of violation.
  4. Rule
---------- classes as follows ---------------------------------------------------------------------------


package com.plugin.sonar.report.foo;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;

import java.util.ArrayList;
import java.util.List;

public class Violations {

@JacksonXmlElementWrapper(useWrapping = false)
private List<File> file = new ArrayList<>();

public List<File> getFile() {
return file;
}

public void setFile(List<File> file) {
this.file = file;
}

@Override
public String toString() {
return "Violations{" +
"file=" + file +
'}';
}
}

--------------------------------------------------------

package com.plugin.sonar.report.foo;

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlID;
import java.util.ArrayList;
import java.util.List;

@XmlAccessorType(XmlAccessType.FIELD)
public class File {
@XmlAttribute(name = "name")
@XmlID
private String name;

@JacksonXmlElementWrapper(useWrapping = false)
private List<Violation> violation = new ArrayList<>();

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Violation> getViolation() {
return violation;
}

public void setViolation(List<Violation> violation) {
this.violation = violation;
}

@Override
public String toString() {
return "File{" +
"name='" + name + '\'' +
", violation=" + violation +
'}';
}
}

----------------------------------------------------------------

package com.plugin.sonar.report.foo;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

@XmlAccessorType(XmlAccessType.FIELD)
public class Violation {
private String message;
private int line;
private int column;
private String type;
private String severity;
private Rule rule;

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public int getLine() {
return line;
}

public void setLine(int line) {
this.line = line;
}

public int getColumn() {
return column;
}

public void setColumn(int column) {
this.column = column;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getSeverity() {
return severity;
}

public void setSeverity(String severity) {
this.severity = severity;
}

public Rule getRule() {
return rule;
}

public void setRule(Rule rule) {
this.rule = rule;
}

@Override
public String toString() {
return "Violation{" +
"message='" + message + '\'' +
", line=" + line +
", column=" + column +
", type='" + type + '\'' +
", severity='" + severity + '\'' +
", rule=" + rule +
'}';
}
}

----------------------------------------------------------------------------

package com.plugin.sonar.report.foo;

import javax.xml.bind.annotation.XmlAttribute;

public class Rule {
@XmlAttribute(name = "id")
private String id;

@XmlAttribute(name = "name")
private String name;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Rule{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
-------------------------------------------------------------------------------
Now we have all  the objects

 We can write a class "Converter" to handle this process

----------------------------------------------------------------------------------------------------------

package com.plugin.sonar.util;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.plugin.sonar.report.foo.Violations;
import com.plugin.sonar.exception.FooPluginException;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

import java.io.File;

public final class Converter {

private static final Logger LOG = Loggers.get(Converter.class);

private Converter() {
}

public static Violations convertXMLToJava(String filePath) throws FooPluginException {
LOG.info("File path here " + filePath);
File xmlFile = new File(filePath);
try {
XmlMapper xmlMapper = new XmlMapper();
Violations violations = xmlMapper.readValue(xmlFile, Violations.class);
LOG.info(">>>>>>Converting violation xml to pojo \n" + violations.toString());
return violations;
} catch (Exception e) {
LOG.error(e.getLocalizedMessage(), e);
LOG.error("Error converting violation xml");
throw new FooPluginException("Error converting violation xml");
}
}
}

Create Sonar Plugin - Scanner Basic - Save Issues 1

 To save issues

  • Get the violations/ISsues for the files analyzed
  • For each file contains violations/ Issues
    • Get the violations ( one file may contain more than one violations or Issue )
      • Get the Input file and create new issue from context and save Issue

-------------------------------------------------------------------------------------------------

private void saveIssues() {
Violations violations = null;
try {
violations = getReport();
LOG.info("Call saveIssues *********************");
Iterable<InputFile> files = fileSystem.inputFiles(fileSystem.predicates().hasType(InputFile.Type.MAIN));
files.forEach(f -> LOG.info("******* inputfiles FILENAME INSIDE DIRECTORY filename=" + f.filename() + "\n relativePath="+ f.relativePath()+"\n absolutePath="+f.absolutePath()));

for (com.plugin.sonar.report.foo.File file : violations.getFile()) {
String baseDir = fileSystem.baseDir().getCanonicalPath();
String srcDirectory = context.config().get(SONAR_SOURCES).orElseThrow(RuntimeException::new);
String configDirectory = context.config().get(SONAR_CONFIG).orElseThrow(RuntimeException::new);
String astFilepath = configDirectory + File.separator + AST_FOLDER;
String srcFilePath = baseDir + File.separator + srcDirectory + File.separator;

LOG.info("baseDir=" + baseDir);
LOG.info("srcDirectory=" + srcDirectory);
LOG.info("configDirectory=" + configDirectory);
LOG.info("astFilepath=" + astFilepath);
LOG.info("srcFilePath=" + srcFilePath);
LOG.info(("file.getName()="+file.getName()));
//replacing ast folder path
String astReplaceSrcPath = file.getName().replace(astFilepath, srcFilePath);
LOG.info("astReplaceSrcPath=" + astReplaceSrcPath);
for (Violation violation : file.getViolation()) {
getResourceAndSaveIssue(violation, file.getName());
}
}
} catch (Exception e) {
LOG.error(e.getLocalizedMessage(), e);
}
}


 

Create Sonar Plugin - Scanner Basic - Save Issues 2

 Need to create a InputFile (org.sonar.api.batch.fs.InputFile) to save the issue. 

Retrieve the input file from the file system ( org.sonar.api.batch.fs.FileSystem) by mmatching name and the type of the file (MAIN)

    Currently we have two types (MAIN and TEST)

-----------

public static enum Type {
MAIN,
TEST;

private Type() {
}
}

----------------------

below is the sample code snippet to get the Input file

-------------------------------------------------------------------------------------

private void getResourceAndSaveIssue(final Violation violation, String fileName) {
LOG.debug("Calling getResourceAndSaveIssue for file=" + fileName);
LOG.debug(violation.toString());

InputFile inputFile = fileSystem.inputFile(
fileSystem.predicates().and(
fileSystem.predicates().hasAbsolutePath(fileName),
fileSystem.predicates().hasType(InputFile.Type.MAIN)));

LOG.debug("inputFile null ? " + (inputFile == null));

if (inputFile != null) {
saveIssue(inputFile, violation.getLine(), violation.getRule().getId(), violation.getRule().getName());
} else {
LOG.error("Not able to find a InputFile with " + fileName);
}
}

Create Sonar Plugin - Scanner Basic - Save issue 3

 saving your issues , when you have input file details and rule key information. If you have these you can create a new issue using the context ( org.sonar.api.batch.sensor.SensorContext)



-----------------------------------------------------------------------------------------------


private void saveIssue(final InputFile inputFile, int line, final String externalRuleKey, final String message) {
try {
RuleKey ruleKey = RuleKey.of(getRepositoryKeyForLanguage(Objects.requireNonNull(inputFile.language())), externalRuleKey);
NewIssue newIssue = context.newIssue()
.forRule(ruleKey);
LOG.info(ruleKey.toString() + " RULE KEY");
NewIssueLocation primaryLocation = newIssue.newLocation()
.on(inputFile)
.message(message);
if (line > 0) {
primaryLocation.at(inputFile.selectLine(line));
}
newIssue.at(primaryLocation);
newIssue.save();
LOG.info(newIssue.toString() + " saved");
} catch (Exception ex) {
LOG.error(ex.getLocalizedMessage(), ex);
}
}



Create Sonar Plugin - Scanner Basic - Read the XML violation report

 XML violation report will be created inside working folder.

find  the report and convert to POJO

For this we need to have a separate logic to convert using something like Jackson. Basic structure of the method would be like this.

Converter class has the XML to JAVA mapping conversion.

------------------------------------------------------------------------------------------------------------------------


private Violations getReport() {
Violations report = new Violations();
try {
String workDirRoot = fileSystem.workDir().getCanonicalPath();
String reportPath = workDirRoot + File.separator + VIOLATION_REPORT_NAME;
report = Converter.convertXMLToJava(reportPath);
} catch (Exception ex) {
LOG.error(ex.getLocalizedMessage(), ex);
}
return report;
}


Create Sonar Plugin - Scanner create command to execute a jar file


We need to create

  • Command to execute Jar file
  • execute the command


---------------------------------------------------------------------------------


private void analyze() {

try {
String workdirRoot = fileSystem.workDir().getCanonicalPath();

extractor.extractPlsql(workdirRoot);
String baseDir = fileSystem.baseDir().getCanonicalPath();
String srcDirectory = context.config().get(SONAR_SOURCES).orElseThrow(RuntimeException::new);
String configDirectory = context.config().get(SONAR_CONFIG).orElseThrow(RuntimeException::new);

LOG.info("workdirRoot="+workdirRoot);
LOG.info("baseDir="+baseDir);
LOG.info("srcDirectory="+srcDirectory);
LOG.info("configDirectory="+configDirectory);
LOG.info("executablePatj="+workdirRoot + File.separator + EXECUTABLE);
LOG.info("srcDirPath="+baseDir + File.separator + srcDirectory);
LOG.info("InitFile="+workdirRoot + File.separator + FooSQL_INIT_PROPERTY_FILE);
LOG.info("violationReport="+workdirRoot + File.separator + VIOLATION_REPORT_NAME);
LOG.info("astFolder="+configDirectory + File.separator + AST_FOLDER);

Command command = Command.create("java");
command.addArgument("-jar")
.addArgument(workdirRoot + File.separator + EXECUTABLE)
// .addArgument(baseDir + File.separator + srcDirectory)
.addArgument(baseDir)
.addArgument(workdirRoot + File.separator + FooSQL_INIT_PROPERTY_FILE)
.addArgument(workdirRoot + File.separator + VIOLATION_REPORT_NAME)
.addArgument(configDirectory + File.separator + AST_FOLDER)
.addArgument(workdirRoot)
.addArgument(configDirectory);

LOG.info(command.toCommandLine());
CommandExecutor.create().execute(command, LOG::info, LOG::error, Integer.MAX_VALUE);
} catch (Exception e) {
LOG.error(e.getLocalizedMessage(), e);
}

}

Create Sonar Plugin - Scanner Basic

 Scanner is the main functionality of the sonar plugin. All the analysis is done here. As we are using third party tool to analyze our foo language files, we are getting the result of the analysis as a XML or JSON report depending on the situation.

  • So we have few key things
  • Call third party app -  executable Jar
  • Get the analysis report
  • Then read the XML/JSON report and convert to JAVA/POJO
  • Create input files with issues we have collected
  • save those issues to show them in dashboard, SonarQube


  1.  Scanner implements org.sonar.api.batch.sensor.Sensor;
  2. We have two contracts. describe and execute
    1. describe - set Key and Name for our foo language
    2. define the flow of analysis to show the analysis in dashboard
  3. To do our work , we need SensorContext, Filesystem and extractor
    1. org.sonar.api.batch.sensor.SensorContext
      1. to retrieve parameter details we need to run - parameters and values
    2. org.sonar.api.batch.fs.FileSystem
    3. File system has the information of base directory and files used to analyze(name, absolute path, relative path...)
    4. Extractor

    5. Extractor is used to extract third party tool/jar and other related files need for do the analysis 

4. Execute is the main functionality - where all happens

Basically we have divided to three parts

  • setting the context
  • Analyse
  • save Issues

Analyse  - we call the third party app/jar and get the results of the analysis. To call the the jar, we need to make the command with parameters attached.

After the analysis done, we have the results - violations

Then in save issues, we convert them back to POJO and save and show in  the dashboard.


-----------------------------------------------------------------------------------------------


package com.plugin.sonar.scanner;

import com.plugin.sonar.report.foo.Violation;
import com.plugin.sonar.report.foo.Violations;
import com.plugin.sonar.util.Converter;
import com.plugin.sonar.util.Extractor;
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.issue.NewIssue;
import org.sonar.api.batch.sensor.issue.NewIssueLocation;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.command.Command;
import org.sonar.api.utils.command.CommandExecutor;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

import java.io.File;
import java.util.Objects;

import static com.plugin.sonar.util.FooConstants.*;

public class FooScanner implements Sensor {

private static final Logger LOG = Loggers.get(FooScanner.class);
private FileSystem fileSystem;
private Extractor extractor;

private SensorContext context;

public FooScanner(final FileSystem fileSystem, final Extractor extractor) {
this.fileSystem = fileSystem;
LOG.debug("Inside PlsqlScanner::Constructor::fileSystem=" + fileSystem);
this.extractor = extractor;
}

@Override
public void describe(SensorDescriptor sensorDescriptor) {
sensorDescriptor.name(LANGUAGE_NAME);
sensorDescriptor.onlyOnLanguages(LANGUAGE_KEY);
}

@Override
public void execute(SensorContext sensorContext) {
LOG.info("executing plsql sensor");
this.context = sensorContext;
LOG.info("start Pl/SQL analysing");
analyze();
LOG.info("executing Pl/SQL sensor: analyze completed.");
saveIssues();
LOG.info("executing Pl/SQL sensor: save issues completed.");
}

private void saveIssues() {
// for each violation we have got,
            // get Input file and save issue for file name / file absolute path
}

private void getResourceAndSaveIssue(final Violation violation, String fileName) {
        // get the Input file from file system
        // save issue with input file using details from the XML/JSON report

}

private void saveIssue(final InputFile inputFile, int line, final String externalRuleKey, final String message) {
//get rule key for file analysed
        // create new issue in context using rule key
        // save issue
}

private Violations getReport() {
// convert XML report to POJO
}

private static String getRepositoryKeyForLanguage(String languageKey) {
return languageKey.toLowerCase() + "-" + LANGUAGE_KEY;
}


private void analyze() {
        // create command to execute jar
        // execute the command


}
}

Create Sonar Plugin - Constant string values used in development

 There are few String constants used in Sonar Plugin creation. It's always better to have all these in a one single place.

------------------------------------------------------------------------------------------------------------------

package com.plugin.sonar.util;

public final class FooConstants {

private FooConstants() {
}

public static final String LANGUAGE_KEY = "foo";
public static final String LANGUAGE_NAME = "Foo";

public static final String FILE_SUFFIXES_KEY = "sonar.lang.patterns.foo";
public static final String FILE_SUFFIXES_DEFAULT_VALUE = ".foo,.txt,.sql";

public static final String FooSQL_QUALITY_PROFILE_NAME = "Foo Quality Profile";
public static final String JSON_PROFILE_PATH = "foo_quality_profile.json";

public static final String PATH_TO_RULES_XML = "/foo-sonar-rules.xml";

public static final String RULES_KEY = "foo";
public static final String REPO_KEY = LANGUAGE_KEY + "-" + RULES_KEY;
public static final String RULES_NAME = "Foo";
public static final String REPO_NAME = LANGUAGE_KEY + "-" + RULES_NAME;
/**
* {@code Extractor} related constant values
*/
public static final String FooSQL_JAR = "/foo-rule-engine.jar";
public static final String LOGIN_CONFIGURATION = "/log4j.conf";
public static final String INIT_PROPERTIES = "/init.properties";
public static final String FooSQL_SONAR_RULES = "/foo-sonar-rules.xml";
public static final String RULE_ENGINE_PROPERTIES = "/fooruleengine.properties";
public static final String RULES = "/Rules.xml";

/**
* {@code PlsqlScanner} related constant values
*/
public static final String VIOLATION_REPORT_NAME = "violationReport.xml";
public static final String EXECUTABLE = "foo-rule-engine.jar";
public static final String FooSQL_INIT_PROPERTY_FILE = "init.properties";
public static final String AST_FOLDER = "ast\\";

/**
* Sonar Scanner parameters.
*/
public static final String SONAR_SOURCES = "sonar.sources";
public static final String SONAR_CONFIG = "configDir";
}

Monday, November 23, 2020

Create Sonar Plugin - Rules definition class

 Rules definition class helps to load the rules to SonarQube. These rules will be shown under "Rules" tab.

  1. FooRulesDefinition implements RulesDefinition
  2. Create repository
  3. load the rules from the xml file -  eg: foo-sonar-rules.xml

------------------------------------------------------------------------------------------------------------

package com.plugin.sonar.language.foo;

import com.plugin.sonar.util.FooConstants;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.api.server.rule.RulesDefinitionXmlLoader;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class FooRulesDefinition implements RulesDefinition {


private static String rulesDefinitionFilePath() {
return FooConstants.PATH_TO_RULES_XML;
}

private void defineRulesForLanguage(Context context, String repositoryKey,
                 String repositoryName, String languageKey) {
NewRepository repository = context.createRepository
                (repositoryKey, languageKey).setName(repositoryName);

InputStream rulesXml = this.getClass().getResourceAsStream(rulesDefinitionFilePath());
if (rulesXml != null) {
RulesDefinitionXmlLoader rulesLoader = new RulesDefinitionXmlLoader();
rulesLoader.load(repository, rulesXml, StandardCharsets.UTF_8.name());
}

repository.done();
}

@Override
public void define(Context context) {
defineRulesForLanguage(context,
            FooConstants.REPO_KEY, FooConstants.REPO_NAME, FooConstants.LANGUAGE_KEY);
}
}

-----------------------------------------------------------------------------------

Below is the sample "foo-sonar-rules.xml"

---------------------------------------------------------------------------------

<foo-rules>
<rule>
<key>1</key>
<name>rule name</name>
<internalKey>1</internalKey>
<description>Description of rule 1
</description>
<severity>BLOCKER</severity>
<cardinality>SINGLE</cardinality>
<status>READY</status>
<type>BUG</type>
<tag>foo</tag>
<remediationFunction>CONSTANT_ISSUE</remediationFunction>
<remediationFunctionBaseEffort>2min</remediationFunctionBaseEffort>
</rule>
</foo-rules>

Create Sonar Plugin - Create Qulaity Profile

  1.  Create a built in Quality profile for the Foo language.
  2. implements BuiltInQualityProfilesDefinition
  3. can set default profile -  profile.setDefault(true)
  4. Load the profile giving profile name and active rule set. Here active rules were given in "foo_quality_profile.json" inside ""ruleKeys""



------------------------------------------------------------------

package com.plugin.sonar.language.foo;

import org.sonar.api.server.profile.BuiltInQualityProfilesDefinition;
import org.sonarsource.analyzer.commons.BuiltInQualityProfileJsonLoader;

import static com.plugin.sonar.util.FooConstants.*;

public class FooQualityProfile implements BuiltInQualityProfilesDefinition {

@Override
public void define(Context context) {
NewBuiltInQualityProfile profile = context.createBuiltInQualityProfile(FooSQL_QUALITY_PROFILE_NAME, LANGUAGE_KEY);
profile.setDefault(true);
BuiltInQualityProfileJsonLoader.load(profile, REPO_KEY, JSON_PROFILE_PATH);
profile.done();
}
}

-------------------------------

below is the sample "foo_quality_profile.json" with 5 active keys.

-----------------------------------------------------------------------

{
"name": "Foo Quality Profile",
"ruleKeys": [
"1",
"2",
"3",
"4",
"5"

]
}

Create Sonar Plugin - Language properties

 Language property class contains the properties related to the language.


----------------------------------------------------------------------------------------------

package com.plugin.sonar.language.foo;

import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.resources.Qualifiers;

import java.util.Collections;
import java.util.List;

import static com.plugin.sonar.util.FooConstants.*;

public class FooLanguageProperties {

private FooLanguageProperties() {
// only statics
}

public static List<PropertyDefinition> getProperties() {
return Collections.singletonList(PropertyDefinition.builder(FILE_SUFFIXES_KEY)//".foo,.f"
.defaultValue(FILE_SUFFIXES_DEFAULT_VALUE)
.category(LANGUAGE_NAME)
.name("File Suffixes")
.description("Comma-separated list of suffixes for files to analyze.")
.onQualifiers(Qualifiers.PROJECT)
.build());
}
}


Create Sonar Plugin - Language class

 Language class contains the

  1. Class extends AbstractLanguage  
  2. defines KEY and Name for Language
  3. File extensions for the specific language. ( here Foo.) Note we have used comma as seprator when you define multiple extensions.
  4. We have injected "Configuration" - org.sonar.api.config.Configuration;
    1. Basically at the  construction of the object, Key and Name for the language and configurations are set ( Constructor injection )


----------------------------------------------------------------------------------------------

package com.plugin.sonar.language.foo;

import org.apache.commons.lang.StringUtils;
import org.sonar.api.config.Configuration;
import org.sonar.api.resources.AbstractLanguage;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static com.plugin.sonar.util.FooConstants.*;

public class FooLanguage extends AbstractLanguage {

private Configuration config;

public FooLanguage(Configuration configuration) {
super(LANGUAGE_KEY, LANGUAGE_NAME);
this.config = configuration;
}

@Override
public String[] getFileSuffixes() {
String[] suffixes = filterEmptyStrings(config.getStringArray(FILE_SUFFIXES_KEY));
if (suffixes.length == 0) {
suffixes = StringUtils.split(FILE_SUFFIXES_DEFAULT_VALUE, ",");
}
return suffixes;
}

private String[] filterEmptyStrings(String[] stringArray) {
List<String> nonEmptyStrings = new ArrayList<>();
for (String string : stringArray) {
if (StringUtils.isNotBlank(string.trim())) {
nonEmptyStrings.add(string.trim());
}
}
return nonEmptyStrings.toArray(new String[0]);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
FooLanguage that = (FooLanguage) o;
return Objects.equals(config, that.config);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), config);
}
}

Create Sonar Plugin - Plugin exception class

 you can create your own exception class. Here is a sample of a exception class with message only.


package com.plugin.sonar.exception;

public class FooPluginException extends Exception {
public FooPluginException(String errorMsg) {
super(errorMsg);
}
}

Create Sonar Plugin - Plugin class

We create a plugin class implementing org.sonar.api.Plugin.

Implement "define" contract and register all classes as extensions. Plugin class is the entry point.

Below is the sample code for FooPlugin class.

You can see that, you need to implement all other specialized classes before registering as extensions.

---------------------------------------------------------------------------------------------------


package com.plugin.sonar;

import com.plugin.sonar.language.foo.FooLanguage;
import com.plugin.sonar.language.foo.FooLanguageProperties;
import com.plugin.sonar.language.foo.FooQualityProfile;
import com.plugin.sonar.scanner.FooScanner;
import com.plugin.sonar.util.Extractor;
import com.plugin.sonar.language.foo.FooRulesDefinition;
import org.sonar.api.Plugin;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;

public class FooPlugin implements Plugin {

private static final Logger LOG = Loggers.get(FooPlugin.class);

public void define(Context context) {
LOG.info("defining all extensions for Foo Language plugin");
context.addExtensions(FooLanguage.class, FooRulesDefinition.class)
.addExtensions(FooQualityProfile.class, FooLanguageProperties.getProperties());
context.addExtension(FooScanner.class);
context.addExtension(Extractor.class);
}
}

Create Sonar Plugin - Basic Class Structure for Sonar Plugin using Java

You can write a sonar Plugin for a langauge using Java.

When you write a plugin there few key components you need to write. Below is the basic structure of classes.

Let's assume you are writting plugin for "Foo" language. 

Here we are calling a third party component which bundled in a executable Jar file. we generate violations in a xml file and show them in SonarQube.

1. class FooPlugin implements Plugin

Plugin class which connects all the component

2. class FooLanguage extends AbstractLanguage

Implement Key and Name for language and some other features

3. class FooRulesDefinition implements RulesDefinition

create repository and define rules for language

4. class FooQualityProfile implements BuiltInQualityProfilesDefinition

Create a quality profile for language. you can set this profile as your default quality profile and also define rules for the profile.

5. class FooLanguageProperties

define properties for the language.

6. class FooScanner implements Sensor

Logic for analysis of the files written language "foo" will be handled/written here. once analysis is done, result/issues will be saved as report in SonarQube.

7. class Extractor

As i explained files will  be analysed using a thirid party jar, we need to extarct that jar from our plugin Jar and also related files need to do the excution. Logic for extraction is written here. 

Tuesday, November 10, 2020

Property file loader

 properties file is used to store project configuration data or settings. You can write your own property file loader or use Java java.util.Properties directly.

Here is an sample of custom property file loader.


import com.test.properties.exception.PropertyException;

import org.apache.log4j.Logger;


import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.util.HashMap;

import java.util.Map;

import java.util.Properties;


public class PropertyLoader {

    private final Map<String, Properties> propertyRepo;

    private static final Logger LOGGER = Logger.getLogger(PropertyLoader.class);

    private PropertyLoader() {

        propertyRepo = new HashMap<String, Properties>();

    }

    public String getProperty(final String propertyFileLocation, final String property, final String defaultValue) throws PropertyException {

        Properties properties = propertyRepo.get(propertyFileLocation);

        if (properties == null) {

            properties = loadPropertes(propertyFileLocation);

            propertyRepo.put(propertyFileLocation, properties);

        }

        return properties.getProperty(property) != null ? properties.getProperty(property) : defaultValue;

    }

    public String getProperty(final String propertyFileLocation, final String property) throws PropertyException {

        return getProperty(propertyFileLocation, property, null);

    }

    private static Properties loadPropertes(final String fileLocation) throws PropertyException {

        Properties properties = new Properties();

        try {

            try (InputStream inputStream = new FileInputStream(fileLocation)) {

                properties.load(inputStream);

            }

        } catch (IOException e) {

            LOGGER.error("Error in loading property file : " + fileLocation, e);

            throw new PropertyException("Error in loading property file : " + fileLocation);

        }

        return properties;

    }

    public static PropertyLoader getInstance() {

        return HOLDER.INSTANCE;

    }

    private static final class HOLDER {

        private static final PropertyLoader INSTANCE = new PropertyLoader();

    }

}