View Javadoc

1   /***
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd;
5   
6   import net.sourceforge.pmd.util.ResourceLoader;
7   import org.w3c.dom.Document;
8   import org.w3c.dom.Element;
9   import org.w3c.dom.Node;
10  import org.w3c.dom.NodeList;
11  
12  import javax.xml.parsers.DocumentBuilder;
13  import javax.xml.parsers.DocumentBuilderFactory;
14  import javax.xml.parsers.ParserConfigurationException;
15  import java.io.IOException;
16  import java.io.InputStream;
17  import java.util.ArrayList;
18  import java.util.HashSet;
19  import java.util.Iterator;
20  import java.util.List;
21  import java.util.Properties;
22  import java.util.Set;
23  import java.util.StringTokenizer;
24  
25  // Note that ruleset parsing may fail on JDK 1.6 beta
26  // due to this bug - http://www.netbeans.org/issues/show_bug.cgi?id=63257
27  public class RuleSetFactory {
28  
29      private static class OverrideParser {
30          private Element ruleElement;
31          public OverrideParser(Element ruleElement) {
32              this.ruleElement = ruleElement;
33          }
34          public void overrideAsNecessary(Rule rule) {
35              if (ruleElement.hasAttribute("name")) {
36                  rule.setName(ruleElement.getAttribute("name"));
37              }
38              if (ruleElement.hasAttribute("message")) {
39                  rule.setMessage(ruleElement.getAttribute("message"));
40              }
41              if (ruleElement.hasAttribute("externalInfoUrl")) {
42                  rule.setExternalInfoUrl(ruleElement.getAttribute("externalInfoUrl"));
43              }
44              for (int i = 0; i < ruleElement.getChildNodes().getLength(); i++) {
45                  Node node = ruleElement.getChildNodes().item(i);
46                  if (node.getNodeType() == Node.ELEMENT_NODE) {
47                      if (node.getNodeName().equals("description")) {
48                          rule.setDescription(parseTextNode(node));
49                      } else if (node.getNodeName().equals("example")) {
50                          rule.setExample(parseTextNode(node));
51                      } else if (node.getNodeName().equals("priority")) {
52                          rule.setPriority(Integer.parseInt(parseTextNode(node)));
53                      } else if (node.getNodeName().equals("properties")) {
54                          Properties p = new Properties();
55                          parsePropertiesNode(p, node);
56                          rule.addProperties(p);
57                      }
58                  }
59              }
60          }
61      }
62  
63  
64      private ClassLoader classLoader;
65  
66      /***
67       * Returns an Iterator of RuleSet objects loaded from descriptions from
68       * the "rulesets.properties" resource.
69       *
70       * @return an iterator of RuleSet objects
71       */
72      public Iterator getRegisteredRuleSets() throws RuleSetNotFoundException {
73          try {
74              Properties props = new Properties();
75              props.load(ResourceLoader.loadResourceAsStream("rulesets/rulesets.properties"));
76              String rulesetFilenames = props.getProperty("rulesets.filenames");
77              List ruleSets = new ArrayList();
78              for (StringTokenizer st = new StringTokenizer(rulesetFilenames, ","); st.hasMoreTokens();) {
79                  ruleSets.add(createRuleSet(st.nextToken()));
80              }
81              return ruleSets.iterator();
82          } catch (IOException ioe) {
83              throw new RuntimeException("Couldn't find rulesets.properties; please ensure that the rulesets directory is on the classpath.  Here's the current classpath: " + System.getProperty("java.class.path"));
84          }
85      }
86  
87      /***
88       * Create a ruleset from a name or from a list of names
89       *
90       * @param name        name of rule set file loaded as a resource
91       * @param classLoader the classloader used to load the ruleset and subsequent rules
92       * @return the new ruleset
93       * @throws RuleSetNotFoundException
94       */
95      public RuleSet createRuleSet(String name, ClassLoader classLoader) throws RuleSetNotFoundException {
96          this.classLoader = classLoader;
97          if (name.indexOf(',') == -1) {
98              return createRuleSet(tryToGetStreamTo(name, classLoader));
99          }
100         RuleSet ruleSet = new RuleSet();
101         for (StringTokenizer st = new StringTokenizer(name, ","); st.hasMoreTokens();) {
102             ruleSet.addRuleSet(createRuleSet(st.nextToken().trim(), classLoader));
103         }
104         return ruleSet;
105     }
106 
107     /***
108      * Creates a ruleset.  If passed a comma-delimited string (rulesets/basic.xml,rulesets/unusedcode.xml)
109      * it will parse that string and create a new ruleset for each item in the list.
110      * Same as createRuleSet(name, ruleSetFactory.getClassLoader()).
111      */
112     public RuleSet createRuleSet(String name) throws RuleSetNotFoundException {
113         return createRuleSet(name, getClass().getClassLoader());
114     }
115 
116     /***
117      * Create a ruleset from an inputsteam.
118      * Same as createRuleSet(inputStream, ruleSetFactory.getClassLoader()).
119      *
120      * @param inputStream an input stream  that contains a ruleset descripion
121      * @return a new ruleset
122      */
123     public RuleSet createRuleSet(InputStream inputStream) {
124         return createRuleSet(inputStream, getClass().getClassLoader());
125     }
126 
127     /***
128      * Create a ruleset from an input stream with a specified class loader
129      *
130      * @param inputStream an input stream that contains a ruleset descripion
131      * @param classLoader a class loader used to load rule classes
132      * @return a new ruleset
133      */
134     public RuleSet createRuleSet(InputStream inputStream, ClassLoader classLoader) {
135         try {
136             this.classLoader = classLoader;
137             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
138             Document doc = builder.parse(inputStream);
139             Element root = doc.getDocumentElement();
140 
141             RuleSet ruleSet = new RuleSet();
142             ruleSet.setName(root.getAttribute("name"));
143 
144             NodeList nodeList = root.getChildNodes();
145             for (int i = 0; i < nodeList.getLength(); i++) {
146                 Node node = nodeList.item(i);
147                 if (node.getNodeType() == Node.ELEMENT_NODE) {
148                     if (node.getNodeName().equals("description")) {
149                         ruleSet.setDescription(parseTextNode(node));
150                     } else if (node.getNodeName().equals("rule")) {
151                         parseRuleNode(ruleSet, node);
152                     }
153                 }
154             }
155 
156             return ruleSet;
157         } catch (IllegalArgumentException illae) {
158             throw illae;    // this is hideous, but see not in catch Exception block...
159         } catch (ClassNotFoundException cnfe) {
160             cnfe.printStackTrace();
161             throw new RuntimeException("Couldn't find that class " + cnfe.getMessage());
162         } catch (InstantiationException ie) {
163             ie.printStackTrace();
164             throw new RuntimeException("Couldn't find that class " + ie.getMessage());
165         } catch (IllegalAccessException iae) {
166             iae.printStackTrace();
167             throw new RuntimeException("Couldn't find that class " + iae.getMessage());
168         } catch (ParserConfigurationException pce) {
169             pce.printStackTrace();
170             throw new RuntimeException("Couldn't find that class " + pce.getMessage());
171         } catch (Exception e) {
172             /*
173             I hate to catch Exception, but I need to catch SaxException, and
174             I can't figure out where it's declared.  It's not in our pmd/lib jar files;
175             it must be buried in the JDK somewhere.  If anyone knows how to fix this, pls
176             let me know.... thanks!
177             */
178             e.printStackTrace();
179             throw new RuntimeException("Couldn't find that class " + e.getMessage());
180         }
181     }
182 
183     /***
184      * Try to load a resource with the specified class loader
185      *
186      * @param name   a resource name (contains a ruleset description)
187      * @param loader a class loader used to load that rule set description
188      * @return an inputstream to that resource
189      * @throws RuleSetNotFoundException
190      */
191     private InputStream tryToGetStreamTo(String name, ClassLoader loader) throws RuleSetNotFoundException {
192         InputStream in = ResourceLoader.loadResourceAsStream(name, loader);
193         if (in == null) {
194             throw new RuleSetNotFoundException("Can't find resource " + name + ".  Make sure the resource is a valid file or URL or is on the CLASSPATH.  Here's the current classpath: " + System.getProperty("java.class.path"));
195         }
196         return in;
197     }
198 
199     /***
200      * Parse a rule node
201      *
202      * @param ruleSet     the ruleset being constructed
203      * @param ruleNode must be a rule element node
204      */
205     private void parseRuleNode(RuleSet ruleSet, Node ruleNode) throws ClassNotFoundException, InstantiationException, IllegalAccessException, RuleSetNotFoundException {
206         Element ruleElement = (Element) ruleNode;
207         String ref = ruleElement.getAttribute("ref");
208         if (ref.trim().length() == 0) {
209             parseInternallyDefinedRuleNode(ruleSet, ruleNode);
210         } else {
211             parseExternallyDefinedRuleNode(ruleSet, ruleNode);
212         }
213     }
214 
215     /***
216      * Process a rule definition node
217      *
218      * @param ruleSet  the ruleset being constructed
219      * @param ruleNode must be a rule element node
220      */
221     private void parseInternallyDefinedRuleNode(RuleSet ruleSet, Node ruleNode) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
222         Element ruleElement = (Element) ruleNode;
223 
224         String attribute = ruleElement.getAttribute("class");
225         Rule rule = (Rule)classLoader.loadClass(attribute).newInstance();
226 
227         rule.setName(ruleElement.getAttribute("name"));
228         rule.setMessage(ruleElement.getAttribute("message"));
229         rule.setRuleSetName(ruleSet.getName());
230         rule.setExternalInfoUrl(ruleElement.getAttribute("externalInfoUrl"));
231 
232         if (ruleElement.hasAttribute("dfa") && ruleElement.getAttribute("dfa").equals("true")) {
233             rule.setUsesDFA();
234         }
235 
236         for (int i = 0; i < ruleElement.getChildNodes().getLength(); i++) {
237             Node node = ruleElement.getChildNodes().item(i);
238             if (node.getNodeType() == Node.ELEMENT_NODE) {
239                 if (node.getNodeName().equals("description")) {
240                     rule.setDescription(parseTextNode(node));
241                 } else if (node.getNodeName().equals("example")) {
242                     rule.setExample(parseTextNode(node));
243                 } else if (node.getNodeName().equals("priority")) {
244                     rule.setPriority(new Integer(parseTextNode(node).trim()).intValue());
245                 } else if (node.getNodeName().equals("properties")) {
246                     Properties p = new Properties();
247                     parsePropertiesNode(p, node);
248                     for (Iterator j = p.keySet().iterator(); j.hasNext();) {
249                         String key = (String)j.next();
250                         rule.addProperty(key, p.getProperty(key));
251                     }
252                 }
253             }
254         }
255         ruleSet.addRule(rule);
256     }
257 
258     /***
259      * Process a reference to a rule
260      *
261      * @param ruleSet  the ruleset being constructucted
262      * @param ruleNode must be a rule element node
263      */
264     private void parseExternallyDefinedRuleNode(RuleSet ruleSet, Node ruleNode) throws RuleSetNotFoundException, ClassNotFoundException, InstantiationException, IllegalAccessException {
265         Element ruleElement = (Element) ruleNode;
266         String ref = ruleElement.getAttribute("ref");
267         if (ref.endsWith("xml")) {
268             parseRuleNodeWithExclude(ruleSet, ruleElement, ref);
269         } else {
270             parseRuleNodeWithSimpleReference(ruleSet, ruleNode, ref);
271         }
272     }
273 
274     /***
275      * Parse a rule node with a simple reference
276      *
277      * @param ruleSet the ruleset being constructed
278      * @param ref     a reference to a rule
279      */
280     private void parseRuleNodeWithSimpleReference(RuleSet ruleSet, Node ruleNode, String ref) throws RuleSetNotFoundException {
281         RuleSetFactory rsf = new RuleSetFactory();
282 
283         ExternalRuleID externalRuleID = new ExternalRuleID(ref);
284         RuleSet externalRuleSet = rsf.createRuleSet(ResourceLoader.loadResourceAsStream(externalRuleID.getFilename()));
285         Rule externalRule = externalRuleSet.getRuleByName(externalRuleID.getRuleName());
286         if (externalRule == null) {
287             throw new IllegalArgumentException("Unable to find rule " + externalRuleID.getRuleName() + "; perhaps the rule name is mispelled?");
288         }
289 
290         OverrideParser p = new OverrideParser((Element)ruleNode);
291         p.overrideAsNecessary(externalRule);
292 
293         ruleSet.addRule(externalRule);
294     }
295 
296     /***
297      * Parse a reference rule node with excludes
298      *
299      * @param ruleSet     the ruleset being constructed
300      * @param ruleElement must be a rule element
301      * @param ref         the ruleset reference
302      */
303     private void parseRuleNodeWithExclude(RuleSet ruleSet, Element ruleElement, String ref) throws RuleSetNotFoundException {
304         NodeList excludeNodes = ruleElement.getChildNodes();
305         Set excludes = new HashSet();
306         for (int i = 0; i < excludeNodes.getLength(); i++) {
307             if ((excludeNodes.item(i).getNodeType() == Node.ELEMENT_NODE) && (excludeNodes.item(i).getNodeName().equals("exclude"))) {
308                 Element excludeElement = (Element) excludeNodes.item(i);
309                 excludes.add(excludeElement.getAttribute("name"));
310             }
311         }
312 
313         RuleSetFactory rsf = new RuleSetFactory();
314         RuleSet externalRuleSet = rsf.createRuleSet(ResourceLoader.loadResourceAsStream(ref));
315         for (Iterator i = externalRuleSet.getRules().iterator(); i.hasNext();) {
316             Rule rule = (Rule) i.next();
317             if (!excludes.contains(rule.getName())) {
318                 ruleSet.addRule(rule);
319             }
320         }
321     }
322 
323     private static String parseTextNode(Node exampleNode) {
324         StringBuffer buffer = new StringBuffer();
325         for (int i = 0; i < exampleNode.getChildNodes().getLength(); i++) {
326             Node node = exampleNode.getChildNodes().item(i);
327             if (node.getNodeType() == Node.CDATA_SECTION_NODE || node.getNodeType() == Node.TEXT_NODE) {
328                 buffer.append(node.getNodeValue());
329             }
330         }
331         return buffer.toString();
332     }
333     /***
334      * Parse a properties node
335      *
336      * @param propertiesNode must be a properties element node
337      */
338     private static void parsePropertiesNode(Properties p, Node propertiesNode) {
339         for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) {
340             Node node = propertiesNode.getChildNodes().item(i);
341             if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("property")) {
342                 parsePropertyNode(p, node);
343             }
344         }
345     }
346 
347     /***
348      * Parse a property node
349      *
350      * @param propertyNode must be a property element node
351      */
352     private static void parsePropertyNode(Properties p, Node propertyNode) {
353         Element propertyElement = (Element) propertyNode;
354         String name = propertyElement.getAttribute("name");
355         String value = propertyElement.getAttribute("value");
356         // TODO String desc = propertyElement.getAttribute("description");
357         if (value.trim().length() == 0) {
358             for (int i = 0; i < propertyNode.getChildNodes().getLength(); i++) {
359                 Node node = propertyNode.getChildNodes().item(i);
360                 if ((node.getNodeType() == Node.ELEMENT_NODE) && node.getNodeName().equals("value")) {
361                     value = parseTextNode(node);
362                 }
363             }
364         }
365         if (propertyElement.hasAttribute("pluginname")) {
366             p.setProperty("pluginname", propertyElement.getAttributeNode("pluginname").getNodeValue());
367         }
368         p.setProperty(name, value);
369     }
370 }