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