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     * @see org.sonar.api.server.rule.RulesDefinition
081     * @since 4.3
082     */
083    public class RulesDefinitionXmlLoader implements ServerComponent {
084    
085      public void load(RulesDefinition.NewRepository repo, InputStream input, String encoding) {
086        Reader reader = null;
087        try {
088          reader = new InputStreamReader(input, encoding);
089          load(repo, reader);
090    
091        } catch (IOException e) {
092          throw new IllegalStateException("Fail to load XML file", e);
093    
094        } finally {
095          Closeables.closeQuietly(reader);
096        }
097      }
098    
099      public void load(RulesDefinition.NewRepository repo, Reader reader) {
100        XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
101        xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
102        xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
103        // just so it won't try to load DTD in if there's DOCTYPE
104        xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
105        xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
106        SMInputFactory inputFactory = new SMInputFactory(xmlFactory);
107        try {
108          SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
109          rootC.advance(); // <rules>
110    
111          SMInputCursor rulesC = rootC.childElementCursor("rule");
112          while (rulesC.getNext() != null) {
113            // <rule>
114            processRule(repo, rulesC);
115          }
116    
117        } catch (XMLStreamException e) {
118          throw new IllegalStateException("XML is not valid", e);
119        }
120      }
121    
122      private void processRule(RulesDefinition.NewRepository repo, SMInputCursor ruleC) throws XMLStreamException {
123        String key = null, name = null, description = null, internalKey = null, severity = Severity.defaultSeverity(), status = null;
124        Cardinality cardinality = Cardinality.SINGLE;
125        List<ParamStruct> params = new ArrayList<ParamStruct>();
126        List<String> tags = new ArrayList<String>();
127    
128        /* BACKWARD COMPATIBILITY WITH VERY OLD FORMAT */
129        String keyAttribute = ruleC.getAttrValue("key");
130        if (StringUtils.isNotBlank(keyAttribute)) {
131          key = StringUtils.trim(keyAttribute);
132        }
133        String priorityAttribute = ruleC.getAttrValue("priority");
134        if (StringUtils.isNotBlank(priorityAttribute)) {
135          severity = StringUtils.trim(priorityAttribute);
136        }
137    
138        SMInputCursor cursor = ruleC.childElementCursor();
139        while (cursor.getNext() != null) {
140          String nodeName = cursor.getLocalName();
141    
142          if (StringUtils.equalsIgnoreCase("name", nodeName)) {
143            name = StringUtils.trim(cursor.collectDescendantText(false));
144    
145          } else if (StringUtils.equalsIgnoreCase("description", nodeName)) {
146            description = StringUtils.trim(cursor.collectDescendantText(false));
147    
148          } else if (StringUtils.equalsIgnoreCase("key", nodeName)) {
149            key = StringUtils.trim(cursor.collectDescendantText(false));
150    
151          } else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) {
152            // deprecated field, replaced by internalKey
153            internalKey = StringUtils.trim(cursor.collectDescendantText(false));
154    
155          } else if (StringUtils.equalsIgnoreCase("internalKey", nodeName)) {
156            internalKey = StringUtils.trim(cursor.collectDescendantText(false));
157    
158          } else if (StringUtils.equalsIgnoreCase("priority", nodeName)) {
159            // deprecated field, replaced by severity
160            severity = StringUtils.trim(cursor.collectDescendantText(false));
161    
162          } else if (StringUtils.equalsIgnoreCase("severity", nodeName)) {
163            severity = StringUtils.trim(cursor.collectDescendantText(false));
164    
165          } else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) {
166            cardinality = Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false)));
167    
168          } else if (StringUtils.equalsIgnoreCase("status", nodeName)) {
169            status = StringUtils.trim(cursor.collectDescendantText(false));
170    
171          } else if (StringUtils.equalsIgnoreCase("param", nodeName)) {
172            params.add(processParameter(cursor));
173    
174          } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) {
175            tags.add(StringUtils.trim(cursor.collectDescendantText(false)));
176          }
177        }
178        RulesDefinition.NewRule rule = repo.createRule(key)
179          .setHtmlDescription(description)
180          .setSeverity(severity)
181          .setName(name)
182          .setInternalKey(internalKey)
183          .setTags(tags.toArray(new String[tags.size()]))
184          .setTemplate(cardinality == Cardinality.MULTIPLE);
185        if (status != null) {
186          rule.setStatus(RuleStatus.valueOf(status));
187        }
188        for (ParamStruct param : params) {
189          rule.createParam(param.key)
190            .setDefaultValue(param.defaultValue)
191            .setType(param.type)
192            .setDescription(param.description);
193        }
194      }
195    
196      private static class ParamStruct {
197        String key, description, defaultValue;
198        RuleParamType type = RuleParamType.STRING;
199      }
200    
201      private ParamStruct processParameter(SMInputCursor ruleC) throws XMLStreamException {
202        ParamStruct param = new ParamStruct();
203    
204        // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
205        String keyAttribute = ruleC.getAttrValue("key");
206        if (StringUtils.isNotBlank(keyAttribute)) {
207          param.key = StringUtils.trim(keyAttribute);
208        }
209    
210        // BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT
211        String typeAttribute = ruleC.getAttrValue("type");
212        if (StringUtils.isNotBlank(typeAttribute)) {
213          param.type = RuleParamType.parse(typeAttribute);
214        }
215    
216        SMInputCursor paramC = ruleC.childElementCursor();
217        while (paramC.getNext() != null) {
218          String propNodeName = paramC.getLocalName();
219          String propText = StringUtils.trim(paramC.collectDescendantText(false));
220          if (StringUtils.equalsIgnoreCase("key", propNodeName)) {
221            param.key = propText;
222    
223          } else if (StringUtils.equalsIgnoreCase("description", propNodeName)) {
224            param.description = propText;
225    
226          } else if (StringUtils.equalsIgnoreCase("type", propNodeName)) {
227            param.type = RuleParamType.parse(propText);
228    
229          } else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) {
230            param.defaultValue = propText;
231          }
232        }
233        return param;
234      }
235    }