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     */
020    package org.sonar.api.server.rule;
021    
022    import com.google.common.io.Closeables;
023    import org.apache.commons.lang.StringUtils;
024    import org.codehaus.staxmate.SMInputFactory;
025    import org.codehaus.staxmate.in.SMHierarchicCursor;
026    import org.codehaus.staxmate.in.SMInputCursor;
027    import org.sonar.api.ServerComponent;
028    import org.sonar.api.rule.RuleStatus;
029    import org.sonar.api.rule.Severity;
030    import org.sonar.check.Cardinality;
031    
032    import javax.xml.stream.XMLInputFactory;
033    import javax.xml.stream.XMLStreamException;
034    import java.io.IOException;
035    import java.io.InputStream;
036    import java.io.InputStreamReader;
037    import java.io.Reader;
038    import java.util.ArrayList;
039    import java.util.List;
040    
041    /**
042     * Loads definitions of rules from a XML file.
043     *
044     * <h3>XML Format</h3>
045     * <pre>
046     * &lt;rules&gt;
047     *   &lt;rule&gt;
048     *     &lt;!-- required fields --&gt;
049     *     &lt;key&gt;the-rule-key&lt;/key&gt;
050     *     &lt;name&gt;The purpose of the rule&lt;/name&gt;
051     *
052     *     &lt;!-- optional fields --&gt;
053     *     &lt;description&gt;
054     *       &lt;![CDATA[The description]]&gt;
055     *     &lt;/description&gt;
056     *     &lt;internalKey&gt;Checker/TreeWalker/LocalVariableName&lt;/internalKey&gt;
057     *     &lt;severity&gt;BLOCKER&lt;/severity&gt;
058     *     &lt;cardinality&gt;MULTIPLE&lt;/cardinality&gt;
059     *     &lt;status&gt;BETA&lt;/status&gt;
060     *     &lt;param&gt;
061     *       &lt;key&gt;the-param-key&lt;/key&gt;
062     *       &lt;tag&gt;style&lt;/tag&gt;
063     *       &lt;tag&gt;security&lt;/tag&gt;
064     *       &lt;description&gt;
065     *         &lt;![CDATA[the param-description]]&gt;
066     *       &lt;/description&gt;
067     *       &lt;defaultValue&gt;42&lt;/defaultValue&gt;
068     *     &lt;/param&gt;
069     *     &lt;param&gt;
070     *       &lt;key&gt;another-param&lt;/key&gt;
071     *     &lt;/param&gt;
072     *
073     *     &lt;!-- deprecated fields --&gt;
074     *     &lt;configKey&gt;Checker/TreeWalker/LocalVariableName&lt;/configKey&gt;
075     *     &lt;priority&gt;BLOCKER&lt;/priority&gt;
076     *   &lt;/rule&gt;
077     * &lt;/rules&gt;
078     * </pre>
079     *
080     * @since 4.3
081     */
082    public class RulesDefinitionXmlLoader implements ServerComponent {
083    
084      public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) {
085        Reader reader = null;
086        try {
087          reader = new InputStreamReader(input, encoding);
088          load(repo, reader);
089    
090        } catch (IOException e) {
091          throw new IllegalStateException("Fail to load XML file", e);
092    
093        } finally {
094          Closeables.closeQuietly(reader);
095        }
096      }
097    
098      public void load(RulesDefinition.NewRepository repo, Reader reader) {
099        XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
100        xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
101        xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
102        // just so it won't try to load DTD in if there's DOCTYPE
103        xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
104        xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
105        SMInputFactory inputFactory = new SMInputFactory(xmlFactory);
106        try {
107          SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
108          rootC.advance(); // <rules>
109    
110          SMInputCursor rulesC = rootC.childElementCursor("rule");
111          while (rulesC.getNext() != null) {
112            // <rule>
113            processRule(repo, rulesC);
114          }
115    
116        } catch (XMLStreamException e) {
117          throw new IllegalStateException("XML is not valid", e);
118        }
119      }
120    
121      private void processRule(RulesDefinition.NewRepository repo, SMInputCursor ruleC) throws XMLStreamException {
122        String key = null, name = null, description = null, internalKey = null, severity = Severity.defaultSeverity(), status = null;
123        Cardinality cardinality = Cardinality.SINGLE;
124        List<ParamStruct> params = new ArrayList<ParamStruct>();
125        List<String> tags = new ArrayList<String>();
126    
127        /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
128        String keyAttribute = ruleC.getAttrValue("key");
129        if (StringUtils.isNotBlank(keyAttribute)) {
130          key = StringUtils.trim(keyAttribute);
131        }
132        String priorityAttribute = ruleC.getAttrValue("priority");
133        if (StringUtils.isNotBlank(priorityAttribute)) {
134          severity = StringUtils.trim(priorityAttribute);
135        }
136    
137        SMInputCursor cursor = ruleC.childElementCursor();
138        while (cursor.getNext() != null) {
139          String nodeName = cursor.getLocalName();
140    
141          if (StringUtils.equalsIgnoreCase("name", nodeName)) {
142            name = StringUtils.trim(cursor.collectDescendantText(false));
143    
144          } else if (StringUtils.equalsIgnoreCase("description", nodeName)) {
145            description = StringUtils.trim(cursor.collectDescendantText(false));
146    
147          } else if (StringUtils.equalsIgnoreCase("key", nodeName)) {
148            key = StringUtils.trim(cursor.collectDescendantText(false));
149    
150          } else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) {
151            // deprecated field, replaced by internalKey
152            internalKey = StringUtils.trim(cursor.collectDescendantText(false));
153    
154          } else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) {
155            internalKey = StringUtils.trim(cursor.collectDescendantText(false));
156    
157          } else if (StringUtils.equalsIgnoreCase("priority", nodeName)) {
158            // deprecated field, replaced by severity
159            severity = StringUtils.trim(cursor.collectDescendantText(false));
160    
161          } else if (StringUtils.equalsIgnoreCase("severity", nodeName)) {
162            severity = StringUtils.trim(cursor.collectDescendantText(false));
163    
164          } else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) {
165            cardinality = Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false)));
166    
167          } else if (StringUtils.equalsIgnoreCase("status", nodeName)) {
168            status = StringUtils.trim(cursor.collectDescendantText(false));
169    
170          } else if (StringUtils.equalsIgnoreCase("param", nodeName)) {
171            params.add(processParameter(cursor));
172    
173          } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) {
174            tags.add(StringUtils.trim(cursor.collectDescendantText(false)));
175          }
176        }
177        RulesDefinition.NewRule rule = repo.createRule(key)
178          .setHtmlDescription(description)
179          .setSeverity(severity)
180          .setName(name)
181          .setInternalKey(internalKey)
182          .setTags(tags.toArray(new String[tags.size()]))
183          .setTemplate(cardinality == Cardinality.MULTIPLE);
184        if (status != null) {
185          rule.setStatus(RuleStatus.valueOf(status));
186        }
187        for (ParamStruct param : params) {
188          rule.createParam(param.key)
189            .setDefaultValue(param.defaultValue)
190            .setType(param.type)
191            .setDescription(param.description);
192        }
193      }
194    
195      private static class ParamStruct {
196        String key, description, defaultValue;
197        RuleParamType type = RuleParamType.STRING;
198      }
199    
200      private ParamStruct processParameter(SMInputCursor ruleC) throws XMLStreamException {
201        ParamStruct param = new ParamStruct();
202    
203        // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
204        String keyAttribute = ruleC.getAttrValue("key");
205        if (StringUtils.isNotBlank(keyAttribute)) {
206          param.key = StringUtils.trim(keyAttribute);
207        }
208    
209        // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
210        String typeAttribute = ruleC.getAttrValue("type");
211        if (StringUtils.isNotBlank(typeAttribute)) {
212          param.type = RuleParamType.parse(typeAttribute);
213        }
214    
215        SMInputCursor paramC = ruleC.childElementCursor();
216        while (paramC.getNext() != null) {
217          String propNodeName = paramC.getLocalName();
218          String propText = StringUtils.trim(paramC.collectDescendantText(false));
219          if (StringUtils.equalsIgnoreCase("key", propNodeName)) {
220            param.key = propText;
221    
222          } else if (StringUtils.equalsIgnoreCase("description", propNodeName)) {
223            param.description = propText;
224    
225          } else if (StringUtils.equalsIgnoreCase("type", propNodeName)) {
226            param.type = RuleParamType.parse(propText);
227    
228          } else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) {
229            param.defaultValue = propText;
230          }
231        }
232        return param;
233      }
234    }