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