001/*
002 * SonarQube
003 * Copyright (C) 2009-2017 SonarSource SA
004 * mailto:info AT sonarsource DOT com
005 *
006 * This program 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 * This program 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 java.io.File;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.Reader;
032import java.util.ArrayList;
033import java.util.List;
034import java.util.Map;
035import javax.xml.stream.XMLInputFactory;
036import javax.xml.stream.XMLStreamException;
037import org.apache.commons.io.FileUtils;
038import org.apache.commons.lang.CharEncoding;
039import org.apache.commons.lang.StringUtils;
040import org.codehaus.staxmate.SMInputFactory;
041import org.codehaus.staxmate.in.SMHierarchicCursor;
042import org.codehaus.staxmate.in.SMInputCursor;
043import org.sonar.api.PropertyType;
044import org.sonar.api.ce.ComputeEngineSide;
045import org.sonar.api.server.ServerSide;
046import org.sonar.api.utils.SonarException;
047import org.sonar.check.Cardinality;
048
049/**
050 * @since 2.3
051 * @deprecated in 4.2. Replaced by org.sonar.api.server.rule.RulesDefinition and org.sonar.api.server.rule.RulesDefinitionXmlLoader
052 */
053@Deprecated
054@ServerSide
055@ComputeEngineSide
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}