All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.api.server.rule.RulesDefinitionXmlLoader Maven / Gradle / Ivy

There is a newer version: 10.14.0.2599
Show newest version
/*
 * Sonar Plugin API
 * Copyright (C) 2009-2022 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.api.server.rule;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.apache.commons.io.ByteOrderMark;
import org.apache.commons.io.input.BOMInputStream;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ServerSide;
import org.sonar.api.server.debt.DebtRemediationFunction;
import org.sonar.check.Cardinality;
import org.sonarsource.api.sonarlint.SonarLintSide;

import static java.lang.String.format;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.apache.commons.lang.StringUtils.trim;

/**
 * Loads definitions of rules from a XML file.
 *
 * 

Usage

*
 * public class MyJsRulesDefinition implements RulesDefinition {
 *
 *   private static final String PATH = "my-js-rules.xml";
 *   private final RulesDefinitionXmlLoader xmlLoader;
 *
 *   public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) {
 *     this.xmlLoader = xmlLoader;
 *   }
 *
 *   {@literal @}Override
 *   public void define(Context context) {
 *     try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(PATH), StandardCharsets.UTF_8)) {
 *       NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
 *       xmlLoader.load(repository, reader);
 *       repository.done();
 *     } catch (IOException e) {
 *       throw new IllegalStateException(String.format("Fail to read file %s", PATH), e);
 *     }
 *   }
 * }
 * 
* *

XML Format

*
 * <rules>
 *   <rule>
 *     <!-- Required key. Max length is 200 characters. -->
 *     <key>the-rule-key</key>
 *
 *     <!-- Required name. Max length is 200 characters. -->
 *     <name>The purpose of the rule</name>
 *
 *     <!-- Required description. No max length. -->
 *     <description>
 *       <![CDATA[The description]]>
 *     </description>
 *     <!-- Optional format of description. Supported values are HTML (default) and MARKDOWN. -->
 *     <descriptionFormat>HTML</descriptionFormat>
 *
 *     <!-- Optional key for configuration of some rule engines -->
 *     <internalKey>Checker/TreeWalker/LocalVariableName</internalKey>
 *
 *     <!-- Default severity when enabling the rule in a Quality profile.  -->
 *     <!-- Possible values are INFO, MINOR, MAJOR (default), CRITICAL, BLOCKER. -->
 *     <severity>BLOCKER</severity>
 *
 *     <!-- Possible values are SINGLE (default) and MULTIPLE for template rules -->
 *     <cardinality>SINGLE</cardinality>
 *
 *     <!-- Status displayed in rules console. Possible values are BETA, READY (default), DEPRECATED. -->
 *     <status>BETA</status>
 *
 *     <!-- Type as defined by the SonarQube Quality Model. Possible values are CODE_SMELL (default), BUG and VULNERABILITY.-->
 *     <type>BUG</type>
 *
 *     <!-- Optional tags. See org.sonar.api.server.rule.RuleTagFormat. The maximal length of all tags is 4000 characters. -->
 *     <tag>misra</tag>
 *     <tag>multi-threading</tag>
 *
 *     <!-- Optional parameters -->
 *     <param>
 *       <!-- Required key. Max length is 128 characters. -->
 *       <key>the-param-key</key>
 *       <description>
 *         <![CDATA[the optional description, in HTML format. Max length is 4000 characters.]]>
 *       </description>
 *       <!-- Optional default value, used when enabling the rule in a Quality profile. Max length is 4000 characters. -->
 *       <defaultValue>42</defaultValue>
 *     </param>
 *     <param>
 *       <key>another-param</key>
 *     </param>
 *
 *     <!-- Quality Model - type of debt remediation function -->
 *     <!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values -->
 *     <!-- It was previously named 'debtRemediationFunction'. -->
 *     <!-- Since 5.5 -->
 *     <remediationFunction>LINEAR_OFFSET</remediationFunction>
 *
 *     <!-- Quality Model - raw description of the "gap", used for some types of remediation functions. -->
 *     <!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setGapDescription(String)} -->
 *     <!-- It was previously named 'effortToFixDescription'. -->
 *     <!-- Since 5.5 -->
 *     <gapDescription>Effort to test one uncovered condition</gapFixDescription>
 *
 *     <!-- Quality Model - gap multiplier of debt remediation function. Must be defined only for some function types. -->
 *     <!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} -->
 *     <!-- It was previously named 'debtRemediationFunctionCoefficient'. -->
 *     <!-- Since 5.5 -->
 *     <remediationFunctionGapMultiplier>10min</remediationFunctionGapMultiplier>
 *
 *     <!-- Quality Model - base effort of debt remediation function. Must be defined only for some function types. -->
 *     <!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} -->
 *     <!-- It was previously named 'debtRemediationFunctionOffset'. -->
 *     <!-- Since 5.5 -->
 *     <remediationFunctionBaseEffort>2min</remediationFunctionBaseEffort>
 *
 *     <!-- Deprecated field, replaced by "internalKey" -->
 *     <configKey>Checker/TreeWalker/LocalVariableName</configKey>
 *
 *     <!-- Deprecated field, replaced by "severity" -->
 *     <priority>BLOCKER</priority>
 *
 *   </rule>
 * </rules>
 * 
* *

XML Example

*
 * <rules>
 *   <rule>
 *     <key>S1442</key>
 *     <name>"alert(...)" should not be used</name>
 *     <description>alert(...) can be useful for debugging during development, but ...</description>
 *     <tag>cwe</tag>
 *     <tag>security</tag>
 *     <tag>user-experience</tag>
 *     <debtRemediationFunction>CONSTANT_ISSUE</debtRemediationFunction>
 *     <debtRemediationFunctionBaseOffset>10min</debtRemediationFunctionBaseOffset>
 *   </rule>
 *
 *   <!-- another rules... -->
 * </rules>
 * 
* * @see org.sonar.api.server.rule.RulesDefinition * @since 4.3 * @deprecated since 9.0. Use the sonar-check-api to annotate rule classes instead of loading the metadata from XML files. See {@link org.sonar.check.Rule}. */ @ServerSide @ComputeEngineSide @SonarLintSide @Deprecated public class RulesDefinitionXmlLoader { private static final String ELEMENT_RULES = "rules"; private static final String ELEMENT_RULE = "rule"; private static final String ELEMENT_PARAM = "param"; private enum DescriptionFormat { HTML, MARKDOWN } /** * Loads rules by reading the XML input stream. The input stream is not always closed by the method, so it * should be handled by the caller. * * @since 4.3 */ public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) { load(repo, input, Charset.forName(encoding)); } /** * @since 5.1 */ public void load(RulesDefinition.NewRepository repo, InputStream input, Charset charset) { try (Reader reader = new InputStreamReader(new BOMInputStream(input, ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE, ByteOrderMark.UTF_32LE, ByteOrderMark.UTF_32BE), charset)) { load(repo, reader); } catch (IOException e) { throw new IllegalStateException("Error while reading XML rules definition for repository " + repo.key(), e); } } /** * Loads rules by reading the XML input stream. The reader is not closed by the method, so it * should be handled by the caller. * * @since 4.3 */ public void load(RulesDefinition.NewRepository repo, Reader inputReader) { XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); // just so it won't try to load DTD in if there's DOCTYPE xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); try { final XMLEventReader reader = xmlFactory.createXMLEventReader(inputReader); while (reader.hasNext()) { final XMLEvent event = reader.nextEvent(); if (event.isStartElement() && event.asStartElement().getName() .getLocalPart().equals(ELEMENT_RULES)) { parseRules(repo, reader); } } } catch (XMLStreamException e) { throw new IllegalStateException("XML is not valid", e); } } private static void parseRules(RulesDefinition.NewRepository repo, XMLEventReader reader) throws XMLStreamException { while (reader.hasNext()) { final XMLEvent event = reader.nextEvent(); if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(ELEMENT_RULES)) { return; } if (event.isStartElement()) { final StartElement element = event.asStartElement(); final String elementName = element.getName().getLocalPart(); if (ELEMENT_RULE.equals(elementName)) { processRule(repo, element, reader); } } } } private static void processRule(RulesDefinition.NewRepository repo, StartElement ruleElement, XMLEventReader reader) throws XMLStreamException { String key = null; String name = null; String description = null; // enum is not used as variable type as we want to raise an exception with the rule key when format is not supported String descriptionFormat = DescriptionFormat.HTML.name(); String internalKey = null; String severity = Severity.defaultSeverity(); String type = null; RuleStatus status = RuleStatus.defaultStatus(); boolean template = false; String gapDescription = null; String debtRemediationFunction = null; String debtRemediationFunctionGapMultiplier = null; String debtRemediationFunctionBaseEffort = null; List params = new ArrayList<>(); List tags = new ArrayList<>(); /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */ Attribute keyAttribute = ruleElement.getAttributeByName(new QName("key")); if (keyAttribute != null && StringUtils.isNotBlank(keyAttribute.getValue())) { key = trim(keyAttribute.getValue()); } Attribute priorityAttribute = ruleElement.getAttributeByName(new QName("priority")); if (priorityAttribute != null && StringUtils.isNotBlank(priorityAttribute.getValue())) { severity = trim(priorityAttribute.getValue()); } while (reader.hasNext()) { final XMLEvent event = reader.nextEvent(); if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(ELEMENT_RULE)) { buildRule(repo, key, name, description, descriptionFormat, internalKey, severity, type, status, template, gapDescription, debtRemediationFunction, debtRemediationFunctionGapMultiplier, debtRemediationFunctionBaseEffort, params, tags); return; } if (event.isStartElement()) { final StartElement element = event.asStartElement(); final String elementName = element.getName().getLocalPart(); if ("name".equalsIgnoreCase(elementName)) { name = StringUtils.trim(reader.getElementText()); } else if ("type".equalsIgnoreCase(elementName)) { type = StringUtils.trim(reader.getElementText()); } else if ("description".equalsIgnoreCase(elementName)) { description = StringUtils.trim(reader.getElementText()); } else if ("descriptionFormat".equalsIgnoreCase(elementName)) { descriptionFormat = StringUtils.trim(reader.getElementText()); } else if ("key".equalsIgnoreCase(elementName)) { key = StringUtils.trim(reader.getElementText()); } else if ("configKey".equalsIgnoreCase(elementName)) { // deprecated field, replaced by internalKey internalKey = StringUtils.trim(reader.getElementText()); } else if ("internalKey".equalsIgnoreCase(elementName)) { internalKey = StringUtils.trim(reader.getElementText()); } else if ("priority".equalsIgnoreCase(elementName) || "severity".equalsIgnoreCase(elementName)) { // "priority" is deprecated field and has been replaced by "severity" severity = StringUtils.trim(reader.getElementText()); } else if ("cardinality".equalsIgnoreCase(elementName)) { template = Cardinality.MULTIPLE == Cardinality.valueOf(StringUtils.trim(reader.getElementText())); } else if ("gapDescription".equalsIgnoreCase(elementName) || "effortToFixDescription".equalsIgnoreCase(elementName)) { gapDescription = StringUtils.trim(reader.getElementText()); } else if ("remediationFunction".equalsIgnoreCase(elementName) || "debtRemediationFunction".equalsIgnoreCase(elementName)) { debtRemediationFunction = StringUtils.trim(reader.getElementText()); } else if ("remediationFunctionBaseEffort".equalsIgnoreCase(elementName) || "debtRemediationFunctionOffset".equalsIgnoreCase(elementName)) { debtRemediationFunctionGapMultiplier = StringUtils.trim(reader.getElementText()); } else if ("remediationFunctionGapMultiplier".equalsIgnoreCase(elementName) || "debtRemediationFunctionCoefficient".equalsIgnoreCase(elementName)) { debtRemediationFunctionBaseEffort = StringUtils.trim(reader.getElementText()); } else if ("status".equalsIgnoreCase(elementName)) { String s = StringUtils.trim(reader.getElementText()); if (s != null) { status = RuleStatus.valueOf(s); } } else if (ELEMENT_PARAM.equalsIgnoreCase(elementName)) { params.add(processParameter(element, reader)); } else if ("tag".equalsIgnoreCase(elementName)) { tags.add(StringUtils.trim(reader.getElementText())); } } } } private static void buildRule(RulesDefinition.NewRepository repo, String key, String name, @Nullable String description, String descriptionFormat, @Nullable String internalKey, String severity, @Nullable String type, RuleStatus status, boolean template, @Nullable String gapDescription, @Nullable String debtRemediationFunction, @Nullable String debtRemediationFunctionGapMultiplier, @Nullable String debtRemediationFunctionBaseEffort, List params, List tags) { try { RulesDefinition.NewRule rule = repo.createRule(key) .setSeverity(severity) .setName(name) .setInternalKey(internalKey) .setTags(tags.toArray(new String[tags.size()])) .setTemplate(template) .setStatus(status) .setGapDescription(gapDescription); if (type != null) { rule.setType(RuleType.valueOf(type)); } fillDescription(rule, descriptionFormat, description); fillRemediationFunction(rule, debtRemediationFunction, debtRemediationFunctionGapMultiplier, debtRemediationFunctionBaseEffort); fillParams(rule, params); } catch (Exception e) { throw new IllegalStateException(format("Fail to load the rule with key [%s:%s]", repo.key(), key), e); } } private static void fillDescription(RulesDefinition.NewRule rule, String descriptionFormat, @Nullable String description) { if (isNotBlank(description)) { switch (DescriptionFormat.valueOf(descriptionFormat)) { case HTML: rule.setHtmlDescription(description); break; case MARKDOWN: rule.setMarkdownDescription(description); break; default: throw new IllegalArgumentException("Value of descriptionFormat is not supported: " + descriptionFormat); } } } private static void fillRemediationFunction(RulesDefinition.NewRule rule, @Nullable String debtRemediationFunction, @Nullable String functionOffset, @Nullable String functionCoeff) { if (isNotBlank(debtRemediationFunction)) { DebtRemediationFunction.Type functionType = DebtRemediationFunction.Type.valueOf(debtRemediationFunction); rule.setDebtRemediationFunction(rule.debtRemediationFunctions().create(functionType, functionCoeff, functionOffset)); } } private static void fillParams(RulesDefinition.NewRule rule, List params) { for (ParamStruct param : params) { rule.createParam(param.key) .setDefaultValue(param.defaultValue) .setType(param.type) .setDescription(param.description); } } private static class ParamStruct { String key; String description; String defaultValue; RuleParamType type = RuleParamType.STRING; } private static ParamStruct processParameter(StartElement paramElement, XMLEventReader reader) throws XMLStreamException { ParamStruct param = new ParamStruct(); // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT Attribute keyAttribute = paramElement.getAttributeByName(new QName("key")); if (keyAttribute != null && StringUtils.isNotBlank(keyAttribute.getValue())) { param.key = StringUtils.trim(keyAttribute.getValue()); } // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT Attribute typeAttribute = paramElement.getAttributeByName(new QName("type")); if (typeAttribute != null && StringUtils.isNotBlank(typeAttribute.getValue())) { param.type = RuleParamType.parse(StringUtils.trim(typeAttribute.getValue())); } while (reader.hasNext()) { final XMLEvent event = reader.nextEvent(); if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(ELEMENT_PARAM)) { return param; } if (event.isStartElement()) { final StartElement element = event.asStartElement(); final String elementName = element.getName().getLocalPart(); if ("key".equalsIgnoreCase(elementName)) { param.key = StringUtils.trim(reader.getElementText()); } else if ("description".equalsIgnoreCase(elementName)) { param.description = StringUtils.trim(reader.getElementText()); } else if ("type".equalsIgnoreCase(elementName)) { param.type = RuleParamType.parse(StringUtils.trim(reader.getElementText())); } else if ("defaultValue".equalsIgnoreCase(elementName)) { param.defaultValue = StringUtils.trim(reader.getElementText()); } } } return param; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy