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