1 /**
2 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3 */
4 package net.sourceforge.pmd;
5
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.util.Iterator;
9 import java.util.Map;
10 import java.util.Properties;
11 import java.util.StringTokenizer;
12
13 import javax.xml.parsers.DocumentBuilder;
14 import javax.xml.parsers.DocumentBuilderFactory;
15 import javax.xml.parsers.ParserConfigurationException;
16
17 import net.sourceforge.pmd.util.ResourceLoader;
18
19 import org.w3c.dom.Document;
20 import org.w3c.dom.Element;
21 import org.w3c.dom.Node;
22 import org.w3c.dom.NodeList;
23 import org.xml.sax.SAXException;
24
25 /**
26 * RuleSetFactory is responsible for creating RuleSet instances from XML content.
27 */
28 public class RuleSetFactory {
29
30 private int minPriority = Rule.LOWEST_PRIORITY;
31
32 /**
33 * Set the minimum rule priority threshold for all Rules which are loaded
34 * from RuleSets via reference.
35 *
36 * @param minPriority The minimum priority.
37 */
38 public void setMinimumPriority(int minPriority) {
39 this.minPriority = minPriority;
40 }
41
42 /**
43 * Returns an Iterator of RuleSet objects loaded from descriptions from the
44 * "rulesets.properties" resource.
45 *
46 * @return An Iterator of RuleSet objects.
47 */
48 public Iterator<RuleSet> getRegisteredRuleSets() throws RuleSetNotFoundException {
49 try {
50 Properties props = new Properties();
51 props.load(ResourceLoader.loadResourceAsStream("rulesets/rulesets.properties"));
52 String rulesetFilenames = props.getProperty("rulesets.filenames");
53 return createRuleSets(rulesetFilenames).getRuleSetsIterator();
54 } catch (IOException ioe) {
55 throw new RuntimeException(
56 "Couldn't find rulesets.properties; please ensure that the rulesets directory is on the classpath. Here's the current classpath: "
57 + System.getProperty("java.class.path"));
58 }
59 }
60
61 /**
62 * Create a RuleSets from a list of names.
63 * The ClassLoader of the RuleSetFactory class is used.
64 *
65 * @param ruleSetFileNames A comma-separated list of rule set files.
66 * @return The new RuleSets.
67 * @throws RuleSetNotFoundException if unable to find a resource.
68 */
69 public RuleSets createRuleSets(String ruleSetFileNames) throws RuleSetNotFoundException {
70 return createRuleSets(ruleSetFileNames, getClass().getClassLoader());
71 }
72
73 /**
74 * Create a RuleSets from a list of names with a specified ClassLoader.
75 *
76 * @param ruleSetFileNames A comma-separated list of rule set files.
77 * @param classLoader The ClassLoader to load Classes and resources.
78 * @return The new RuleSets.
79 * @throws RuleSetNotFoundException if unable to find a resource.
80 */
81 public RuleSets createRuleSets(String ruleSetFileNames, ClassLoader classLoader) throws RuleSetNotFoundException {
82 RuleSets ruleSets = new RuleSets();
83
84 for (StringTokenizer st = new StringTokenizer(ruleSetFileNames, ","); st.hasMoreTokens();) {
85 RuleSet ruleSet = createSingleRuleSet(st.nextToken().trim(), classLoader);
86 ruleSets.addRuleSet(ruleSet);
87 }
88
89 return ruleSets;
90 }
91
92 /**
93 * Create a ruleset from a name or from a list of names
94 *
95 * @param name name of rule set file loaded as a resource
96 * @param classLoader the classloader used to load the ruleset and subsequent rules
97 * @return the new ruleset
98 * @throws RuleSetNotFoundException
99 * @deprecated Use createRuleSets instead, because this method puts all rules in one
100 * single RuleSet object, and thus removes name and language of the
101 * originating rule set files.
102 */
103 public RuleSet createRuleSet(String name, ClassLoader classLoader) throws RuleSetNotFoundException {
104 RuleSets ruleSets = createRuleSets(name, classLoader);
105 RuleSet result = new RuleSet();
106 RuleSet[] allRuleSets = ruleSets.getAllRuleSets();
107 for (RuleSet ruleSet : allRuleSets) {
108 result.addRuleSet(ruleSet);
109 }
110 return result;
111 }
112
113 /**
114 * Create a RuleSet from a file name resource.
115 * The ClassLoader of the RuleSetFactory class is used.
116 *
117 * @param ruleSetFileName The name of rule set file loaded as a resource.
118 * @return A new RuleSet.
119 * @throws RuleSetNotFoundException if unable to find a resource.
120 */
121 public RuleSet createSingleRuleSet(String ruleSetFileName) throws RuleSetNotFoundException {
122 return createSingleRuleSet(ruleSetFileName, getClass().getClassLoader());
123 }
124
125 /**
126 * Create a RuleSet from a file name resource with a specified ClassLoader.
127 *
128 * @param ruleSetFileName The name of rule set file loaded as a resource.
129 * @param classLoader The ClassLoader to load Classes and resources.
130 * @return A new RuleSet.
131 * @throws RuleSetNotFoundException if unable to find a resource.
132 */
133 private RuleSet createSingleRuleSet(String ruleSetFileName, ClassLoader classLoader)
134 throws RuleSetNotFoundException {
135 return parseRuleSetNode(ruleSetFileName, tryToGetStreamTo(ruleSetFileName, classLoader), classLoader);
136 }
137
138 /**
139 * Create a RuleSet from an InputStream.
140 * The ClassLoader of the RuleSetFactory class is used.
141 *
142 * @param inputStream InputStream containing the RuleSet XML configuration.
143 * @return A new RuleSet.
144 */
145 public RuleSet createRuleSet(InputStream inputStream) {
146 return createRuleSet(inputStream, getClass().getClassLoader());
147 }
148
149 /**
150 * Create a RuleSet from an InputStream with a specified ClassLoader.
151 *
152 * @param inputStream InputStream containing the RuleSet XML configuration.
153 * @param classLoader The ClassLoader to load Classes and resources.
154 * @return A new RuleSet.
155 */
156 public RuleSet createRuleSet(InputStream inputStream, ClassLoader classLoader) {
157 return parseRuleSetNode(null, inputStream, classLoader);
158 }
159
160 /**
161 * Try to load a resource with the specified class loader
162 *
163 * @param name A resource name (e.g. a RuleSet description).
164 * @param classLoader The ClassLoader to load Classes and resources.
165 * @return An InputStream to that resource.
166 * @throws RuleSetNotFoundException if unable to find a resource.
167 */
168 private InputStream tryToGetStreamTo(String name, ClassLoader classLoader) throws RuleSetNotFoundException {
169 InputStream in = ResourceLoader.loadResourceAsStream(name, classLoader);
170 if (in == null) {
171 throw new RuleSetNotFoundException(
172 "Can't find resource "
173 + name
174 + ". Make sure the resource is a valid file or URL or is on the CLASSPATH. Here's the current classpath: "
175 + System.getProperty("java.class.path"));
176 }
177 return in;
178 }
179
180 /**
181 * Parse a ruleset node to construct a RuleSet.
182 *
183 * @param inputStream InputStream containing the RuleSet XML configuration.
184 * @param classLoader The ClassLoader to load Classes and resources.
185 * @return The new RuleSet.
186 */
187 private RuleSet parseRuleSetNode(String fileName, InputStream inputStream, ClassLoader classLoader) {
188 try {
189 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
190 Document document = builder.parse(inputStream);
191 Element ruleSetElement = document.getDocumentElement();
192
193 RuleSet ruleSet = new RuleSet();
194 ruleSet.setFileName(fileName);
195 ruleSet.setName(ruleSetElement.getAttribute("name"));
196 ruleSet.setLanguage(Language.getByName(ruleSetElement.getAttribute("language")));
197
198 NodeList nodeList = ruleSetElement.getChildNodes();
199 for (int i = 0; i < nodeList.getLength(); i++) {
200 Node node = nodeList.item(i);
201 if (node.getNodeType() == Node.ELEMENT_NODE) {
202 if (node.getNodeName().equals("description")) {
203 ruleSet.setDescription(parseTextNode(node));
204 } else if (node.getNodeName().equals("include-pattern")) {
205 ruleSet.addIncludePattern(parseTextNode(node));
206 } else if (node.getNodeName().equals("exclude-pattern")) {
207 ruleSet.addExcludePattern(parseTextNode(node));
208 } else if (node.getNodeName().equals("rule")) {
209 parseRuleNode(ruleSet, node, classLoader);
210 }
211 }
212 }
213
214 return ruleSet;
215 } catch (ClassNotFoundException cnfe) {
216 cnfe.printStackTrace();
217 throw new RuntimeException("Couldn't find that class " + cnfe.getMessage());
218 } catch (InstantiationException ie) {
219 ie.printStackTrace();
220 throw new RuntimeException("Couldn't find that class " + ie.getMessage());
221 } catch (IllegalAccessException iae) {
222 iae.printStackTrace();
223 throw new RuntimeException("Couldn't find that class " + iae.getMessage());
224 } catch (ParserConfigurationException pce) {
225 pce.printStackTrace();
226 throw new RuntimeException("Couldn't find that class " + pce.getMessage());
227 } catch (RuleSetNotFoundException rsnfe) {
228 rsnfe.printStackTrace();
229 throw new RuntimeException("Couldn't find that class " + rsnfe.getMessage());
230 } catch (IOException ioe) {
231 ioe.printStackTrace();
232 throw new RuntimeException("Couldn't find that class " + ioe.getMessage());
233 } catch (SAXException se) {
234 se.printStackTrace();
235 throw new RuntimeException("Couldn't find that class " + se.getMessage());
236 }
237 }
238
239 /**
240 * Parse a rule node.
241 *
242 * @param ruleSet The RuleSet being constructed.
243 * @param ruleNode Must be a rule element node.
244 * @param classLoader The ClassLoader to load Classes and resources.
245 */
246 private void parseRuleNode(RuleSet ruleSet, Node ruleNode, ClassLoader classLoader) throws ClassNotFoundException,
247 InstantiationException, IllegalAccessException, RuleSetNotFoundException {
248 Element ruleElement = (Element)ruleNode;
249 String ref = ruleElement.getAttribute("ref");
250 if (ref.endsWith("xml")) {
251 parseRuleSetReferenceNode(ruleSet, ruleElement, ref);
252 } else if (ref.trim().length() == 0) {
253 parseSingleRuleNode(ruleSet, ruleNode, classLoader);
254 } else {
255 parseRuleReferenceNode(ruleSet, ruleNode, ref);
256 }
257 }
258
259 /**
260 * Parse a rule node as an RuleSetReference for all Rules. Every Rule from
261 * the referred to RuleSet will be added as a RuleReference except for those
262 * explicitly excluded.
263 *
264 * @param ruleSet The RuleSet being constructed.
265 * @param ruleElement Must be a rule element node.
266 * @param ref The RuleSet reference.
267 */
268 private void parseRuleSetReferenceNode(RuleSet ruleSet, Element ruleElement, String ref)
269 throws RuleSetNotFoundException {
270
271 RuleSetReference ruleSetReference = new RuleSetReference();
272 ruleSetReference.setAllRules(true);
273 ruleSetReference.setRuleSetFileName(ref);
274 NodeList excludeNodes = ruleElement.getChildNodes();
275 for (int i = 0; i < excludeNodes.getLength(); i++) {
276 if ((excludeNodes.item(i).getNodeType() == Node.ELEMENT_NODE)
277 && (excludeNodes.item(i).getNodeName().equals("exclude"))) {
278 Element excludeElement = (Element)excludeNodes.item(i);
279 ruleSetReference.addExclude(excludeElement.getAttribute("name"));
280 }
281 }
282
283 RuleSetFactory ruleSetFactory = new RuleSetFactory();
284 RuleSet otherRuleSet = ruleSetFactory.createRuleSet(ResourceLoader.loadResourceAsStream(ref));
285 for (Rule rule : otherRuleSet.getRules()) {
286 if (!ruleSetReference.getExcludes().contains(rule.getName()) && rule.getPriority() <= minPriority) {
287 RuleReference ruleReference = new RuleReference();
288 ruleReference.setRuleSetReference(ruleSetReference);
289 ruleReference.setRule(rule);
290 ruleSet.addRule(ruleReference);
291 }
292 }
293 }
294
295 /**
296 * Parse a rule node as a single Rule. The Rule has been fully defined within
297 * the context of the current RuleSet.
298 *
299 * @param ruleSet The RuleSet being constructed.
300 * @param ruleNode Must be a rule element node.
301 * @param classLoader The ClassLoader to load Classes and resources.
302 */
303 private void parseSingleRuleNode(RuleSet ruleSet, Node ruleNode, ClassLoader classLoader)
304 throws ClassNotFoundException, InstantiationException, IllegalAccessException {
305 Element ruleElement = (Element)ruleNode;
306
307 String attribute = ruleElement.getAttribute("class");
308 Class<?> c = classLoader.loadClass(attribute);
309 Rule rule = (Rule)c.newInstance();
310
311 rule.setName(ruleElement.getAttribute("name"));
312 String since = ruleElement.getAttribute("since");
313 if (since.length() > 0) {
314 rule.setSince(since);
315 }
316 rule.setMessage(ruleElement.getAttribute("message"));
317 rule.setRuleSetName(ruleSet.getName());
318 rule.setExternalInfoUrl(ruleElement.getAttribute("externalInfoUrl"));
319
320 if (ruleElement.hasAttribute("dfa") && ruleElement.getAttribute("dfa").equals("true")) {
321 rule.setUsesDFA();
322 }
323
324 if (ruleElement.hasAttribute("typeResolution") && ruleElement.getAttribute("typeResolution").equals("true")) {
325 rule.setUsesTypeResolution();
326 }
327
328 for (int i = 0; i < ruleElement.getChildNodes().getLength(); i++) {
329 Node node = ruleElement.getChildNodes().item(i);
330 if (node.getNodeType() == Node.ELEMENT_NODE) {
331 if (node.getNodeName().equals("description")) {
332 rule.setDescription(parseTextNode(node));
333 } else if (node.getNodeName().equals("example")) {
334 rule.addExample(parseTextNode(node));
335 } else if (node.getNodeName().equals("priority")) {
336 rule.setPriority(Integer.parseInt(parseTextNode(node).trim()));
337 } else if (node.getNodeName().equals("properties")) {
338 Properties p = new Properties();
339 parsePropertiesNode(p, node);
340 for (Map.Entry<Object, Object> entry : p.entrySet()) {
341 rule.addProperty((String)entry.getKey(), (String)entry.getValue());
342 }
343 }
344 }
345 }
346 if (rule.getPriority() <= minPriority) {
347 ruleSet.addRule(rule);
348 }
349 }
350
351 /**
352 * Parse a rule node as a RuleReference. A RuleReference is a single Rule
353 * which comes from another RuleSet with some of it's attributes potentially
354 * overridden.
355 *
356 * @param ruleSet The RuleSet being constructed.
357 * @param ruleNode Must be a rule element node.
358 * @param classLoader The ClassLoader to load Classes and resources.
359 * @param ref A reference to a Rule.
360 */
361 private void parseRuleReferenceNode(RuleSet ruleSet, Node ruleNode, String ref) throws RuleSetNotFoundException {
362 RuleSetFactory ruleSetFactory = new RuleSetFactory();
363
364 ExternalRuleID externalRuleID = new ExternalRuleID(ref);
365 RuleSet externalRuleSet = ruleSetFactory.createRuleSet(ResourceLoader.loadResourceAsStream(externalRuleID.getFilename()));
366 Rule externalRule = externalRuleSet.getRuleByName(externalRuleID.getRuleName());
367 if (externalRule == null) {
368 throw new IllegalArgumentException("Unable to find rule " + externalRuleID.getRuleName()
369 + "; perhaps the rule name is mispelled?");
370 }
371
372 RuleSetReference ruleSetReference = new RuleSetReference();
373 ruleSetReference.setAllRules(false);
374 ruleSetReference.setRuleSetFileName(externalRuleID.getFilename());
375
376 RuleReference ruleReference = new RuleReference();
377 ruleReference.setRuleSetReference(ruleSetReference);
378 ruleReference.setRule(externalRule);
379
380 Element ruleElement = (Element)ruleNode;
381 if (ruleElement.hasAttribute("name")) {
382 ruleReference.setName(ruleElement.getAttribute("name"));
383 }
384 if (ruleElement.hasAttribute("message")) {
385 ruleReference.setMessage(ruleElement.getAttribute("message"));
386 }
387 if (ruleElement.hasAttribute("externalInfoUrl")) {
388 ruleReference.setExternalInfoUrl(ruleElement.getAttribute("externalInfoUrl"));
389 }
390 for (int i = 0; i < ruleElement.getChildNodes().getLength(); i++) {
391 Node node = ruleElement.getChildNodes().item(i);
392 if (node.getNodeType() == Node.ELEMENT_NODE) {
393 if (node.getNodeName().equals("description")) {
394 ruleReference.setDescription(parseTextNode(node));
395 } else if (node.getNodeName().equals("example")) {
396 ruleReference.addExample(parseTextNode(node));
397 } else if (node.getNodeName().equals("priority")) {
398 ruleReference.setPriority(Integer.parseInt(parseTextNode(node)));
399 } else if (node.getNodeName().equals("properties")) {
400 Properties p = new Properties();
401 parsePropertiesNode(p, node);
402 ruleReference.addProperties(p);
403 }
404 }
405 }
406
407 if (externalRule.getPriority() <= minPriority) {
408 ruleSet.addRule(ruleReference);
409 }
410 }
411
412 /**
413 * Parse a properties node.
414 *
415 * @param p The Properties to which the properties should be added.
416 * @param propertiesNode Must be a properties element node.
417 */
418 private static void parsePropertiesNode(Properties p, Node propertiesNode) {
419 for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) {
420 Node node = propertiesNode.getChildNodes().item(i);
421 if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("property")) {
422 parsePropertyNode(p, node);
423 }
424 }
425 }
426
427 /**
428 * Parse a property node.
429 *
430 * @param p The Properties to which the property should be added.
431 * @param propertyNode Must be a property element node.
432 */
433 private static void parsePropertyNode(Properties p, Node propertyNode) {
434 Element propertyElement = (Element)propertyNode;
435 String name = propertyElement.getAttribute("name");
436 String value = propertyElement.getAttribute("value");
437
438 if (value.trim().length() == 0) {
439 for (int i = 0; i < propertyNode.getChildNodes().getLength(); i++) {
440 Node node = propertyNode.getChildNodes().item(i);
441 if ((node.getNodeType() == Node.ELEMENT_NODE) && node.getNodeName().equals("value")) {
442 value = parseTextNode(node);
443 }
444 }
445 }
446 if (propertyElement.hasAttribute("pluginname")) {
447 p.setProperty("pluginname", propertyElement.getAttributeNode("pluginname").getNodeValue());
448 }
449 p.setProperty(name, value);
450 }
451
452 /**
453 * Parse a String from a textually type node.
454 *
455 * @param node The node.
456 * @return The String.
457 */
458 private static String parseTextNode(Node node) {
459 StringBuffer buffer = new StringBuffer();
460 for (int i = 0; i < node.getChildNodes().getLength(); i++) {
461 Node childNode = node.getChildNodes().item(i);
462 if (childNode.getNodeType() == Node.CDATA_SECTION_NODE || childNode.getNodeType() == Node.TEXT_NODE) {
463 buffer.append(childNode.getNodeValue());
464 }
465 }
466 return buffer.toString();
467 }
468 }