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