/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.nio.charset.Charset; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; /** * Provides a simple filter mechanism to avoid failing to parse an old ruleset, * which references rules, that have either been removed from PMD already or * renamed or moved to another ruleset. * * @see <a href="https://sourceforge.net/p/pmd/bugs/1360/">issue 1360</a> */ public class RuleSetFactoryCompatibility { private static final Logger LOG = Logger.getLogger(RuleSetFactoryCompatibility.class.getName()); private List<RuleSetFilter> filters = new LinkedList<>(); /** * Creates a new instance of the compatibility filter with the built-in * filters for the modified PMD rules. */ public RuleSetFactoryCompatibility() { // PMD 5.3.0 addFilterRuleRenamed("java", "design", "UncommentedEmptyMethod", "UncommentedEmptyMethodBody"); addFilterRuleRemoved("java", "controversial", "BooleanInversion"); // PMD 5.3.1 addFilterRuleRenamed("java", "design", "UseSingleton", "UseUtilityClass"); // PMD 5.4.0 addFilterRuleMoved("java", "basic", "empty", "EmptyCatchBlock"); addFilterRuleMoved("java", "basic", "empty", "EmptyIfStatement"); addFilterRuleMoved("java", "basic", "empty", "EmptyWhileStmt"); addFilterRuleMoved("java", "basic", "empty", "EmptyTryBlock"); addFilterRuleMoved("java", "basic", "empty", "EmptyFinallyBlock"); addFilterRuleMoved("java", "basic", "empty", "EmptySwitchStatements"); addFilterRuleMoved("java", "basic", "empty", "EmptySynchronizedBlock"); addFilterRuleMoved("java", "basic", "empty", "EmptyStatementNotInLoop"); addFilterRuleMoved("java", "basic", "empty", "EmptyInitializer"); addFilterRuleMoved("java", "basic", "empty", "EmptyStatementBlock"); addFilterRuleMoved("java", "basic", "empty", "EmptyStaticInitializer"); addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryConversionTemporary"); addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryReturn"); addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryFinalModifier"); addFilterRuleMoved("java", "basic", "unnecessary", "UselessOverridingMethod"); addFilterRuleMoved("java", "basic", "unnecessary", "UselessOperationOnImmutable"); addFilterRuleMoved("java", "basic", "unnecessary", "UnusedNullCheckInEquals"); addFilterRuleMoved("java", "basic", "unnecessary", "UselessParentheses"); // PMD 5.6.0 addFilterRuleRenamed("java", "design", "AvoidConstantsInterface", "ConstantsInInterface"); // unused/UnusedModifier moved AND renamed, order is important! addFilterRuleMoved("java", "unusedcode", "unnecessary", "UnusedModifier"); addFilterRuleRenamed("java", "unnecessary", "UnusedModifier", "UnnecessaryModifier"); } void addFilterRuleRenamed(String language, String ruleset, String oldName, String newName) { filters.add(RuleSetFilter.ruleRenamed(language, ruleset, oldName, newName)); } void addFilterRuleMoved(String language, String oldRuleset, String newRuleset, String ruleName) { filters.add(RuleSetFilter.ruleMoved(language, oldRuleset, newRuleset, ruleName)); } void addFilterRuleRemoved(String language, String ruleset, String name) { filters.add(RuleSetFilter.ruleRemoved(language, ruleset, name)); } /** * Applies all configured filters against the given input stream. The * resulting reader will contain the original ruleset modified by the * filters. * * @param stream the original ruleset file input stream * @return a reader, from which the filtered ruleset can be read * @throws IOException if the stream couldn't be read */ public Reader filterRuleSetFile(InputStream stream) throws IOException { byte[] bytes = IOUtils.toByteArray(stream); String encoding = determineEncoding(bytes); String ruleset = new String(bytes, encoding); ruleset = applyAllFilters(ruleset); return new StringReader(ruleset); } private String applyAllFilters(String in) { String result = in; for (RuleSetFilter filter : filters) { result = filter.apply(result); } return result; } private static final Pattern ENCODING_PATTERN = Pattern.compile("encoding=\"([^\"]+)\""); /** * Determines the encoding of the given bytes, assuming this is a XML * document, which specifies the encoding in the first 1024 bytes. * * @param bytes * the input bytes, might be more or less than 1024 bytes * @return the determined encoding, falls back to the default UTF-8 encoding */ String determineEncoding(byte[] bytes) { String firstBytes = new String(bytes, 0, bytes.length > 1024 ? 1024 : bytes.length, Charset.forName("ISO-8859-1")); Matcher matcher = ENCODING_PATTERN.matcher(firstBytes); String encoding = Charset.forName("UTF-8").name(); if (matcher.find()) { encoding = matcher.group(1); } return encoding; } private static class RuleSetFilter { private final Pattern refPattern; private final String replacement; private Pattern exclusionPattern; private String exclusionReplacement; private final String logMessage; private RuleSetFilter(String refPattern, String replacement, String logMessage) { this.logMessage = logMessage; if (replacement != null) { this.refPattern = Pattern.compile("ref=\"" + Pattern.quote(refPattern) + "\""); this.replacement = "ref=\"" + replacement + "\""; } else { this.refPattern = Pattern.compile("<rule\\s+ref=\"" + Pattern.quote(refPattern) + "\"\\s*/>"); this.replacement = ""; } } private void setExclusionPattern(String oldName, String newName) { exclusionPattern = Pattern.compile("<exclude\\s+name=[\"']" + Pattern.quote(oldName) + "[\"']\\s*/>"); if (newName != null) { exclusionReplacement = "<exclude name=\"" + newName + "\" />"; } else { exclusionReplacement = ""; } } public static RuleSetFilter ruleRenamed(String language, String ruleset, String oldName, String newName) { String base = "rulesets/" + language + "/" + ruleset + ".xml/"; RuleSetFilter filter = new RuleSetFilter(base + oldName, base + newName, "The rule \"" + oldName + "\" has been renamed to \"" + newName + "\". Please change your ruleset!"); filter.setExclusionPattern(oldName, newName); return filter; } public static RuleSetFilter ruleMoved(String language, String oldRuleset, String newRuleset, String ruleName) { String base = "rulesets/" + language + "/"; return new RuleSetFilter(base + oldRuleset + ".xml/" + ruleName, base + newRuleset + ".xml/" + ruleName, "The rule \"" + ruleName + "\" has been moved from ruleset \"" + oldRuleset + "\" to \"" + newRuleset + "\". Please change your ruleset!"); } public static RuleSetFilter ruleRemoved(String language, String ruleset, String name) { RuleSetFilter filter = new RuleSetFilter("rulesets/" + language + "/" + ruleset + ".xml/" + name, null, "The rule \"" + name + "\" in ruleset \"" + ruleset + "\" has been removed from PMD and no longer exists. Please change your ruleset!"); filter.setExclusionPattern(name, null); return filter; } String apply(String in) { String result = in; Matcher matcher = refPattern.matcher(in); if (matcher.find()) { result = matcher.replaceAll(replacement); if (LOG.isLoggable(Level.WARNING)) { LOG.warning("Applying rule set filter: " + logMessage); } } if (exclusionPattern == null) { return result; } Matcher exclusions = exclusionPattern.matcher(result); if (exclusions.find()) { result = exclusions.replaceAll(exclusionReplacement); if (LOG.isLoggable(Level.WARNING)) { LOG.warning("Applying rule set filter for exclusions: " + logMessage); } } return result; } } }