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