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.rules; 021 022import com.google.common.annotations.VisibleForTesting; 023import com.google.common.base.Strings; 024import com.google.common.collect.Lists; 025import com.google.common.collect.Maps; 026import com.google.common.io.Closeables; 027import org.apache.commons.io.FileUtils; 028import org.apache.commons.lang.CharEncoding; 029import org.apache.commons.lang.StringUtils; 030import org.codehaus.staxmate.SMInputFactory; 031import org.codehaus.staxmate.in.SMHierarchicCursor; 032import org.codehaus.staxmate.in.SMInputCursor; 033import org.sonar.api.PropertyType; 034import org.sonar.api.ServerComponent; 035import org.sonar.api.utils.SonarException; 036import org.sonar.check.Cardinality; 037 038import javax.xml.stream.XMLInputFactory; 039import javax.xml.stream.XMLStreamException; 040 041import java.io.File; 042import java.io.IOException; 043import java.io.InputStream; 044import java.io.InputStreamReader; 045import java.io.Reader; 046import java.util.ArrayList; 047import java.util.List; 048import java.util.Map; 049 050/** 051 * @since 2.3 052 */ 053public final class XMLRuleParser implements ServerComponent { 054 private static final Map<String, String> TYPE_MAP = typeMapWithDeprecatedValues(); 055 056 public List<Rule> parse(File file) { 057 Reader reader = null; 058 try { 059 reader = new InputStreamReader(FileUtils.openInputStream(file), CharEncoding.UTF_8); 060 return parse(reader); 061 062 } catch (IOException e) { 063 throw new SonarException("Fail to load the file: " + file, e); 064 065 } finally { 066 Closeables.closeQuietly(reader); 067 } 068 } 069 070 /** 071 * Warning : the input stream is closed in this method 072 */ 073 public List<Rule> parse(InputStream input) { 074 Reader reader = null; 075 try { 076 reader = new InputStreamReader(input, CharEncoding.UTF_8); 077 return parse(reader); 078 079 } catch (IOException e) { 080 throw new SonarException("Fail to load the xml stream", e); 081 082 } finally { 083 Closeables.closeQuietly(reader); 084 } 085 } 086 087 public List<Rule> parse(Reader reader) { 088 XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); 089 xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); 090 xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); 091 // just so it won't try to load DTD in if there's DOCTYPE 092 xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); 093 xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); 094 SMInputFactory inputFactory = new SMInputFactory(xmlFactory); 095 try { 096 SMHierarchicCursor rootC = inputFactory.rootElementCursor(reader); 097 rootC.advance(); // <rules> 098 List<Rule> rules = new ArrayList<Rule>(); 099 100 SMInputCursor rulesC = rootC.childElementCursor("rule"); 101 while (rulesC.getNext() != null) { 102 // <rule> 103 Rule rule = Rule.create(); 104 rules.add(rule); 105 106 processRule(rule, rulesC); 107 } 108 return rules; 109 110 } catch (XMLStreamException e) { 111 throw new SonarException("XML is not valid", e); 112 } 113 } 114 115 private static void processRule(Rule rule, SMInputCursor ruleC) throws XMLStreamException { 116 /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */ 117 String keyAttribute = ruleC.getAttrValue("key"); 118 if (StringUtils.isNotBlank(keyAttribute)) { 119 rule.setKey(StringUtils.trim(keyAttribute)); 120 } 121 122 /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */ 123 String priorityAttribute = ruleC.getAttrValue("priority"); 124 if (StringUtils.isNotBlank(priorityAttribute)) { 125 rule.setSeverity(RulePriority.valueOf(StringUtils.trim(priorityAttribute))); 126 } 127 128 List<String> tags = Lists.newArrayList(); 129 SMInputCursor cursor = ruleC.childElementCursor(); 130 131 while (cursor.getNext() != null) { 132 String nodeName = cursor.getLocalName(); 133 134 if (StringUtils.equalsIgnoreCase("name", nodeName)) { 135 rule.setName(StringUtils.trim(cursor.collectDescendantText(false))); 136 137 } else if (StringUtils.equalsIgnoreCase("description", nodeName)) { 138 rule.setDescription(StringUtils.trim(cursor.collectDescendantText(false))); 139 140 } else if (StringUtils.equalsIgnoreCase("key", nodeName)) { 141 rule.setKey(StringUtils.trim(cursor.collectDescendantText(false))); 142 143 } else if (StringUtils.equalsIgnoreCase("configKey", nodeName)) { 144 rule.setConfigKey(StringUtils.trim(cursor.collectDescendantText(false))); 145 146 } else if (StringUtils.equalsIgnoreCase("priority", nodeName)) { 147 rule.setSeverity(RulePriority.valueOf(StringUtils.trim(cursor.collectDescendantText(false)))); 148 149 } else if (StringUtils.equalsIgnoreCase("cardinality", nodeName)) { 150 rule.setCardinality(Cardinality.valueOf(StringUtils.trim(cursor.collectDescendantText(false)))); 151 152 } else if (StringUtils.equalsIgnoreCase("status", nodeName)) { 153 rule.setStatus(StringUtils.trim(cursor.collectDescendantText(false))); 154 155 } else if (StringUtils.equalsIgnoreCase("param", nodeName)) { 156 processParameter(rule, cursor); 157 158 } else if (StringUtils.equalsIgnoreCase("tag", nodeName)) { 159 tags.add(StringUtils.trim(cursor.collectDescendantText(false))); 160 } 161 } 162 if (Strings.isNullOrEmpty(rule.getKey())) { 163 throw new SonarException("Node <key> is missing in <rule>"); 164 } 165 rule.setTags(tags.toArray(new String[tags.size()])); 166 } 167 168 private static void processParameter(Rule rule, SMInputCursor ruleC) throws XMLStreamException { 169 RuleParam param = rule.createParameter(); 170 171 String keyAttribute = ruleC.getAttrValue("key"); 172 if (StringUtils.isNotBlank(keyAttribute)) { 173 /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */ 174 param.setKey(StringUtils.trim(keyAttribute)); 175 } 176 177 String typeAttribute = ruleC.getAttrValue("type"); 178 if (StringUtils.isNotBlank(typeAttribute)) { 179 /* BACKWARD COMPATIBILITY WITH DEPRECATED FORMAT */ 180 param.setType(type(StringUtils.trim(typeAttribute))); 181 } 182 183 SMInputCursor paramC = ruleC.childElementCursor(); 184 while (paramC.getNext() != null) { 185 String propNodeName = paramC.getLocalName(); 186 String propText = StringUtils.trim(paramC.collectDescendantText(false)); 187 if (StringUtils.equalsIgnoreCase("key", propNodeName)) { 188 param.setKey(propText); 189 190 } else if (StringUtils.equalsIgnoreCase("description", propNodeName)) { 191 param.setDescription(propText); 192 193 } else if (StringUtils.equalsIgnoreCase("type", propNodeName)) { 194 param.setType(type(propText)); 195 196 } else if (StringUtils.equalsIgnoreCase("defaultValue", propNodeName)) { 197 param.setDefaultValue(propText); 198 } 199 } 200 if (Strings.isNullOrEmpty(param.getKey())) { 201 throw new SonarException("Node <key> is missing in <param>"); 202 } 203 } 204 205 private static Map<String, String> typeMapWithDeprecatedValues() { 206 Map<String, String> map = Maps.newHashMap(); 207 map.put("i", PropertyType.INTEGER.name()); 208 map.put("s", PropertyType.STRING.name()); 209 map.put("b", PropertyType.BOOLEAN.name()); 210 map.put("r", PropertyType.REGULAR_EXPRESSION.name()); 211 map.put("s{}", "s{}"); 212 map.put("i{}", "i{}"); 213 for (PropertyType propertyType : PropertyType.values()) { 214 map.put(propertyType.name(), propertyType.name()); 215 } 216 return map; 217 } 218 219 @VisibleForTesting 220 static String type(String type) { 221 String validType = TYPE_MAP.get(type); 222 if (null != validType) { 223 return validType; 224 } 225 226 if (type.matches(".\\[.+\\]")) { 227 return type; 228 } 229 throw new SonarException("Invalid property type [" + type + "]"); 230 } 231 232}