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