001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.api.server.rule;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.InputStreamReader;
025import java.io.Reader;
026import java.nio.charset.Charset;
027import java.util.ArrayList;
028import java.util.List;
029import javax.annotation.Nullable;
030import javax.xml.stream.XMLInputFactory;
031import javax.xml.stream.XMLStreamException;
032import org.codehaus.staxmate.SMInputFactory;
033import org.codehaus.staxmate.in.SMHierarchicCursor;
034import org.codehaus.staxmate.in.SMInputCursor;
035import org.sonar.api.ce.ComputeEngineSide;
036import org.sonar.api.rule.RuleStatus;
037import org.sonar.api.rule.Severity;
038import org.sonar.api.rules.RuleType;
039import org.sonar.api.server.ServerSide;
040import org.sonar.api.server.debt.DebtRemediationFunction;
041import org.sonar.check.Cardinality;
042import org.sonarsource.api.sonarlint.SonarLintSide;
043
044import static java.lang.String.format;
045import static org.apache.commons.lang.StringUtils.equalsIgnoreCase;
046import static org.apache.commons.lang.StringUtils.isNotBlank;
047import static org.apache.commons.lang.StringUtils.trim;
048
049/**
050 * Loads definitions of rules from a XML file.
051 *
052 * <h3>Usage</h3>
053 * <pre>
054 * public class MyJsRulesDefinition implements RulesDefinition {
055 *
056 *   private static final String PATH = "my-js-rules.xml";
057 *   private final RulesDefinitionXmlLoader xmlLoader;
058 *
059 *   public MyJsRulesDefinition(RulesDefinitionXmlLoader xmlLoader) {
060 *     this.xmlLoader = xmlLoader;
061 *   }
062 *
063 *   {@literal @}Override
064 *   public void define(Context context) {
065 *     try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(PATH), StandardCharsets.UTF_8)) {
066 *       NewRepository repository = context.createRepository("my_js", "js").setName("My Javascript Analyzer");
067 *       xmlLoader.load(repository, reader);
068 *       repository.done();
069 *     } catch (IOException e) {
070 *       throw new IllegalStateException(String.format("Fail to read file %s", PATH), e);
071 *     }
072 *   }
073 * }
074 * </pre>
075 *
076 * <h3>XML Format</h3>
077 * <pre>
078 * &lt;rules&gt;
079 *   &lt;rule&gt;
080 *     &lt;!-- Required key. Max length is 200 characters. --&gt;
081 *     &lt;key&gt;the-rule-key&lt;/key&gt;
082 *
083 *     &lt;!-- Required name. Max length is 200 characters. --&gt;
084 *     &lt;name&gt;The purpose of the rule&lt;/name&gt;
085 *
086 *     &lt;!-- Required description. No max length. --&gt;
087 *     &lt;description&gt;
088 *       &lt;![CDATA[The description]]&gt;
089 *     &lt;/description&gt;
090 *     &lt;!-- Optional format of description. Supported values are HTML (default) and MARKDOWN. --&gt;
091 *     &lt;descriptionFormat&gt;HTML&lt;/descriptionFormat&gt;
092 *
093 *     &lt;!-- Optional key for configuration of some rule engines --&gt;
094 *     &lt;internalKey&gt;Checker/TreeWalker/LocalVariableName&lt;/internalKey&gt;
095 *
096 *     &lt;!-- Default severity when enabling the rule in a Quality profile.  --&gt;
097 *     &lt;!-- Possible values are INFO, MINOR, MAJOR (default), CRITICAL, BLOCKER. --&gt;
098 *     &lt;severity&gt;BLOCKER&lt;/severity&gt;
099 *
100 *     &lt;!-- Possible values are SINGLE (default) and MULTIPLE for template rules --&gt;
101 *     &lt;cardinality&gt;SINGLE&lt;/cardinality&gt;
102 *
103 *     &lt;!-- Status displayed in rules console. Possible values are BETA, READY (default), DEPRECATED. --&gt;
104 *     &lt;status&gt;BETA&lt;/status&gt;
105 *
106 *     &lt;!-- Type as defined by the SonarQube Quality Model. Possible values are CODE_SMELL (default), BUG and VULNERABILITY.--&gt;
107 *     &lt;type&gt;BUG&lt;/type&gt;
108 *
109 *     &lt;!-- Optional tags. See org.sonar.api.server.rule.RuleTagFormat. The maximal length of all tags is 4000 characters. --&gt;
110 *     &lt;tag&gt;misra&lt;/tag&gt;
111 *     &lt;tag&gt;multi-threading&lt;/tag&gt;
112 *
113 *     &lt;!-- Optional parameters --&gt;
114 *     &lt;param&gt;
115 *       &lt;!-- Required key. Max length is 128 characters. --&gt;
116 *       &lt;key&gt;the-param-key&lt;/key&gt;
117 *       &lt;description&gt;
118 *         &lt;![CDATA[the optional description, in HTML format. Max length is 4000 characters.]]&gt;
119 *       &lt;/description&gt;
120 *       &lt;!-- Optional default value, used when enabling the rule in a Quality profile. Max length is 4000 characters. --&gt;
121 *       &lt;defaultValue&gt;42&lt;/defaultValue&gt;
122 *     &lt;/param&gt;
123 *     &lt;param&gt;
124 *       &lt;key&gt;another-param&lt;/key&gt;
125 *     &lt;/param&gt;
126 *
127 *     &lt;!-- Quality Model - type of debt remediation function --&gt;
128 *     &lt;!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values --&gt;
129 *     &lt;!-- It was previously named 'debtRemediationFunction' which is still supported but deprecated since 5.5 --&gt;
130 *     &lt;!-- Since 5.5 --&gt;
131 *     &lt;remediationFunction&gt;LINEAR_OFFSET&lt;/remediationFunction&gt;
132 *
133 *     &lt;!-- Quality Model - raw description of the "gap", used for some types of remediation functions. --&gt;
134 *     &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setGapDescription(String)} --&gt;
135 *     &lt;!-- It was previously named 'effortToFixDescription' which is still supported but deprecated since 5.5 --&gt;
136 *     &lt;!-- Since 5.5 --&gt;
137 *     &lt;gapDescription&gt;Effort to test one uncovered condition&lt;/gapFixDescription&gt;
138 *
139 *     &lt;!-- Quality Model - gap multiplier of debt remediation function. Must be defined only for some function types. --&gt;
140 *     &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
141 *     &lt;!-- It was previously named 'debtRemediationFunctionCoefficient' which is still supported but deprecated since 5.5 --&gt;
142 *     &lt;!-- Since 5.5 --&gt;
143 *     &lt;remediationFunctionGapMultiplier&gt;10min&lt;/remediationFunctionGapMultiplier&gt;
144 *
145 *     &lt;!-- Quality Model - base effort of debt remediation function. Must be defined only for some function types. --&gt;
146 *     &lt;!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --&gt;
147 *     &lt;!-- It was previously named 'debtRemediationFunctionOffset' which is still supported but deprecated since 5.5 --&gt;
148 *     &lt;!-- Since 5.5 --&gt;
149 *     &lt;remediationFunctionBaseEffort&gt;2min&lt;/remediationFunctionBaseEffort&gt;
150 *
151 *     &lt;!-- Deprecated field, replaced by "internalKey" --&gt;
152 *     &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
153 *
154 *     &lt;!-- Deprecated field, replaced by "severity" --&gt;
155 *     &lt;priority&gt;BLOCKER&lt;/priority&gt;
156 *
157 *   &lt;/rule&gt;
158 * &lt;/rules&gt;
159 * </pre>
160 *
161 * <h3>XML Example</h3>
162 * <pre>
163 * &lt;rules&gt;
164 *   &lt;rule&gt;
165 *     &lt;key&gt;S1442&lt;/key&gt;
166 *     &lt;name&gt;"alert(...)" should not be used&lt;/name&gt;
167 *     &lt;description&gt;alert(...) can be useful for debugging during development, but ...&lt;/description&gt;
168 *     &lt;tag&gt;cwe&lt;/tag&gt;
169 *     &lt;tag&gt;security&lt;/tag&gt;
170 *     &lt;tag&gt;user-experience&lt;/tag&gt;
171 *     &lt;debtRemediationFunction&gt;CONSTANT_ISSUE&lt;/debtRemediationFunction&gt;
172 *     &lt;debtRemediationFunctionBaseOffset&gt;10min&lt;/debtRemediationFunctionBaseOffset&gt;
173 *   &lt;/rule&gt;
174 *
175 *   &lt;!-- another rules... --&gt;
176 * &lt;/rules&gt;
177 * </pre>
178 *
179 * @see org.sonar.api.server.rule.RulesDefinition
180 * @since 4.3
181 */
182@ServerSide
183@ComputeEngineSide
184@SonarLintSide
185public class RulesDefinitionXmlLoader {
186
187  private enum DescriptionFormat {
188    HTML, MARKDOWN
189  }
190
191  /**
192   * Loads rules by reading the XML input stream. The input stream is not always closed by the method, so it
193   * should be handled by the caller.
194   * @since 4.3
195   */
196  public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) {
197    load(repo, input, Charset.forName(encoding));
198  }
199
200  /**
201   * @since 5.1
202   */
203  public void load(RulesDefinition.NewRepository repo, InputStream input, Charset charset) {
204    try (Reader reader = new InputStreamReader(input, charset)) {
205      load(repo, reader);
206    } catch (IOException e) {
207      throw new IllegalStateException("Error while reading XML rules definition for repository " + repo.key(), e);
208    }
209  }
210
211  /**
212   * Loads rules by reading the XML input stream. The reader is not closed by the method, so it
213   * should be handled by the caller.
214   * @since 4.3
215   */
216  public void load(RulesDefinition.NewRepository repo, Reader reader) {
217    XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
218    xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
219    xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
220    // just so it won't try to load DTD in if there's DOCTYPE
221    xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
222    xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
223    SMInputFactory inputFactory = new SMInputFactory(xmlFactory);
224    try {
225      SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
226      rootC.advance(); // <rules>
227
228      SMInputCursor rulesC = rootC.childElementCursor("rule");
229      while (rulesC.getNext() != null) {
230        // <rule>
231        processRule(repo, rulesC);
232      }
233
234    } catch (XMLStreamException e) {
235      throw new IllegalStateException("XML is not valid", e);
236    }
237  }
238
239  private static void processRule(RulesDefinition.NewRepository repo, SMInputCursor ruleC) throws XMLStreamException {
240    String key = null;
241    String name = null;
242    String description = null;
243    // enum is not used as variable type as we want to raise an exception with the rule key when format is not supported
244    String descriptionFormat = DescriptionFormat.HTML.name();
245    String internalKey = null;
246    String severity = Severity.defaultSeverity();
247    String type = null;
248    RuleStatus status = RuleStatus.defaultStatus();
249    boolean template = false;
250    String gapDescription = null;
251    String debtRemediationFunction = null;
252    String debtRemediationFunctionGapMultiplier = null;
253    String debtRemediationFunctionBaseEffort = null;
254    List<ParamStruct> params = new ArrayList<>();
255    List<String> tags = new ArrayList<>();
256
257    /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
258    String keyAttribute = ruleC.getAttrValue("key");
259    if (isNotBlank(keyAttribute)) {
260      key = trim(keyAttribute);
261    }
262    String priorityAttribute = ruleC.getAttrValue("priority");
263    if (isNotBlank(priorityAttribute)) {
264      severity = trim(priorityAttribute);
265    }
266
267    SMInputCursor cursor = ruleC.childElementCursor();
268    while (cursor.getNext() != null) {
269      String nodeName = cursor.getLocalName();
270
271      if (equalsIgnoreCase("name", nodeName)) {
272        name = nodeValue(cursor);
273
274      } else if (equalsIgnoreCase("type", nodeName)) {
275        type = nodeValue(cursor);
276
277      } else if (equalsIgnoreCase("description", nodeName)) {
278        description = nodeValue(cursor);
279
280      } else if (equalsIgnoreCase("descriptionFormat", nodeName)) {
281        descriptionFormat = nodeValue(cursor);
282
283      } else if (equalsIgnoreCase("key", nodeName)) {
284        key = nodeValue(cursor);
285
286      } else if (equalsIgnoreCase("configKey", nodeName)) {
287        // deprecated field, replaced by internalKey
288        internalKey = nodeValue(cursor);
289
290      } else if (equalsIgnoreCase("internalKey", nodeName)) {
291        internalKey = nodeValue(cursor);
292
293      } else if (equalsIgnoreCase("priority", nodeName) || equalsIgnoreCase("severity", nodeName)) {
294        // "priority" is deprecated field and has been replaced by "severity"
295        severity = nodeValue(cursor);
296
297      } else if (equalsIgnoreCase("cardinality", nodeName)) {
298        template = Cardinality.MULTIPLE == Cardinality.valueOf(nodeValue(cursor));
299
300      } else if (equalsIgnoreCase("gapDescription", nodeName) || equalsIgnoreCase("effortToFixDescription", nodeName)) {
301        gapDescription = nodeValue(cursor);
302
303      } else if (equalsIgnoreCase("remediationFunction", nodeName) || equalsIgnoreCase("debtRemediationFunction", nodeName)) {
304        debtRemediationFunction = nodeValue(cursor);
305
306      } else if (equalsIgnoreCase("remediationFunctionBaseEffort", nodeName) || equalsIgnoreCase("debtRemediationFunctionOffset", nodeName)) {
307        debtRemediationFunctionGapMultiplier = nodeValue(cursor);
308
309      } else if (equalsIgnoreCase("remediationFunctionGapMultiplier", nodeName) || equalsIgnoreCase("debtRemediationFunctionCoefficient", nodeName)) {
310        debtRemediationFunctionBaseEffort = nodeValue(cursor);
311
312      } else if (equalsIgnoreCase("status", nodeName)) {
313        String s = nodeValue(cursor);
314        if (s != null) {
315          status = RuleStatus.valueOf(s);
316        }
317
318      } else if (equalsIgnoreCase("param", nodeName)) {
319        params.add(processParameter(cursor));
320
321      } else if (equalsIgnoreCase("tag", nodeName)) {
322        tags.add(nodeValue(cursor));
323      }
324    }
325
326    try {
327      RulesDefinition.NewRule rule = repo.createRule(key)
328        .setSeverity(severity)
329        .setName(name)
330        .setInternalKey(internalKey)
331        .setTags(tags.toArray(new String[tags.size()]))
332        .setTemplate(template)
333        .setStatus(status)
334        .setGapDescription(gapDescription);
335      if (type != null) {
336        rule.setType(RuleType.valueOf(type));
337      }
338      fillDescription(rule, descriptionFormat, description);
339      fillRemediationFunction(rule, debtRemediationFunction, debtRemediationFunctionGapMultiplier, debtRemediationFunctionBaseEffort);
340      fillParams(rule, params);
341    } catch (Exception e) {
342      throw new IllegalStateException(format("Fail to load the rule with key [%s:%s]", repo.key(), key), e);
343    }
344  }
345
346  private static void fillDescription(RulesDefinition.NewRule rule, String descriptionFormat, @Nullable String description) {
347    if (isNotBlank(description)) {
348      switch (DescriptionFormat.valueOf(descriptionFormat)) {
349        case HTML:
350          rule.setHtmlDescription(description);
351          break;
352        case MARKDOWN:
353          rule.setMarkdownDescription(description);
354          break;
355        default:
356          throw new IllegalArgumentException("Value of descriptionFormat is not supported: " + descriptionFormat);
357      }
358    }
359  }
360
361  private static void fillRemediationFunction(RulesDefinition.NewRule rule, @Nullable String debtRemediationFunction,
362    @Nullable String functionOffset, @Nullable String functionCoeff) {
363    if (isNotBlank(debtRemediationFunction)) {
364      DebtRemediationFunction.Type functionType = DebtRemediationFunction.Type.valueOf(debtRemediationFunction);
365      rule.setDebtRemediationFunction(rule.debtRemediationFunctions().create(functionType, functionCoeff, functionOffset));
366    }
367  }
368
369  private static void fillParams(RulesDefinition.NewRule rule, List<ParamStruct> params) {
370    for (ParamStruct param : params) {
371      rule.createParam(param.key)
372        .setDefaultValue(param.defaultValue)
373        .setType(param.type)
374        .setDescription(param.description);
375    }
376  }
377
378  private static String nodeValue(SMInputCursor cursor) throws XMLStreamException {
379    return trim(cursor.collectDescendantText(false));
380  }
381
382  private static class ParamStruct {
383
384    String key;
385    String description;
386    String defaultValue;
387    RuleParamType type = RuleParamType.STRING;
388  }
389
390  private static ParamStruct processParameter(SMInputCursor ruleC) throws XMLStreamException {
391    ParamStruct param = new ParamStruct();
392
393    // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
394    String keyAttribute = ruleC.getAttrValue("key");
395    if (isNotBlank(keyAttribute)) {
396      param.key = trim(keyAttribute);
397    }
398
399    // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
400    String typeAttribute = ruleC.getAttrValue("type");
401    if (isNotBlank(typeAttribute)) {
402      param.type = RuleParamType.parse(typeAttribute);
403    }
404
405    SMInputCursor paramC = ruleC.childElementCursor();
406    while (paramC.getNext() != null) {
407      String propNodeName = paramC.getLocalName();
408      String propText = nodeValue(paramC);
409      if (equalsIgnoreCase("key", propNodeName)) {
410        param.key = propText;
411
412      } else if (equalsIgnoreCase("description", propNodeName)) {
413        param.description = propText;
414
415      } else if (equalsIgnoreCase("type", propNodeName)) {
416        param.type = RuleParamType.parse(propText);
417
418      } else if (equalsIgnoreCase("defaultValue", propNodeName)) {
419        param.defaultValue = propText;
420      }
421    }
422    return param;
423  }
424}