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.ServerComponent;
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 * Loads definitions of rules from a XML file.
044 *
045 * <h3>XML Format</h3>
046 * <pre>
047 * &lt;rules&gt;
048 *   &lt;rule&gt;
049 *     &lt;!-- required fields --&gt;
050 *     &lt;key&gt;the-rule-key&lt;/key&gt;
051 *     &lt;name&gt;The purpose of the rule&lt;/name&gt;
052 *
053 *     &lt;!-- optional fields --&gt;
054 *     &lt;description&gt;
055 *       &lt;![CDATA[The description]]&gt;
056 *     &lt;/description&gt;
057 *     &lt;internalKey&gt;Checker/TreeWalker/LocalVariableName&lt;/internalKey&gt;
058 *     &lt;severity&gt;BLOCKER&lt;/severity&gt;
059 *     &lt;cardinality&gt;MULTIPLE&lt;/cardinality&gt;
060 *     &lt;status&gt;BETA&lt;/status&gt;
061 *     &lt;tag&gt;style&lt;/tag&gt;
062 *     &lt;tag&gt;security&lt;/tag&gt;
063 *     &lt;param&gt;
064 *       &lt;key&gt;the-param-key&lt;/key&gt;
065 *       &lt;description&gt;
066 *         &lt;![CDATA[the param-description]]&gt;
067 *       &lt;/description&gt;
068 *       &lt;defaultValue&gt;42&lt;/defaultValue&gt;
069 *     &lt;/param&gt;
070 *     &lt;param&gt;
071 *       &lt;key&gt;another-param&lt;/key&gt;
072 *     &lt;/param&gt;
073 *
074 *     &lt;!-- deprecated fields --&gt;
075 *     &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
076 *     &lt;priority&gt;BLOCKER&lt;/priority&gt;
077 *   &lt;/rule&gt;
078 * &lt;/rules&gt;
079 * </pre>
080 *
081 * @see org.sonar.api.server.rule.RulesDefinition
082 * @since 4.3
083 */
084public class RulesDefinitionXmlLoader implements ServerComponent {
085
086  public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) {
087    load(repo, input, Charset.forName(encoding));
088  }
089
090  /**
091   * @since 5.1
092   */
093  public void load(RulesDefinition.NewRepository repo, InputStream input, Charset charset) {
094    try (Reader reader = new InputStreamReader(input, charset)) {
095      load(repo, reader);
096    } catch (IOException e) {
097      throw new IllegalStateException("Error while reading XML rules definition for repository " + repo.key(), e);
098    }
099  }
100
101  public void load(RulesDefinition.NewRepository repo, Reader reader) {
102    XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
103    xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
104    xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
105    // just so it won't try to load DTD in if there's DOCTYPE
106    xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
107    xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
108    SMInputFactory inputFactory = new SMInputFactory(xmlFactory);
109    try {
110      SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
111      rootC.advance(); // <rules>
112
113      SMInputCursor rulesC = rootC.childElementCursor("rule");
114      while (rulesC.getNext() != null) {
115        // <rule>
116        processRule(repo, rulesC);
117      }
118
119    } catch (XMLStreamException e) {
120      throw new IllegalStateException("XML is not valid", e);
121    }
122  }
123
124  private void processRule(RulesDefinition.NewRepository repo, SMInputCursor ruleC) throws XMLStreamException {
125    String key = null, name = null, description = null, internalKey = null, severity = Severity.defaultSeverity(), status = null;
126    Cardinality cardinality = Cardinality.SINGLE;
127    List<ParamStruct> params = new ArrayList<ParamStruct>();
128    List<String> tags = new ArrayList<String>();
129
130    /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
131    String keyAttribute = ruleC.getAttrValue("key");
132    if (StringUtils.isNotBlank(keyAttribute)) {
133      key = StringUtils.trim(keyAttribute);
134    }
135    String priorityAttribute = ruleC.getAttrValue("priority");
136    if (StringUtils.isNotBlank(priorityAttribute)) {
137      severity = StringUtils.trim(priorityAttribute);
138    }
139
140    SMInputCursor cursor = ruleC.childElementCursor();
141    while (cursor.getNext() != null) {
142      String nodeName = cursor.getLocalName();
143
144      if (StringUtils.equalsIgnoreCase("name", nodeName)) {
145        name = StringUtils.trim(cursor.collectDescendantText(false));
146
147      } else if (StringUtils.equalsIgnoreCase("description", nodeName)) {
148        description = StringUtils.trim(cursor.collectDescendantText(false));
149
150      } else if (StringUtils.equalsIgnoreCase("key", nodeName)) {
151        key = StringUtils.trim(cursor.collectDescendantText(false));
152
153      } else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) {
154        // deprecated field, replaced by internalKey
155        internalKey = StringUtils.trim(cursor.collectDescendantText(false));
156
157      } else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) {
158        internalKey = StringUtils.trim(cursor.collectDescendantText(false));
159
160      } else if (StringUtils.equalsIgnoreCase("priority", nodeName)) {
161        // deprecated field, replaced by severity
162        severity = StringUtils.trim(cursor.collectDescendantText(false));
163
164      } else if (StringUtils.equalsIgnoreCase("severity", nodeName)) {
165        severity = StringUtils.trim(cursor.collectDescendantText(false));
166
167      } else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) {
168        cardinality = Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false)));
169
170      } else if (StringUtils.equalsIgnoreCase("status", nodeName)) {
171        status = StringUtils.trim(cursor.collectDescendantText(false));
172
173      } else if (StringUtils.equalsIgnoreCase("param", nodeName)) {
174        params.add(processParameter(cursor));
175
176      } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) {
177        tags.add(StringUtils.trim(cursor.collectDescendantText(false)));
178      }
179    }
180    RulesDefinition.NewRule rule = repo.createRule(key)
181      .setHtmlDescription(description)
182      .setSeverity(severity)
183      .setName(name)
184      .setInternalKey(internalKey)
185      .setTags(tags.toArray(new String[tags.size()]))
186      .setTemplate(cardinality == Cardinality.MULTIPLE);
187    if (status != null) {
188      rule.setStatus(RuleStatus.valueOf(status));
189    }
190    for (ParamStruct param : params) {
191      rule.createParam(param.key)
192        .setDefaultValue(param.defaultValue)
193        .setType(param.type)
194        .setDescription(param.description);
195    }
196  }
197
198  private static class ParamStruct {
199    String key, description, defaultValue;
200    RuleParamType type = RuleParamType.STRING;
201  }
202
203  private ParamStruct processParameter(SMInputCursor ruleC) throws XMLStreamException {
204    ParamStruct param = new ParamStruct();
205
206    // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
207    String keyAttribute = ruleC.getAttrValue("key");
208    if (StringUtils.isNotBlank(keyAttribute)) {
209      param.key = StringUtils.trim(keyAttribute);
210    }
211
212    // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
213    String typeAttribute = ruleC.getAttrValue("type");
214    if (StringUtils.isNotBlank(typeAttribute)) {
215      param.type = RuleParamType.parse(typeAttribute);
216    }
217
218    SMInputCursor paramC = ruleC.childElementCursor();
219    while (paramC.getNext() != null) {
220      String propNodeName = paramC.getLocalName();
221      String propText = StringUtils.trim(paramC.collectDescendantText(false));
222      if (StringUtils.equalsIgnoreCase("key", propNodeName)) {
223        param.key = propText;
224
225      } else if (StringUtils.equalsIgnoreCase("description", propNodeName)) {
226        param.description = propText;
227
228      } else if (StringUtils.equalsIgnoreCase("type", propNodeName)) {
229        param.type = RuleParamType.parse(propText);
230
231      } else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) {
232        param.defaultValue = propText;
233      }
234    }
235    return param;
236  }
237}