123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- /*
- * SonarQube
- * Copyright (C) 2009-2021 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.
- *
- * <h3>Usage</h3>
- * <pre>
- * 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);
- * }
- * }
- * }
- * </pre>
- *
- * <h3>XML Format</h3>
- * <pre>
- * <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>
- * </pre>
- *
- * <h3>XML Example</h3>
- * <pre>
- * <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>
- * </pre>
- *
- * @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<ParamStruct> params = new ArrayList<>();
- List<String> 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<ParamStruct> params, List<String> 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<ParamStruct> 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;
- }
- }
|