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 */
020package org.sonar.api.profiles;
021
022import com.google.common.base.Charsets;
023import org.apache.commons.io.IOUtils;
024import org.apache.commons.lang.StringUtils;
025import org.codehaus.staxmate.SMInputFactory;
026import org.codehaus.staxmate.in.SMHierarchicCursor;
027import org.codehaus.staxmate.in.SMInputCursor;
028import org.sonar.api.ServerComponent;
029import org.sonar.api.measures.Metric;
030import org.sonar.api.measures.MetricFinder;
031import org.sonar.api.rules.ActiveRule;
032import org.sonar.api.rules.Rule;
033import org.sonar.api.rules.RuleFinder;
034import org.sonar.api.rules.RulePriority;
035import org.sonar.api.utils.Logs;
036import org.sonar.api.utils.ValidationMessages;
037
038import javax.xml.stream.XMLInputFactory;
039import javax.xml.stream.XMLStreamException;
040
041import java.io.InputStreamReader;
042import java.io.Reader;
043import java.util.HashMap;
044import java.util.Map;
045
046/**
047 * TODO should be an interface
048 *
049 * @since 2.3
050 */
051public final class XMLProfileParser implements ServerComponent {
052
053  private final RuleFinder ruleFinder;
054  private MetricFinder metricFinder;
055
056  /**
057   * For backward compatibility.
058   *
059   * @deprecated since 2.5. Plugins shouldn't directly instantiate this class,
060   *             because it should be retrieved as an IoC dependency.
061   */
062  @Deprecated
063  public XMLProfileParser(RuleFinder ruleFinder) {
064    this.ruleFinder = ruleFinder;
065  }
066
067  /**
068   * @deprecated since 2.5. Plugins shouldn't directly instantiate this class,
069   *             because it should be retrieved as an IoC dependency.
070   */
071  @Deprecated
072  public XMLProfileParser(RuleFinder ruleFinder, MetricFinder metricFinder) {
073    this.ruleFinder = ruleFinder;
074    this.metricFinder = metricFinder;
075  }
076
077  public RulesProfile parseResource(ClassLoader classloader, String xmlClassPath, ValidationMessages messages) {
078    Reader reader = new InputStreamReader(classloader.getResourceAsStream(xmlClassPath), Charsets.UTF_8);
079    try {
080      return parse(reader, messages);
081
082    } finally {
083      IOUtils.closeQuietly(reader);
084    }
085  }
086
087  public RulesProfile parse(Reader reader, ValidationMessages messages) {
088    RulesProfile profile = RulesProfile.create();
089    SMInputFactory inputFactory = initStax();
090    try {
091      SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader);
092      rootC.advance(); // <profile>
093      SMInputCursor cursor = rootC.childElementCursor();
094      while (cursor.getNext() != null) {
095        String nodeName = cursor.getLocalName();
096        if (StringUtils.equals("rules", nodeName)) {
097          SMInputCursor rulesCursor = cursor.childElementCursor("rule");
098          processRules(rulesCursor, profile, messages);
099
100        } else if (StringUtils.equals("alerts", nodeName)) {
101          SMInputCursor alertsCursor = cursor.childElementCursor("alert");
102          processAlerts(alertsCursor, profile, messages);
103
104        } else if (StringUtils.equals("name", nodeName)) {
105          profile.setName(StringUtils.trim(cursor.collectDescendantText(false)));
106
107        } else if (StringUtils.equals("language", nodeName)) {
108          profile.setLanguage(StringUtils.trim(cursor.collectDescendantText(false)));
109        }
110      }
111    } catch (XMLStreamException e) {
112      messages.addErrorText("XML is not valid: " + e.getMessage());
113    }
114    checkProfile(profile, messages);
115    return profile;
116  }
117
118  private void checkProfile(RulesProfile profile, ValidationMessages messages) {
119    if (StringUtils.isBlank(profile.getName())) {
120      messages.addErrorText("The mandatory node <name> is missing.");
121    }
122    if (StringUtils.isBlank(profile.getLanguage())) {
123      messages.addErrorText("The mandatory node <language> is missing.");
124    }
125  }
126
127  private SMInputFactory initStax() {
128    XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
129    xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
130    xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
131    // just so it won't try to load DTD in if there's DOCTYPE
132    xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
133    xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
134    return new SMInputFactory(xmlFactory);
135  }
136
137  private void processRules(SMInputCursor rulesCursor, RulesProfile profile, ValidationMessages messages) throws XMLStreamException {
138    Map<String, String> parameters = new HashMap<String, String>();
139    while (rulesCursor.getNext() != null) {
140      SMInputCursor ruleCursor = rulesCursor.childElementCursor();
141
142      String repositoryKey = null, key = null;
143      RulePriority priority = null;
144      parameters.clear();
145
146      while (ruleCursor.getNext() != null) {
147        String nodeName = ruleCursor.getLocalName();
148
149        if (StringUtils.equals("repositoryKey", nodeName)) {
150          repositoryKey = StringUtils.trim(ruleCursor.collectDescendantText(false));
151
152        } else if (StringUtils.equals("key", nodeName)) {
153          key = StringUtils.trim(ruleCursor.collectDescendantText(false));
154
155        } else if (StringUtils.equals("priority", nodeName)) {
156          priority = RulePriority.valueOf(StringUtils.trim(ruleCursor.collectDescendantText(false)));
157
158        } else if (StringUtils.equals("parameters", nodeName)) {
159          SMInputCursor propsCursor = ruleCursor.childElementCursor("parameter");
160          processParameters(propsCursor, parameters);
161        }
162      }
163
164      Rule rule = ruleFinder.findByKey(repositoryKey, key);
165      if (rule == null) {
166        messages.addWarningText("Rule not found: " + ruleToString(repositoryKey, key));
167
168      } else {
169        ActiveRule activeRule = profile.activateRule(rule, priority);
170        for (Map.Entry<String, String> entry : parameters.entrySet()) {
171          if (rule.getParam(entry.getKey()) == null) {
172            messages.addWarningText("The parameter '" + entry.getKey() + "' does not exist in the rule: " + ruleToString(repositoryKey, key));
173          } else {
174            activeRule.setParameter(entry.getKey(), entry.getValue());
175          }
176        }
177      }
178    }
179  }
180
181  private String ruleToString(String repositoryKey, String key) {
182    return "[repository=" + repositoryKey + ", key=" + key + "]";
183  }
184
185  private void processParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException {
186    while (propsCursor.getNext() != null) {
187      SMInputCursor propCursor = propsCursor.childElementCursor();
188      String key = null;
189      String value = null;
190      while (propCursor.getNext() != null) {
191        String nodeName = propCursor.getLocalName();
192        if (StringUtils.equals("key", nodeName)) {
193          key = StringUtils.trim(propCursor.collectDescendantText(false));
194
195        } else if (StringUtils.equals("value", nodeName)) {
196          value = StringUtils.trim(propCursor.collectDescendantText(false));
197        }
198      }
199      if (key != null) {
200        parameters.put(key, value);
201      }
202    }
203  }
204
205  private void processAlerts(SMInputCursor alertsCursor, RulesProfile profile, ValidationMessages messages) throws XMLStreamException {
206    if (metricFinder == null) {
207      // TODO remove when constructor without MetricFinder would be removed
208      Logs.INFO.error("Unable to parse alerts, because MetricFinder not available.");
209      return;
210    }
211    while (alertsCursor.getNext() != null) {
212      SMInputCursor alertCursor = alertsCursor.childElementCursor();
213
214      String metricKey = null, operator = "", valueError = "", valueWarning = "";
215      Integer period = null;
216
217      while (alertCursor.getNext() != null) {
218        String nodeName = alertCursor.getLocalName();
219
220        if (StringUtils.equals("metric", nodeName)) {
221          metricKey = StringUtils.trim(alertCursor.collectDescendantText(false));
222
223        } else if (StringUtils.equals("period", nodeName)) {
224          String periodParameter = StringUtils.trim(alertCursor.collectDescendantText(false));
225          if (StringUtils.isNotBlank(periodParameter)) {
226            period = Integer.parseInt(periodParameter);
227          }
228        } else if (StringUtils.equals("operator", nodeName)) {
229          operator = StringUtils.trim(alertCursor.collectDescendantText(false));
230
231        } else if (StringUtils.equals("warning", nodeName)) {
232          valueWarning = StringUtils.trim(alertCursor.collectDescendantText(false));
233
234        } else if (StringUtils.equals("error", nodeName)) {
235          valueError = StringUtils.trim(alertCursor.collectDescendantText(false));
236        }
237      }
238
239      Metric metric = metricFinder.findByKey(metricKey);
240      if (metric == null) {
241        messages.addWarningText("Metric '" + metricKey + "' does not exist");
242      } else {
243        Alert alert = new Alert(profile, metric, operator, valueError, valueWarning, period);
244        profile.getAlerts().add(alert);
245      }
246    }
247  }
248
249}