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 * &lt;rules&gt;
067 *   &lt;rule&gt;
068 *     &lt;key&gt;the-required-rule-key&lt;/key&gt;*
069 *     &lt;name&gt;The required purpose of the rule&lt;/name&gt;
070 **     &lt;description&gt;
071 *       &lt;![CDATA[Required HTML description]]&gt;
072 *     &lt;/description&gt;
073 *
074 *     &lt;!-- Optional key for configuration of some rule engines --&gt;
075 *     &lt;internalKey&gt;Checker/TreeWalker/LocalVariableName&lt;/internalKey&gt;
076 *
077 *     &lt;!-- Default severity when enabling the rule in a Quality profile.  --&gt;
078 *     &lt;!-- Possible values are INFO, MINOR, MAJOR (default), CRITICAL, BLOCKER. --&gt;
079 *     &lt;severity&gt;BLOCKER&lt;/severity&gt;
080 *
081 *     &lt;!-- Possible values are SINGLE (default) and MULTIPLE for template rules --&gt;
082 *     &lt;cardinality&gt;SINGLE&lt;/cardinality&gt;
083 *
084 *     &lt;!-- Status displayed in rules console. Possible values are BETA, READY (default), DEPRECATED. --&gt;
085 *     &lt;status&gt;BETA&lt;/status&gt;
086 *
087 *     &lt;!-- Optional tags. See org.sonar.api.server.rule.RuleTagFormat. --&gt;
088 *     &lt;tag&gt;style&lt;/tag&gt;
089 *     &lt;tag&gt;security&lt;/tag&gt;
090 *
091 *     &lt;param&gt;
092 *       &lt;key&gt;the-param-key&lt;/key&gt;
093 *       &lt;description&gt;
094 *         &lt;![CDATA[the optional param description]]&gt;
095 *       &lt;/description&gt;
096 *       &lt;!-- Optional field to define the default value used when enabling the rule in a Quality profile --&gt;
097 *       &lt;defaultValue&gt;42&lt;/defaultValue&gt;
098 *     &lt;/param&gt;
099 *     &lt;param&gt;
100 *       &lt;key&gt;another-param&lt;/key&gt;
101 *     &lt;/param&gt;
102 *
103 *     &lt;!-- Deprecated field, replaced by "internalKey" --&gt;
104 *     &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
105 *
106 *     &lt;!-- Deprecated field, replaced by "severity" --&gt;
107 *     &lt;priority&gt;BLOCKER&lt;/priority&gt;
108 *   &lt;/rule&gt;
109 * &lt;/rules&gt;
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}