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