001/* 002 * SonarQube 003 * Copyright (C) 2009-2016 SonarSource SA 004 * mailto:contact 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 * <rules> 079 * <rule> 080 * <!-- Required key. Max length is 200 characters. --> 081 * <key>the-rule-key</key> 082 * 083 * <!-- Required name. Max length is 200 characters. --> 084 * <name>The purpose of the rule</name> 085 * 086 * <!-- Required description. No max length. --> 087 * <description> 088 * <![CDATA[The description]]> 089 * </description> 090 * <!-- Optional format of description. Supported values are HTML (default) and MARKDOWN. --> 091 * <descriptionFormat>HTML</descriptionFormat> 092 * 093 * <!-- Optional key for configuration of some rule engines --> 094 * <internalKey>Checker/TreeWalker/LocalVariableName</internalKey> 095 * 096 * <!-- Default severity when enabling the rule in a Quality profile. --> 097 * <!-- Possible values are INFO, MINOR, MAJOR (default), CRITICAL, BLOCKER. --> 098 * <severity>BLOCKER</severity> 099 * 100 * <!-- Possible values are SINGLE (default) and MULTIPLE for template rules --> 101 * <cardinality>SINGLE</cardinality> 102 * 103 * <!-- Status displayed in rules console. Possible values are BETA, READY (default), DEPRECATED. --> 104 * <status>BETA</status> 105 * 106 * <!-- Type as defined by the SonarQube Quality Model. Possible values are CODE_SMELL (default), BUG and VULNERABILITY.--> 107 * <type>BUG</type> 108 * 109 * <!-- Optional tags. See org.sonar.api.server.rule.RuleTagFormat. The maximal length of all tags is 4000 characters. --> 110 * <tag>misra</tag> 111 * <tag>multi-threading</tag> 112 * 113 * <!-- Optional parameters --> 114 * <param> 115 * <!-- Required key. Max length is 128 characters. --> 116 * <key>the-param-key</key> 117 * <description> 118 * <![CDATA[the optional description, in HTML format. Max length is 4000 characters.]]> 119 * </description> 120 * <!-- Optional default value, used when enabling the rule in a Quality profile. Max length is 4000 characters. --> 121 * <defaultValue>42</defaultValue> 122 * </param> 123 * <param> 124 * <key>another-param</key> 125 * </param> 126 * 127 * <!-- Quality Model - type of debt remediation function --> 128 * <!-- See enum {@link org.sonar.api.server.debt.DebtRemediationFunction.Type} for supported values --> 129 * <!-- It was previously named 'debtRemediationFunction' which is still supported but deprecated since 5.5 --> 130 * <!-- Since 5.5 --> 131 * <remediationFunction>LINEAR_OFFSET</remediationFunction> 132 * 133 * <!-- Quality Model - raw description of the "gap", used for some types of remediation functions. --> 134 * <!-- See {@link org.sonar.api.server.rule.RulesDefinition.NewRule#setGapDescription(String)} --> 135 * <!-- It was previously named 'effortToFixDescription' which is still supported but deprecated since 5.5 --> 136 * <!-- Since 5.5 --> 137 * <gapDescription>Effort to test one uncovered condition</gapFixDescription> 138 * 139 * <!-- Quality Model - gap multiplier of debt remediation function. Must be defined only for some function types. --> 140 * <!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --> 141 * <!-- It was previously named 'debtRemediationFunctionCoefficient' which is still supported but deprecated since 5.5 --> 142 * <!-- Since 5.5 --> 143 * <remediationFunctionGapMultiplier>10min</remediationFunctionGapMultiplier> 144 * 145 * <!-- Quality Model - base effort of debt remediation function. Must be defined only for some function types. --> 146 * <!-- See {@link org.sonar.api.server.rule.RulesDefinition.DebtRemediationFunctions} --> 147 * <!-- It was previously named 'debtRemediationFunctionOffset' which is still supported but deprecated since 5.5 --> 148 * <!-- Since 5.5 --> 149 * <remediationFunctionBaseEffort>2min</remediationFunctionBaseEffort> 150 * 151 * <!-- Deprecated field, replaced by "internalKey" --> 152 * <configKey>Checker/TreeWalker/LocalVariableName</configKey> 153 * 154 * <!-- Deprecated field, replaced by "severity" --> 155 * <priority>BLOCKER</priority> 156 * 157 * </rule> 158 * </rules> 159 * </pre> 160 * 161 * <h3>XML Example</h3> 162 * <pre> 163 * <rules> 164 * <rule> 165 * <key>S1442</key> 166 * <name>"alert(...)" should not be used</name> 167 * <description>alert(...) can be useful for debugging during development, but ...</description> 168 * <tag>cwe</tag> 169 * <tag>security</tag> 170 * <tag>user-experience</tag> 171 * <debtRemediationFunction>CONSTANT_ISSUE</debtRemediationFunction> 172 * <debtRemediationFunctionBaseOffset>10min</debtRemediationFunctionBaseOffset> 173 * </rule> 174 * 175 * <!-- another rules... --> 176 * </rules> 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 IllegalArgumentException(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}