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