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 org.apache.commons.lang.StringUtils; 023import org.codehaus.staxmate.SMInputFactory; 024import org.codehaus.staxmate.in.SMHierarchicCursor; 025import org.codehaus.staxmate.in.SMInputCursor; 026import org.sonar.api.server.ServerSide; 027import org.sonar.api.rule.RuleStatus; 028import org.sonar.api.rule.Severity; 029import org.sonar.check.Cardinality; 030 031import javax.xml.stream.XMLInputFactory; 032import javax.xml.stream.XMLStreamException; 033 034import java.io.IOException; 035import java.io.InputStream; 036import java.io.InputStreamReader; 037import java.io.Reader; 038import java.nio.charset.Charset; 039import java.util.ArrayList; 040import java.util.List; 041 042/** 043 * Helper class to load {@link RulesDefinition} extension point from a XML file. 044 * 045 * <h3>Example</h3> 046 * <pre> 047 * public class MyRules implements RulesDefinition { 048 * 049 * private final RulesDefinitionXmlLoader xmlLoader; 050 * 051 * public MyRules(RulesDefinitionXmlLoader xmlLoader) { 052 * this.xmlLoader = xmlLoader; 053 * } 054 * 055 * {@literal @}Override 056 * public void define(Context context) { 057 * NewRepository repository = context.createRepository("my-repo", "my-lang"); 058 * xmlLoader.load(repository, getClass().getResourceAsStream("/my-rules.xml"), StandardCharsets.UTF_8.name()); 059 * repository.done(); 060 * } 061 * } 062 * </pre> 063 * 064 * <h3>XML Format</h3> 065 * <pre> 066 * <rules> 067 * <rule> 068 * <key>the-required-rule-key</key>* 069 * <name>The required purpose of the rule</name> 070 ** <description> 071 * <![CDATA[Required HTML description]]> 072 * </description> 073 * 074 * <!-- Optional key for configuration of some rule engines --> 075 * <internalKey>Checker/TreeWalker/LocalVariableName</internalKey> 076 * 077 * <!-- Default severity when enabling the rule in a Quality profile. --> 078 * <!-- Possible values are INFO, MINOR, MAJOR (default), CRITICAL, BLOCKER. --> 079 * <severity>BLOCKER</severity> 080 * 081 * <!-- Possible values are SINGLE (default) and MULTIPLE for template rules --> 082 * <cardinality>SINGLE</cardinality> 083 * 084 * <!-- Status displayed in rules console. Possible values are BETA, READY (default), DEPRECATED. --> 085 * <status>BETA</status> 086 * 087 * <!-- Optional tags. See org.sonar.api.server.rule.RuleTagFormat. --> 088 * <tag>style</tag> 089 * <tag>security</tag> 090 * 091 * <param> 092 * <key>the-param-key</key> 093 * <description> 094 * <![CDATA[the optional param description]]> 095 * </description> 096 * <!-- Optional field to define the default value used when enabling the rule in a Quality profile --> 097 * <defaultValue>42</defaultValue> 098 * </param> 099 * <param> 100 * <key>another-param</key> 101 * </param> 102 * 103 * <!-- Deprecated field, replaced by "internalKey" --> 104 * <configKey>Checker/TreeWalker/LocalVariableName</configKey> 105 * 106 * <!-- Deprecated field, replaced by "severity" --> 107 * <priority>BLOCKER</priority> 108 * </rule> 109 * </rules> 110 * </pre> 111 * 112 * @see org.sonar.api.server.rule.RulesDefinition 113 * @since 4.3 114 */ 115@ServerSide 116public class RulesDefinitionXmlLoader { 117 118 public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) { 119 load(repo, input, Charset.forName(encoding)); 120 } 121 122 /** 123 * @since 5.1 124 */ 125 public void load(RulesDefinition.NewRepository repo, InputStream input, Charset charset) { 126 try (Reader reader = new InputStreamReader(input, charset)) { 127 load(repo, reader); 128 } catch (IOException e) { 129 throw new IllegalStateException("Error while reading XML rules definition for repository " + repo.key(), e); 130 } 131 } 132 133 public void load(RulesDefinition.NewRepository repo, Reader reader) { 134 XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); 135 xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); 136 xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); 137 // just so it won't try to load DTD in if there's DOCTYPE 138 xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); 139 xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); 140 SMInputFactory inputFactory = new SMInputFactory(xmlFactory); 141 try { 142 SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader); 143 rootC.advance(); // <rules> 144 145 SMInputCursor rulesC = rootC.childElementCursor("rule"); 146 while (rulesC.getNext() != null) { 147 // <rule> 148 processRule(repo, rulesC); 149 } 150 151 } catch (XMLStreamException e) { 152 throw new IllegalStateException("XML is not valid", e); 153 } 154 } 155 156 private void processRule(RulesDefinition.NewRepository repo, SMInputCursor ruleC) throws XMLStreamException { 157 String key = null; 158 String name = null; 159 String description = null; 160 String internalKey = null; 161 String severity = Severity.defaultSeverity(); 162 String status = null; 163 Cardinality cardinality = Cardinality.SINGLE; 164 List<ParamStruct> params = new ArrayList<>(); 165 List<String> tags = new ArrayList<>(); 166 167 /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */ 168 String keyAttribute = ruleC.getAttrValue("key"); 169 if (StringUtils.isNotBlank(keyAttribute)) { 170 key = StringUtils.trim(keyAttribute); 171 } 172 String priorityAttribute = ruleC.getAttrValue("priority"); 173 if (StringUtils.isNotBlank(priorityAttribute)) { 174 severity = StringUtils.trim(priorityAttribute); 175 } 176 177 SMInputCursor cursor = ruleC.childElementCursor(); 178 while (cursor.getNext() != null) { 179 String nodeName = cursor.getLocalName(); 180 181 if (StringUtils.equalsIgnoreCase("name", nodeName)) { 182 name = StringUtils.trim(cursor.collectDescendantText(false)); 183 184 } else if (StringUtils.equalsIgnoreCase("description", nodeName)) { 185 description = StringUtils.trim(cursor.collectDescendantText(false)); 186 187 } else if (StringUtils.equalsIgnoreCase("key", nodeName)) { 188 key = StringUtils.trim(cursor.collectDescendantText(false)); 189 190 } else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) { 191 // deprecated field, replaced by internalKey 192 internalKey = StringUtils.trim(cursor.collectDescendantText(false)); 193 194 } else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) { 195 internalKey = StringUtils.trim(cursor.collectDescendantText(false)); 196 197 } else if (StringUtils.equalsIgnoreCase("priority", nodeName)) { 198 // deprecated field, replaced by severity 199 severity = StringUtils.trim(cursor.collectDescendantText(false)); 200 201 } else if (StringUtils.equalsIgnoreCase("severity", nodeName)) { 202 severity = StringUtils.trim(cursor.collectDescendantText(false)); 203 204 } else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) { 205 cardinality = Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false))); 206 207 } else if (StringUtils.equalsIgnoreCase("status", nodeName)) { 208 status = StringUtils.trim(cursor.collectDescendantText(false)); 209 210 } else if (StringUtils.equalsIgnoreCase("param", nodeName)) { 211 params.add(processParameter(cursor)); 212 213 } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) { 214 tags.add(StringUtils.trim(cursor.collectDescendantText(false))); 215 } 216 } 217 RulesDefinition.NewRule rule = repo.createRule(key) 218 .setHtmlDescription(description) 219 .setSeverity(severity) 220 .setName(name) 221 .setInternalKey(internalKey) 222 .setTags(tags.toArray(new String[tags.size()])) 223 .setTemplate(cardinality == Cardinality.MULTIPLE); 224 if (status != null) { 225 rule.setStatus(RuleStatus.valueOf(status)); 226 } 227 for (ParamStruct param : params) { 228 rule.createParam(param.key) 229 .setDefaultValue(param.defaultValue) 230 .setType(param.type) 231 .setDescription(param.description); 232 } 233 } 234 235 private static class ParamStruct { 236 String key; 237 String description; 238 String defaultValue; 239 RuleParamType type = RuleParamType.STRING; 240 } 241 242 private ParamStruct processParameter(SMInputCursor ruleC) throws XMLStreamException { 243 ParamStruct param = new ParamStruct(); 244 245 // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT 246 String keyAttribute = ruleC.getAttrValue("key"); 247 if (StringUtils.isNotBlank(keyAttribute)) { 248 param.key = StringUtils.trim(keyAttribute); 249 } 250 251 // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT 252 String typeAttribute = ruleC.getAttrValue("type"); 253 if (StringUtils.isNotBlank(typeAttribute)) { 254 param.type = RuleParamType.parse(typeAttribute); 255 } 256 257 SMInputCursor paramC = ruleC.childElementCursor(); 258 while (paramC.getNext() != null) { 259 String propNodeName = paramC.getLocalName(); 260 String propText = StringUtils.trim(paramC.collectDescendantText(false)); 261 if (StringUtils.equalsIgnoreCase("key", propNodeName)) { 262 param.key = propText; 263 264 } else if (StringUtils.equalsIgnoreCase("description", propNodeName)) { 265 param.description = propText; 266 267 } else if (StringUtils.equalsIgnoreCase("type", propNodeName)) { 268 param.type = RuleParamType.parse(propText); 269 270 } else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) { 271 param.defaultValue = propText; 272 } 273 } 274 return param; 275 } 276}