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     */
020    package org.sonar.api.rules;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    import com.google.common.collect.Maps;
024    import com.google.common.io.Closeables;
025    import org.apache.commons.io.FileUtils;
026    import org.apache.commons.lang.CharEncoding;
027    import org.apache.commons.lang.StringUtils;
028    import org.codehaus.staxmate.SMInputFactory;
029    import org.codehaus.staxmate.in.SMHierarchicCursor;
030    import org.codehaus.staxmate.in.SMInputCursor;
031    import org.sonar.api.PropertyType;
032    import org.sonar.api.ServerComponent;
033    import org.sonar.api.utils.SonarException;
034    import org.sonar.check.Cardinality;
035    
036    import javax.xml.stream.XMLInputFactory;
037    import javax.xml.stream.XMLStreamException;
038    
039    import java.io.File;
040    import java.io.IOException;
041    import java.io.InputStream;
042    import java.io.InputStreamReader;
043    import java.io.Reader;
044    import java.util.ArrayList;
045    import java.util.List;
046    import java.util.Map;
047    
048    /**
049     * @since 2.3
050     */
051    public 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    }