/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import net.sourceforge.pmd.lang.Language;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.rule.RuleReference;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.util.ResourceLoader;
/**
* Base test class to verify the language's rulesets. This class should be
* subclassed for each language.
*/
public abstract class AbstractRuleSetFactoryTest {
private static SAXParserFactory saxParserFactory;
private static ValidateDefaultHandler validateDefaultHandlerXsd;
private static ValidateDefaultHandler validateDefaultHandlerDtd;
private static SAXParser saxParser;
/**
* Setups the XML parser with validation.
*
* @throws Exception
* any error
*/
@BeforeClass
public static void init() throws Exception {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setValidating(true);
saxParserFactory.setNamespaceAware(true);
// Hope we're using Xerces, or this may not work!
// Note: Features are listed here
// http://xerces.apache.org/xerces2-j/features.html
saxParserFactory.setFeature("http://xml.org/sax/features/validation", true);
saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema", true);
saxParserFactory.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true);
validateDefaultHandlerXsd = new ValidateDefaultHandler("ruleset_2_0_0.xsd");
validateDefaultHandlerDtd = new ValidateDefaultHandler("ruleset_2_0_0.dtd");
saxParser = saxParserFactory.newSAXParser();
}
/**
* Checks all rulesets of all languages on the classpath and verifies that
* all required attributes for all rules are specified.
*
* @throws Exception
* any error
*/
@Test
public void testAllPMDBuiltInRulesMeetConventions() throws Exception {
int invalidSinceAttributes = 0;
int invalidExternalInfoURL = 0;
int invalidClassName = 0;
int invalidRegexSuppress = 0;
int invalidXPathSuppress = 0;
String messages = "";
List<String> ruleSetFileNames = getRuleSetFileNames();
for (String fileName : ruleSetFileNames) {
RuleSet ruleSet = loadRuleSetByFileName(fileName);
for (Rule rule : ruleSet.getRules()) {
// Skip references
if (rule instanceof RuleReference) {
continue;
}
Language language = rule.getLanguage();
String group = fileName.substring(fileName.lastIndexOf('/') + 1);
group = group.substring(0, group.indexOf(".xml"));
if (group.indexOf('-') >= 0) {
group = group.substring(0, group.indexOf('-'));
}
// Is since missing ?
if (rule.getSince() == null) {
invalidSinceAttributes++;
messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'since' attribute" + PMD.EOL;
}
// Is URL valid ?
if (rule.getExternalInfoUrl() == null || "".equalsIgnoreCase(rule.getExternalInfoUrl())) {
invalidExternalInfoURL++;
messages += "Rule " + fileName + "/" + rule.getName() + " is missing 'externalInfoURL' attribute"
+ PMD.EOL;
} else {
String expectedExternalInfoURL = "https?://pmd.(sourceforge.net|github.io)/.+/rules/"
+ fileName.replaceAll("rulesets/", "").replaceAll(".xml", "") + ".html#" + rule.getName();
if (rule.getExternalInfoUrl() == null
|| !rule.getExternalInfoUrl().matches(expectedExternalInfoURL)) {
invalidExternalInfoURL++;
messages += "Rule " + fileName + "/" + rule.getName()
+ " seems to have an invalid 'externalInfoURL' value (" + rule.getExternalInfoUrl()
+ "), it should be:" + expectedExternalInfoURL + PMD.EOL;
}
}
// Proper class name/packaging?
String expectedClassName = "net.sourceforge.pmd.lang." + language.getTerseName() + ".rule." + group
+ "." + rule.getName() + "Rule";
if (!rule.getRuleClass().equals(expectedClassName)
&& !rule.getRuleClass().equals(XPathRule.class.getName())) {
invalidClassName++;
messages += "Rule " + fileName + "/" + rule.getName() + " seems to have an invalid 'class' value ("
+ rule.getRuleClass() + "), it should be:" + expectedClassName + PMD.EOL;
}
// Should not have violation suppress regex property
if (rule.getProperty(Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR) != null) {
invalidRegexSuppress++;
messages += "Rule " + fileName + "/" + rule.getName() + " should not have '"
+ Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name()
+ "', this is intended for end user customization only." + PMD.EOL;
}
// Should not have violation suppress xpath property
if (rule.getProperty(Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR) != null) {
invalidXPathSuppress++;
messages += "Rule " + fileName + "/" + rule.getName() + " should not have '"
+ Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name()
+ "', this is intended for end user customization only." + PMD.EOL;
}
}
}
// We do this at the end to ensure we test ALL the rules before failing
// the test
if (invalidSinceAttributes > 0 || invalidExternalInfoURL > 0 || invalidClassName > 0 || invalidRegexSuppress > 0
|| invalidXPathSuppress > 0) {
fail("All built-in PMD rules need 'since' attribute (" + invalidSinceAttributes
+ " are missing), a proper ExternalURLInfo (" + invalidExternalInfoURL
+ " are invalid), a class name meeting conventions (" + invalidClassName + " are invalid), no '"
+ Rule.VIOLATION_SUPPRESS_REGEX_DESCRIPTOR.name() + "' property (" + invalidRegexSuppress
+ " are invalid), and no '" + Rule.VIOLATION_SUPPRESS_XPATH_DESCRIPTOR.name() + "' property ("
+ invalidXPathSuppress + " are invalid)" + PMD.EOL + messages);
}
}
/**
* Verifies that all rulesets are valid XML according to the xsd schema.
*
* @throws Exception
* any error
*/
@Test
public void testXmlSchema() throws Exception {
boolean allValid = true;
List<String> ruleSetFileNames = getRuleSetFileNames();
for (String fileName : ruleSetFileNames) {
boolean valid = validateAgainstSchema(fileName);
allValid = allValid && valid;
}
assertTrue("All XML must parse without producing validation messages.", allValid);
}
/**
* Verifies that all rulesets are valid XML according to the DTD.
*
* @throws Exception
* any error
*/
@Test
public void testDtd() throws Exception {
boolean allValid = true;
List<String> ruleSetFileNames = getRuleSetFileNames();
for (String fileName : ruleSetFileNames) {
boolean valid = validateAgainstDtd(fileName);
allValid = allValid && valid;
}
assertTrue("All XML must parse without producing validation messages.", allValid);
}
/**
* Reads and writes the rulesets to make sure, that no data is lost if the
* rulests are processed.
*
* @throws Exception
* any error
*/
@Test
public void testReadWriteRoundTrip() throws Exception {
List<String> ruleSetFileNames = getRuleSetFileNames();
for (String fileName : ruleSetFileNames) {
testRuleSet(fileName);
}
}
// Gets all test PMD Ruleset XML files
private List<String> getRuleSetFileNames() throws IOException, RuleSetNotFoundException {
List<String> result = new ArrayList<>();
for (Language language : LanguageRegistry.getLanguages()) {
result.addAll(getRuleSetFileNames(language.getTerseName()));
}
return result;
}
private List<String> getRuleSetFileNames(String language) throws IOException, RuleSetNotFoundException {
List<String> ruleSetFileNames = new ArrayList<>();
try {
Properties properties = new Properties();
try (InputStream is = ResourceLoader.loadResourceAsStream("rulesets/" + language + "/rulesets.properties")) {
properties.load(is);
}
String fileNames = properties.getProperty("rulesets.filenames");
StringTokenizer st = new StringTokenizer(fileNames, ",");
while (st.hasMoreTokens()) {
ruleSetFileNames.add(st.nextToken());
}
} catch (RuleSetNotFoundException e) {
// this might happen if a language is only support by CPD, but not
// by PMD
System.err.println("No ruleset found for language " + language);
}
return ruleSetFileNames;
}
private RuleSet loadRuleSetByFileName(String ruleSetFileName) throws RuleSetNotFoundException {
RuleSetFactory rsf = new RuleSetFactory();
return rsf.createRuleSet(ruleSetFileName);
}
private boolean validateAgainstSchema(String fileName)
throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException {
InputStream inputStream = loadResourceAsStream(fileName);
boolean valid = validateAgainstSchema(inputStream);
if (!valid) {
System.err.println("Validation against XML Schema failed for: " + fileName);
}
return valid;
}
private boolean validateAgainstSchema(InputStream inputStream)
throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException {
saxParser.parse(inputStream, validateDefaultHandlerXsd.resetValid());
inputStream.close();
return validateDefaultHandlerXsd.isValid();
}
private boolean validateAgainstDtd(String fileName)
throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException {
InputStream inputStream = loadResourceAsStream(fileName);
boolean valid = validateAgainstDtd(inputStream);
if (!valid) {
System.err.println("Validation against DTD failed for: " + fileName);
}
return valid;
}
private boolean validateAgainstDtd(InputStream inputStream)
throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException {
// Read file into memory
String file = readFullyToString(inputStream);
inputStream.close();
// Remove XML Schema stuff, replace with DTD
file = file.replaceAll("<\\?xml [ a-zA-Z0-9=\".-]*\\?>", "");
file = file.replaceAll("xmlns=\"" + RuleSetWriter.RULESET_NS_URI + "\"", "");
file = file.replaceAll("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
file = file.replaceAll("xsi:schemaLocation=\"" + RuleSetWriter.RULESET_NS_URI
+ " http://pmd.sourceforge.net/ruleset_2_0_0.xsd\"", "");
file = "<?xml version=\"1.0\"?>" + PMD.EOL + "<!DOCTYPE ruleset SYSTEM \"file://"
+ "/path/does/not/matter/will/be/replaced/ruleset_2_0_0.dtd\">" + PMD.EOL + file;
InputStream modifiedStream = new ByteArrayInputStream(file.getBytes());
saxParser.parse(modifiedStream, validateDefaultHandlerDtd.resetValid());
modifiedStream.close();
return validateDefaultHandlerDtd.isValid();
}
private String readFullyToString(InputStream inputStream) throws IOException {
StringBuilder buf = new StringBuilder(64 * 1024);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
buf.append(line);
buf.append(PMD.EOL);
}
reader.close();
return buf.toString();
}
private static InputStream loadResourceAsStream(String resource) throws RuleSetNotFoundException {
InputStream inputStream = ResourceLoader.loadResourceAsStream(resource,
AbstractRuleSetFactoryTest.class.getClassLoader());
if (inputStream == null) {
throw new RuleSetNotFoundException("Can't find resource " + resource
+ " 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"));
}
return inputStream;
}
private void testRuleSet(String fileName)
throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException {
// Load original XML
// String xml1 =
// readFullyToString(ResourceLoader.loadResourceAsStream(fileName));
// System.out.println("xml1: " + xml1);
// Load the original RuleSet
RuleSet ruleSet1 = loadRuleSetByFileName(fileName);
// Write to XML, first time
ByteArrayOutputStream outputStream1 = new ByteArrayOutputStream();
RuleSetWriter writer1 = new RuleSetWriter(outputStream1);
writer1.write(ruleSet1);
writer1.close();
String xml2 = new String(outputStream1.toByteArray());
// System.out.println("xml2: " + xml2);
// Read RuleSet from XML, first time
RuleSetFactory ruleSetFactory = new RuleSetFactory();
RuleSet ruleSet2 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml2));
// Do write/read a 2nd time, just to be sure
// Write to XML, second time
ByteArrayOutputStream outputStream2 = new ByteArrayOutputStream();
RuleSetWriter writer2 = new RuleSetWriter(outputStream2);
writer2.write(ruleSet2);
writer2.close();
String xml3 = new String(outputStream2.toByteArray());
// System.out.println("xml3: " + xml3);
// Read RuleSet from XML, second time
RuleSet ruleSet3 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml3));
// The 2 written XMLs should all be valid w.r.t Schema/DTD
assertTrue("1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")",
validateAgainstSchema(new ByteArrayInputStream(xml2.getBytes())));
assertTrue("2nd roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")",
validateAgainstSchema(new ByteArrayInputStream(xml3.getBytes())));
assertTrue("1st roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")",
validateAgainstDtd(new ByteArrayInputStream(xml2.getBytes())));
assertTrue("2nd roundtrip RuleSet XML is not valid against DTD (filename: " + fileName + ")",
validateAgainstDtd(new ByteArrayInputStream(xml3.getBytes())));
// All 3 versions of the RuleSet should be the same
assertEqualsRuleSet("Original RuleSet and 1st roundtrip Ruleset not the same (filename: " + fileName + ")",
ruleSet1, ruleSet2);
assertEqualsRuleSet("1st roundtrip Ruleset and 2nd roundtrip RuleSet not the same (filename: " + fileName + ")",
ruleSet2, ruleSet3);
// It's hard to compare the XML DOMs. At least the roundtrip ones should
// textually be the same.
assertEquals("1st roundtrip RuleSet XML and 2nd roundtrip RuleSet XML (filename: " + fileName + ")", xml2,
xml3);
}
private void assertEqualsRuleSet(String message, RuleSet ruleSet1, RuleSet ruleSet2) {
assertEquals(message + ", RuleSet name", ruleSet1.getName(), ruleSet2.getName());
assertEquals(message + ", RuleSet description", ruleSet1.getDescription(), ruleSet2.getDescription());
assertEquals(message + ", RuleSet exclude patterns", ruleSet1.getExcludePatterns(),
ruleSet2.getExcludePatterns());
assertEquals(message + ", RuleSet include patterns", ruleSet1.getIncludePatterns(),
ruleSet2.getIncludePatterns());
assertEquals(message + ", RuleSet rule count", ruleSet1.getRules().size(), ruleSet2.getRules().size());
for (int i = 0; i < ruleSet1.getRules().size(); i++) {
Rule rule1 = ((List<Rule>) ruleSet1.getRules()).get(i);
Rule rule2 = ((List<Rule>) ruleSet2.getRules()).get(i);
assertFalse(message + ", Different RuleReference",
rule1 instanceof RuleReference && !(rule2 instanceof RuleReference)
|| !(rule1 instanceof RuleReference) && rule2 instanceof RuleReference);
if (rule1 instanceof RuleReference) {
RuleReference ruleReference1 = (RuleReference) rule1;
RuleReference ruleReference2 = (RuleReference) rule2;
assertEquals(message + ", RuleReference overridden language", ruleReference1.getOverriddenLanguage(),
ruleReference2.getOverriddenLanguage());
assertEquals(message + ", RuleReference overridden minimum language version",
ruleReference1.getOverriddenMinimumLanguageVersion(),
ruleReference2.getOverriddenMinimumLanguageVersion());
assertEquals(message + ", RuleReference overridden maximum language version",
ruleReference1.getOverriddenMaximumLanguageVersion(),
ruleReference2.getOverriddenMaximumLanguageVersion());
assertEquals(message + ", RuleReference overridden deprecated", ruleReference1.isOverriddenDeprecated(),
ruleReference2.isOverriddenDeprecated());
assertEquals(message + ", RuleReference overridden name", ruleReference1.getOverriddenName(),
ruleReference2.getOverriddenName());
assertEquals(message + ", RuleReference overridden description",
ruleReference1.getOverriddenDescription(), ruleReference2.getOverriddenDescription());
assertEquals(message + ", RuleReference overridden message", ruleReference1.getOverriddenMessage(),
ruleReference2.getOverriddenMessage());
assertEquals(message + ", RuleReference overridden external info url",
ruleReference1.getOverriddenExternalInfoUrl(), ruleReference2.getOverriddenExternalInfoUrl());
assertEquals(message + ", RuleReference overridden priority", ruleReference1.getOverriddenPriority(),
ruleReference2.getOverriddenPriority());
assertEquals(message + ", RuleReference overridden examples", ruleReference1.getOverriddenExamples(),
ruleReference2.getOverriddenExamples());
}
assertEquals(message + ", Rule name", rule1.getName(), rule2.getName());
assertEquals(message + ", Rule class", rule1.getRuleClass(), rule2.getRuleClass());
assertEquals(message + ", Rule description " + rule1.getName(), rule1.getDescription(),
rule2.getDescription());
assertEquals(message + ", Rule message", rule1.getMessage(), rule2.getMessage());
assertEquals(message + ", Rule external info url", rule1.getExternalInfoUrl(), rule2.getExternalInfoUrl());
assertEquals(message + ", Rule priority", rule1.getPriority(), rule2.getPriority());
assertEquals(message + ", Rule examples", rule1.getExamples(), rule2.getExamples());
List<PropertyDescriptor<?>> propertyDescriptors1 = rule1.getPropertyDescriptors();
List<PropertyDescriptor<?>> propertyDescriptors2 = rule2.getPropertyDescriptors();
assertEquals(message + ", Rule property descriptor ", propertyDescriptors1, propertyDescriptors2);
for (int j = 0; j < propertyDescriptors1.size(); j++) {
assertEquals(message + ", Rule property value " + j, rule1.getProperty(propertyDescriptors1.get(j)),
rule2.getProperty(propertyDescriptors2.get(j)));
}
assertEquals(message + ", Rule property descriptor count", propertyDescriptors1.size(),
propertyDescriptors2.size());
}
}
/**
* Create a {@link RuleSetReferenceId} by the given XML string.
*
* @param ruleSetXml
* the ruleset file content as string
* @return the {@link RuleSetReferenceId}
*/
protected static RuleSetReferenceId createRuleSetReferenceId(final String ruleSetXml) {
return new RuleSetReferenceId(null) {
@Override
public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
try {
return new ByteArrayInputStream(ruleSetXml.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
return null;
}
}
};
}
/**
* Validator for the SAX parser
*/
private static class ValidateDefaultHandler extends DefaultHandler {
private final String validateDocument;
private boolean valid = true;
ValidateDefaultHandler(String validateDocument) {
this.validateDocument = validateDocument;
}
public ValidateDefaultHandler resetValid() {
valid = true;
return this;
}
public boolean isValid() {
return valid;
}
@Override
public void error(SAXParseException e) throws SAXException {
log("Error", e);
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
log("FatalError", e);
}
@Override
public void warning(SAXParseException e) throws SAXException {
log("Warning", e);
}
private void log(String prefix, SAXParseException e) {
String message = prefix + " at (" + e.getLineNumber() + ", " + e.getColumnNumber() + "): " + e.getMessage();
System.err.println(message);
valid = false;
}
@Override
public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException {
if ("http://pmd.sourceforge.net/ruleset_2_0_0.xsd".equals(systemId)
|| systemId.endsWith("ruleset_2_0_0.dtd")) {
try {
InputStream inputStream = loadResourceAsStream(validateDocument);
return new InputSource(inputStream);
} catch (RuleSetNotFoundException e) {
System.err.println(e.getMessage());
throw new IOException(e.getMessage());
}
}
throw new IllegalArgumentException(
"No clue how to handle: publicId=" + publicId + ", systemId=" + systemId);
}
}
}