1
2
3
4 package net.sourceforge.pmd;
5 import static org.junit.Assert.assertEquals;
6 import static org.junit.Assert.assertFalse;
7 import static org.junit.Assert.assertTrue;
8 import static org.junit.Assert.fail;
9
10 import java.io.BufferedReader;
11 import java.io.ByteArrayInputStream;
12 import java.io.ByteArrayOutputStream;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.io.InputStreamReader;
16 import java.io.UnsupportedEncodingException;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Properties;
20 import java.util.StringTokenizer;
21
22 import javax.xml.parsers.ParserConfigurationException;
23 import javax.xml.parsers.SAXParser;
24 import javax.xml.parsers.SAXParserFactory;
25
26 import net.sourceforge.pmd.lang.Language;
27 import net.sourceforge.pmd.lang.LanguageRegistry;
28 import net.sourceforge.pmd.lang.rule.RuleReference;
29 import net.sourceforge.pmd.lang.rule.XPathRule;
30 import net.sourceforge.pmd.util.ResourceLoader;
31
32 import org.junit.BeforeClass;
33 import org.junit.Test;
34 import org.xml.sax.InputSource;
35 import org.xml.sax.SAXException;
36 import org.xml.sax.SAXParseException;
37 import org.xml.sax.helpers.DefaultHandler;
38
39
40
41
42
43 public abstract class AbstractRuleSetFactoryTest {
44 private static SAXParserFactory saxParserFactory;
45 private static ValidateDefaultHandler validateDefaultHandlerXsd;
46 private static ValidateDefaultHandler validateDefaultHandlerDtd;
47 private static SAXParser saxParser;
48
49
50
51
52
53 @BeforeClass
54 public static void init() throws Exception {
55 saxParserFactory = SAXParserFactory.newInstance();
56 saxParserFactory.setValidating(true);
57 saxParserFactory.setNamespaceAware(true);
58
59
60
61
62 saxParserFactory.setFeature("http://xml.org/sax/features/validation",
63 true);
64 saxParserFactory.setFeature(
65 "http://apache.org/xml/features/validation/schema", true);
66 saxParserFactory
67 .setFeature(
68 "http://apache.org/xml/features/validation/schema-full-checking",
69 true);
70
71 validateDefaultHandlerXsd = new ValidateDefaultHandler("ruleset_2_0_0.xsd");
72 validateDefaultHandlerDtd = new ValidateDefaultHandler("ruleset_2_0_0.dtd");
73
74 saxParser = saxParserFactory.newSAXParser();
75 }
76
77
78
79
80
81
82 @Test
83 public void testAllPMDBuiltInRulesMeetConventions() throws Exception {
84 int invalidSinceAttributes = 0;
85 int invalidExternalInfoURL = 0;
86 int invalidClassName = 0;
87 int invalidRegexSuppress = 0;
88 int invalidXPathSuppress = 0;
89 String messages = "";
90 List<String> ruleSetFileNames = getRuleSetFileNames();
91 for (String fileName : ruleSetFileNames) {
92 RuleSet ruleSet = loadRuleSetByFileName(fileName);
93 for (Rule rule : ruleSet.getRules()) {
94
95
96 if (rule instanceof RuleReference) {
97 continue;
98 }
99
100 Language language = rule.getLanguage();
101 String group = fileName.substring(fileName.lastIndexOf('/') + 1);
102 group = group.substring(0, group.indexOf(".xml"));
103 if (group.indexOf('-') >= 0) {
104 group = group.substring(0, group.indexOf('-'));
105 }
106
107
108 if (rule.getSince() == null) {
109 invalidSinceAttributes++;
110 messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'since' attribute" + PMD.EOL;
111 }
112
113 if (rule.getExternalInfoUrl() == null || "".equalsIgnoreCase(rule.getExternalInfoUrl())) {
114 invalidExternalInfoURL++;
115 messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'externalInfoURL' attribute"
116 + PMD.EOL;
117 } else {
118 String expectedExternalInfoURL = "https?://pmd.(sourceforge.net|github.io)/.+/rules/"
119 + fileName.replaceAll("rulesets/", "").replaceAll(".xml", "") + ".html#" + rule.getName();
120 if (rule.getExternalInfoUrl() == null
121 || !rule.getExternalInfoUrl().matches(expectedExternalInfoURL)) {
122 invalidExternalInfoURL++;
123 messages += "Rule " + fileName + "/" + rule.getName()
124 + " seems to have an invalid 'externalInfoURL' value (" + rule.getExternalInfoUrl()
125 + "), it should be:" + expectedExternalInfoURL + PMD.EOL;
126 }
127 }
128
129 String expectedClassName = "net.sourceforge.pmd.lang." + language.getTerseName() + ".rule." + group
130 + "." + rule.getName() + "Rule";
131 if (!rule.getRuleClass().equals(expectedClassName)
132 && !rule.getRuleClass().equals(XPathRule.class.getName())) {
133 invalidClassName++;
134 messages += "Rule " + fileName + "/" + rule.getName() + " seems to have an invalid 'class' value ("
135 + rule.getRuleClass() + "), it should be:" + expectedClassName + PMD.EOL;
136 }
137
138 if (rule.getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR) != null) {
139 invalidRegexSuppress++;
140 messages += "Rule " + fileName + "/" + rule.getName() + " should not have '"
141 + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name()
142 + "', this is intended for end user customization only." + PMD.EOL;
143 }
144
145 if (rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR) != null) {
146 invalidXPathSuppress++;
147 messages += "Rule " + fileName + "/" + rule.getName() + " should not have '"
148 + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name()
149 + "', this is intended for end user customization only." + PMD.EOL;
150 }
151 }
152 }
153
154
155 if (invalidSinceAttributes > 0 || invalidExternalInfoURL > 0 || invalidClassName > 0
156 || invalidRegexSuppress > 0 || invalidXPathSuppress > 0) {
157 fail("All built-in PMD rules need 'since' attribute (" + invalidSinceAttributes
158 + " are missing), a proper ExternalURLInfo (" + invalidExternalInfoURL
159 + " are invalid), a class name meeting conventions (" + invalidClassName + " are invalid), no '"
160 + Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "' property (" + invalidRegexSuppress
161 + " are invalid), and no '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "' property ("
162 + invalidXPathSuppress + " are invalid)" + PMD.EOL + messages);
163 }
164 }
165
166
167
168
169
170 @Test
171 public void testXmlSchema() throws Exception {
172 boolean allValid = true;
173 List<String> ruleSetFileNames = getRuleSetFileNames();
174 for (String fileName : ruleSetFileNames) {
175 boolean valid = validateAgainstSchema(fileName);
176 allValid = allValid && valid;
177 }
178 assertTrue("All XML must parse without producing validation messages.", allValid);
179 }
180
181
182
183
184
185 @Test
186 public void testDtd() throws Exception {
187 boolean allValid = true;
188 List<String> ruleSetFileNames = getRuleSetFileNames();
189 for (String fileName : ruleSetFileNames) {
190 boolean valid = validateAgainstDtd(fileName);
191 allValid = allValid && valid;
192 }
193 assertTrue("All XML must parse without producing validation messages.", allValid);
194 }
195
196
197
198
199
200
201 @Test
202 public void testReadWriteRoundTrip() throws Exception {
203
204 List<String> ruleSetFileNames = getRuleSetFileNames();
205 for (String fileName : ruleSetFileNames) {
206 testRuleSet(fileName);
207 }
208 }
209
210
211 private List<String> getRuleSetFileNames() throws IOException, RuleSetNotFoundException {
212 List<String> result = new ArrayList<String>();
213
214 for (Language language : LanguageRegistry.getLanguages()) {
215 result.addAll(getRuleSetFileNames(language.getTerseName()));
216 }
217
218 return result;
219 }
220
221 private List<String> getRuleSetFileNames(String language) throws IOException, RuleSetNotFoundException {
222 List<String> ruleSetFileNames = new ArrayList<String>();
223 try {
224 Properties properties = new Properties();
225 properties.load(ResourceLoader.loadResourceAsStream("rulesets/" + language + "/rulesets.properties"));
226 String fileNames = properties.getProperty("rulesets.filenames");
227 StringTokenizer st = new StringTokenizer(fileNames, ",");
228 while (st.hasMoreTokens()) {
229 ruleSetFileNames.add(st.nextToken());
230 }
231 } catch (RuleSetNotFoundException e) {
232
233
234 System.err.println("No ruleset found for language " + language);
235 }
236 return ruleSetFileNames;
237 }
238
239 private RuleSet loadRuleSetByFileName(String ruleSetFileName) throws RuleSetNotFoundException {
240 RuleSetFactory rsf = new RuleSetFactory();
241 return rsf.createRuleSet(ruleSetFileName);
242 }
243
244 private boolean validateAgainstSchema(String fileName) throws IOException, RuleSetNotFoundException,
245 ParserConfigurationException, SAXException {
246 InputStream inputStream = loadResourceAsStream(fileName);
247 boolean valid = validateAgainstSchema(inputStream);
248 if (!valid) {
249 System.err.println("Validation against XML Schema failed for: " + fileName);
250 }
251 return valid;
252 }
253
254 private boolean validateAgainstSchema(InputStream inputStream) throws IOException, RuleSetNotFoundException,
255 ParserConfigurationException, SAXException {
256
257 saxParser.parse(inputStream, validateDefaultHandlerXsd.resetValid());
258 inputStream.close();
259 return validateDefaultHandlerXsd.isValid();
260 }
261
262 private boolean validateAgainstDtd(String fileName) throws IOException, RuleSetNotFoundException,
263 ParserConfigurationException, SAXException {
264 InputStream inputStream = loadResourceAsStream(fileName);
265 boolean valid = validateAgainstDtd(inputStream);
266 if (!valid) {
267 System.err.println("Validation against DTD failed for: " + fileName);
268 }
269 return valid;
270 }
271
272 private boolean validateAgainstDtd(InputStream inputStream) throws IOException, RuleSetNotFoundException,
273 ParserConfigurationException, SAXException {
274
275
276 String file = readFullyToString(inputStream);
277 inputStream.close();
278
279
280 file = file.replaceAll("<\\?xml [ a-zA-Z0-9=\".-]*\\?>", "");
281 file = file.replaceAll("xmlns=\"" + RuleSetWriter.RULESET_NS_URI + "\"", "");
282 file = file.replaceAll("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
283 file = file.replaceAll("xsi:schemaLocation=\"" + RuleSetWriter.RULESET_NS_URI
284 + " http://pmd.sourceforge.net/ruleset_2_0_0.xsd\"", "");
285
286 file = "<?xml version=\"1.0\"?>" + PMD.EOL + "<!DOCTYPE ruleset SYSTEM \"file://"
287 + "/path/does/not/matter/will/be/replaced/ruleset_2_0_0.dtd\">" + PMD.EOL + file;
288
289 InputStream modifiedStream = new ByteArrayInputStream(file.getBytes());
290
291 saxParser.parse(modifiedStream, validateDefaultHandlerDtd.resetValid());
292 modifiedStream.close();
293 return validateDefaultHandlerDtd.isValid();
294 }
295
296 private String readFullyToString(InputStream inputStream) throws IOException {
297 StringBuilder buf = new StringBuilder(64 * 1024);
298 BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
299 String line;
300 while ((line = reader.readLine()) != null) {
301 buf.append(line);
302 buf.append(PMD.EOL);
303 }
304 reader.close();
305 return buf.toString();
306 }
307
308 private static InputStream loadResourceAsStream(String resource) throws RuleSetNotFoundException {
309 InputStream inputStream = ResourceLoader.loadResourceAsStream(resource,
310 AbstractRuleSetFactoryTest.class.getClassLoader());
311 if (inputStream == null) {
312 throw new RuleSetNotFoundException(
313 "Can't find resource "
314 + resource
315 + " Make sure the resource is a valid file or URL or is on the CLASSPATH. Here's the current classpath: "
316 + System.getProperty("java.class.path"));
317 }
318 return inputStream;
319 }
320
321 private void testRuleSet(String fileName) throws IOException, RuleSetNotFoundException,
322 ParserConfigurationException, SAXException {
323
324
325
326
327
328
329
330 RuleSet ruleSet1 = loadRuleSetByFileName(fileName);
331
332
333 ByteArrayOutputStream outputStream1 = new ByteArrayOutputStream();
334 RuleSetWriter writer1 = new RuleSetWriter(outputStream1);
335 writer1.write(ruleSet1);
336 writer1.close();
337 String xml2 = new String(outputStream1.toByteArray());
338
339
340
341 RuleSetFactory ruleSetFactory = new RuleSetFactory();
342 RuleSet ruleSet2 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml2));
343
344
345
346
347 ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
348 RuleSetWriter writer2 = new RuleSetWriter(outputStream2);
349 writer2.write(ruleSet2);
350 writer2.close();
351 String xml3 = new String(outputStream2.toByteArray());
352
353
354
355 RuleSet ruleSet3 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml3));
356
357
358 assertTrue("1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")",
359 validateAgainstSchema(new ByteArrayInputStream(xml2.getBytes())));
360 assertTrue("2nd roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")",
361 validateAgainstSchema(new ByteArrayInputStream(xml3.getBytes())));
362 assertTrue("1st roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")",
363 validateAgainstDtd(new ByteArrayInputStream(xml2.getBytes())));
364 assertTrue("2nd roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")",
365 validateAgainstDtd(new ByteArrayInputStream(xml3.getBytes())));
366
367
368 assertEqualsRuleSet("Original RuleSet and 1st roundtrip Ruleset not the same (filename: " + fileName + ")",
369 ruleSet1, ruleSet2);
370 assertEqualsRuleSet(
371 "1st roundtrip Ruleset and 2nd roundtrip RuleSet not the same (filename: " + fileName + ")", ruleSet2,
372 ruleSet3);
373
374
375
376 assertEquals("1st roundtrip RuleSet XML and 2nd roundtrip RuleSet XML (filename: " + fileName + ")", xml2, xml3);
377 }
378
379 private void assertEqualsRuleSet(String message, RuleSet ruleSet1,
380 RuleSet ruleSet2) {
381 assertEquals(message + ", RuleSet name", ruleSet1.getName(), ruleSet2
382 .getName());
383 assertEquals(message + ", RuleSet description", ruleSet1
384 .getDescription(), ruleSet2.getDescription());
385 assertEquals(message + ", RuleSet exclude patterns", ruleSet1
386 .getExcludePatterns(), ruleSet2.getExcludePatterns());
387 assertEquals(message + ", RuleSet include patterns", ruleSet1
388 .getIncludePatterns(), ruleSet2.getIncludePatterns());
389 assertEquals(message + ", RuleSet rule count", ruleSet1.getRules()
390 .size(), ruleSet2.getRules().size());
391
392 for (int i = 0; i < ruleSet1.getRules().size(); i++) {
393 Rule rule1 = ((List<Rule>) ruleSet1.getRules()).get(i);
394 Rule rule2 = ((List<Rule>) ruleSet2.getRules()).get(i);
395
396 assertFalse(message + ", Different RuleReference",
397 rule1 instanceof RuleReference
398 && !(rule2 instanceof RuleReference)
399 || !(rule1 instanceof RuleReference)
400 && rule2 instanceof RuleReference);
401
402 if (rule1 instanceof RuleReference) {
403 RuleReference ruleReference1 = (RuleReference) rule1;
404 RuleReference ruleReference2 = (RuleReference) rule2;
405 assertEquals(message + ", RuleReference overridden language",
406 ruleReference1.getOverriddenLanguage(), ruleReference2
407 .getOverriddenLanguage());
408 assertEquals(
409 message
410 + ", RuleReference overridden minimum language version",
411 ruleReference1.getOverriddenMinimumLanguageVersion(),
412 ruleReference2.getOverriddenMinimumLanguageVersion());
413 assertEquals(
414 message
415 + ", RuleReference overridden maximum language version",
416 ruleReference1.getOverriddenMaximumLanguageVersion(),
417 ruleReference2.getOverriddenMaximumLanguageVersion());
418 assertEquals(message + ", RuleReference overridden deprecated",
419 ruleReference1.isOverriddenDeprecated(), ruleReference2
420 .isOverriddenDeprecated());
421 assertEquals(message + ", RuleReference overridden name",
422 ruleReference1.getOverriddenName(), ruleReference2
423 .getOverriddenName());
424 assertEquals(
425 message + ", RuleReference overridden description",
426 ruleReference1.getOverriddenDescription(),
427 ruleReference2.getOverriddenDescription());
428 assertEquals(message + ", RuleReference overridden message",
429 ruleReference1.getOverriddenMessage(), ruleReference2
430 .getOverriddenMessage());
431 assertEquals(message
432 + ", RuleReference overridden external info url",
433 ruleReference1.getOverriddenExternalInfoUrl(),
434 ruleReference2.getOverriddenExternalInfoUrl());
435 assertEquals(message + ", RuleReference overridden priority",
436 ruleReference1.getOverriddenPriority(), ruleReference2
437 .getOverriddenPriority());
438 assertEquals(message + ", RuleReference overridden examples",
439 ruleReference1.getOverriddenExamples(), ruleReference2
440 .getOverriddenExamples());
441 }
442
443 assertEquals(message + ", Rule name", rule1.getName(), rule2
444 .getName());
445 assertEquals(message + ", Rule class", rule1.getRuleClass(), rule2
446 .getRuleClass());
447 assertEquals(message + ", Rule description " + rule1.getName(),
448 rule1.getDescription(), rule2.getDescription());
449 assertEquals(message + ", Rule message", rule1.getMessage(), rule2
450 .getMessage());
451 assertEquals(message + ", Rule external info url", rule1
452 .getExternalInfoUrl(), rule2.getExternalInfoUrl());
453 assertEquals(message + ", Rule priority", rule1.getPriority(),
454 rule2.getPriority());
455 assertEquals(message + ", Rule examples", rule1.getExamples(),
456 rule2.getExamples());
457
458 List<PropertyDescriptor<?>> propertyDescriptors1 = rule1
459 .getPropertyDescriptors();
460 List<PropertyDescriptor<?>> propertyDescriptors2 = rule2
461 .getPropertyDescriptors();
462 assertEquals(message + ", Rule property descriptor ",
463 propertyDescriptors1, propertyDescriptors2);
464 for (int j = 0; j < propertyDescriptors1.size(); j++) {
465 assertEquals(message + ", Rule property value " + j, rule1
466 .getProperty(propertyDescriptors1.get(j)), rule2
467 .getProperty(propertyDescriptors2.get(j)));
468 }
469 assertEquals(message + ", Rule property descriptor count",
470 propertyDescriptors1.size(), propertyDescriptors2.size());
471 }
472 }
473
474
475
476
477
478
479 protected static RuleSetReferenceId createRuleSetReferenceId(final String ruleSetXml) {
480 return new RuleSetReferenceId(null) {
481 @Override
482 public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
483 try {
484 return new ByteArrayInputStream(ruleSetXml.getBytes("UTF-8"));
485 } catch (UnsupportedEncodingException e) {
486 return null;
487 }
488 }
489 };
490 }
491
492
493
494
495 private static class ValidateDefaultHandler extends DefaultHandler {
496 private final String validateDocument;
497 private boolean valid = true;
498
499 public ValidateDefaultHandler(String validateDocument) {
500 this.validateDocument = validateDocument;
501 }
502
503 public ValidateDefaultHandler resetValid() {
504 valid = true;
505 return this;
506 }
507
508 public boolean isValid() {
509 return valid;
510 }
511
512 @Override
513 public void error(SAXParseException e) throws SAXException {
514 log("Error", e);
515 }
516
517 @Override
518 public void fatalError(SAXParseException e) throws SAXException {
519 log("FatalError", e);
520 }
521
522 @Override
523 public void warning(SAXParseException e) throws SAXException {
524 log("Warning", e);
525 }
526
527 private void log(String prefix, SAXParseException e) {
528 String message = prefix + " at (" + e.getLineNumber() + ", " + e.getColumnNumber() + "): " + e.getMessage();
529 System.err.println(message);
530 valid = false;
531 }
532
533 @Override
534 public InputSource resolveEntity(String publicId, String systemId)
535 throws IOException, SAXException {
536 if ("http://pmd.sourceforge.net/ruleset_2_0_0.xsd".equals(systemId)
537 || systemId.endsWith("ruleset_2_0_0.dtd")) {
538 try {
539 InputStream inputStream = loadResourceAsStream(validateDocument);
540 return new InputSource(inputStream);
541 } catch (RuleSetNotFoundException e) {
542 System.err.println(e.getMessage());
543 throw new IOException(e.getMessage());
544 }
545 }
546 throw new IllegalArgumentException(
547 "No clue how to handle: publicId=" + publicId
548 + ", systemId=" + systemId);
549 }
550 }
551
552 }