001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2013 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.rules;
021
022import com.google.common.annotations.VisibleForTesting;
023import com.google.common.base.Strings;
024import com.google.common.collect.Lists;
025import com.google.common.collect.Maps;
026import com.google.common.io.Closeables;
027import org.apache.commons.io.FileUtils;
028import org.apache.commons.lang.CharEncoding;
029import org.apache.commons.lang.StringUtils;
030import org.codehaus.staxmate.SMInputFactory;
031import org.codehaus.staxmate.in.SMHierarchicCursor;
032import org.codehaus.staxmate.in.SMInputCursor;
033import org.sonar.api.PropertyType;
034import org.sonar.api.ServerComponent;
035import org.sonar.api.utils.SonarException;
036import org.sonar.check.Cardinality;
037
038import javax.xml.stream.XMLInputFactory;
039import javax.xml.stream.XMLStreamException;
040
041import java.io.File;
042import java.io.IOException;
043import java.io.InputStream;
044import java.io.InputStreamReader;
045import java.io.Reader;
046import java.util.ArrayList;
047import java.util.List;
048import java.util.Map;
049
050/**
051 * @since 2.3
052 */
053public final class XMLRuleParser implements ServerComponent {
054  private static final Map<String, String> TYPE_MAP = typeMapWithDeprecatedValues();
055
056  public List<Rule> parse(File file) {
057    Reader reader = null;
058    try {
059      reader = new InputStreamReader(FileUtils.openInputStream(file), CharEncoding.UTF_8);
060      return parse(reader);
061
062    } catch (IOException e) {
063      throw new SonarException("Fail to load the file: " + file, e);
064
065    } finally {
066      Closeables.closeQuietly(reader);
067    }
068  }
069
070  /**
071   * Warning : the input stream is closed in this method
072   */
073  public List<Rule> parse(InputStream input) {
074    Reader reader = null;
075    try {
076      reader = new InputStreamReader(input, CharEncoding.UTF_8);
077      return parse(reader);
078
079    } catch (IOException e) {
080      throw new SonarException("Fail to load the xml stream", e);
081
082    } finally {
083      Closeables.closeQuietly(reader);
084    }
085  }
086
087  public List<Rule> parse(Reader reader) {
088    XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
089    xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
090    xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
091    // just so it won't try to load DTD in if there's DOCTYPE
092    xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
093    xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
094    SMInputFactory inputFactory = new SMInputFactory(xmlFactory);
095    try {
096      SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
097      rootC.advance(); // <rules>
098      List<Rule> rules = new ArrayList<Rule>();
099
100      SMInputCursor rulesC = rootC.childElementCursor("rule");
101      while (rulesC.getNext() != null) {
102        // <rule>
103        Rule rule = Rule.create();
104        rules.add(rule);
105
106        processRule(rule, rulesC);
107      }
108      return rules;
109
110    } catch (XMLStreamException e) {
111      throw new SonarException("XML is not valid", e);
112    }
113  }
114
115  private static void processRule(Rule rule, SMInputCursor ruleC) throws XMLStreamException {
116    /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */
117    String keyAttribute = ruleC.getAttrValue("key");
118    if (StringUtils.isNotBlank(keyAttribute)) {
119      rule.setKey(StringUtils.trim(keyAttribute));
120    }
121
122    /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */
123    String priorityAttribute = ruleC.getAttrValue("priority");
124    if (StringUtils.isNotBlank(priorityAttribute)) {
125      rule.setSeverity(RulePriority.valueOf(StringUtils.trim(priorityAttribute)));
126    }
127
128    List<String> tags = Lists.newArrayList();
129    SMInputCursor cursor = ruleC.childElementCursor();
130
131    while (cursor.getNext() != null) {
132      String nodeName = cursor.getLocalName();
133
134      if (StringUtils.equalsIgnoreCase("name", nodeName)) {
135        rule.setName(StringUtils.trim(cursor.collectDescendantText(false)));
136
137      } else if (StringUtils.equalsIgnoreCase("description", nodeName)) {
138        rule.setDescription(StringUtils.trim(cursor.collectDescendantText(false)));
139
140      } else if (StringUtils.equalsIgnoreCase("key", nodeName)) {
141        rule.setKey(StringUtils.trim(cursor.collectDescendantText(false)));
142
143      } else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) {
144        rule.setConfigKey(StringUtils.trim(cursor.collectDescendantText(false)));
145
146      } else if (StringUtils.equalsIgnoreCase("priority", nodeName)) {
147        rule.setSeverity(RulePriority.valueOf(StringUtils.trim(cursor.collectDescendantText(false))));
148
149      } else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) {
150        rule.setCardinality(Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false))));
151
152      } else if (StringUtils.equalsIgnoreCase("status", nodeName)) {
153        rule.setStatus(StringUtils.trim(cursor.collectDescendantText(false)));
154
155      } else if (StringUtils.equalsIgnoreCase("param", nodeName)) {
156        processParameter(rule, cursor);
157
158      } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) {
159        tags.add(StringUtils.trim(cursor.collectDescendantText(false)));
160      }
161    }
162    if (Strings.isNullOrEmpty(rule.getKey())) {
163      throw new SonarException("Node <key> is missing in <rule>");
164    }
165    rule.setTags(tags.toArray(new String[tags.size()]));
166  }
167
168  private static void processParameter(Rule rule, SMInputCursor ruleC) throws XMLStreamException {
169    RuleParam param = rule.createParameter();
170
171    String keyAttribute = ruleC.getAttrValue("key");
172    if (StringUtils.isNotBlank(keyAttribute)) {
173      /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */
174      param.setKey(StringUtils.trim(keyAttribute));
175    }
176
177    String typeAttribute = ruleC.getAttrValue("type");
178    if (StringUtils.isNotBlank(typeAttribute)) {
179      /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */
180      param.setType(type(StringUtils.trim(typeAttribute)));
181    }
182
183    SMInputCursor paramC = ruleC.childElementCursor();
184    while (paramC.getNext() != null) {
185      String propNodeName = paramC.getLocalName();
186      String propText = StringUtils.trim(paramC.collectDescendantText(false));
187      if (StringUtils.equalsIgnoreCase("key", propNodeName)) {
188        param.setKey(propText);
189
190      } else if (StringUtils.equalsIgnoreCase("description", propNodeName)) {
191        param.setDescription(propText);
192
193      } else if (StringUtils.equalsIgnoreCase("type", propNodeName)) {
194        param.setType(type(propText));
195
196      } else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) {
197        param.setDefaultValue(propText);
198      }
199    }
200    if (Strings.isNullOrEmpty(param.getKey())) {
201      throw new SonarException("Node <key> is missing in <param>");
202    }
203  }
204
205  private static Map<String, String> typeMapWithDeprecatedValues() {
206    Map<String, String> map = Maps.newHashMap();
207    map.put("i", PropertyType.INTEGER.name());
208    map.put("s", PropertyType.STRING.name());
209    map.put("b", PropertyType.BOOLEAN.name());
210    map.put("r", PropertyType.REGULAR_EXPRESSION.name());
211    map.put("s{}", "s{}");
212    map.put("i{}", "i{}");
213    for (PropertyType propertyType : PropertyType.values()) {
214      map.put(propertyType.name(), propertyType.name());
215    }
216    return map;
217  }
218
219  @VisibleForTesting
220  static String type(String type) {
221    String validType = TYPE_MAP.get(type);
222    if (null != validType) {
223      return validType;
224    }
225
226    if (type.matches(".\\[.+\\]")) {
227      return type;
228    }
229    throw new SonarException("Invalid property type [" + type + "]");
230  }
231
232}