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 */
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: [repository=" + repositoryKey + ", key=" + 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: [repository=" + repositoryKey
173              + ", key=" + key + "]");
174          } else {
175            activeRule.setParameter(entry.getKey(), entry.getValue());
176          }
177        }
178      }
179    }
180  }
181
182  private void processParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException {
183    while (propsCursor.getNext() != null) {
184      SMInputCursor propCursor = propsCursor.childElementCursor();
185      String key = null;
186      String value = null;
187      while (propCursor.getNext() != null) {
188        String nodeName = propCursor.getLocalName();
189        if (StringUtils.equals("key", nodeName)) {
190          key = StringUtils.trim(propCursor.collectDescendantText(false));
191
192        } else if (StringUtils.equals("value", nodeName)) {
193          value = StringUtils.trim(propCursor.collectDescendantText(false));
194        }
195      }
196      if (key != null) {
197        parameters.put(key, value);
198      }
199    }
200  }
201
202  private void processAlerts(SMInputCursor alertsCursor, RulesProfile profile, ValidationMessages messages) throws XMLStreamException {
203    if (metricFinder == null) {
204      // TODO remove when constructor without MetricFinder would be removed
205      Logs.INFO.error("Unable to parse alerts, because MetricFinder not available.");
206      return;
207    }
208    while (alertsCursor.getNext() != null) {
209      SMInputCursor alertCursor = alertsCursor.childElementCursor();
210
211      String metricKey = null, operator = "", valueError = "", valueWarning = "";
212      Integer period = null;
213
214      while (alertCursor.getNext() != null) {
215        String nodeName = alertCursor.getLocalName();
216
217        if (StringUtils.equals("metric", nodeName)) {
218          metricKey = StringUtils.trim(alertCursor.collectDescendantText(false));
219
220        } else if (StringUtils.equals("period", nodeName)) {
221          String periodParameter = StringUtils.trim(alertCursor.collectDescendantText(false));
222          if (StringUtils.isNotBlank(periodParameter)) {
223            period = Integer.parseInt(periodParameter);
224          }
225        }else if (StringUtils.equals("operator", nodeName)) {
226          operator = StringUtils.trim(alertCursor.collectDescendantText(false));
227
228        } else if (StringUtils.equals("warning", nodeName)) {
229          valueWarning = StringUtils.trim(alertCursor.collectDescendantText(false));
230
231        } else if (StringUtils.equals("error", nodeName)) {
232          valueError = StringUtils.trim(alertCursor.collectDescendantText(false));
233        }
234      }
235
236      Metric metric = metricFinder.findByKey(metricKey);
237      if (metric == null) {
238        messages.addWarningText("Metric '" + metricKey + "' does not exist");
239      } else {
240        Alert alert = new Alert(profile, metric, operator, valueError, valueWarning, period);
241        profile.getAlerts().add(alert);
242      }
243    }
244  }
245
246}