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