/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.rule.design;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.java.rule.regex.RegexHelper;
import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
import net.sourceforge.pmd.lang.rule.properties.StringProperty;
/**
* <p>
* A generic rule that can be configured to "count" classes of certain type
* based on either their name (full name, prefix, suffixes anything can be
* matched with a regex), and/or their type.
* </p>
*
* <p>Example of configurations:</p>
*
* <pre>
* <!-- Property order is MANDATORY !!! -->
* <!-- Several regexes may be provided to ensure a match... -->
* <property name="nameMatch" description="a regex on which to match"
* value="^Abstract.*Bean*$,^*EJB*$"/>
* <!-- An operand to refine match strategy TODO: Not implemented yet !!! -->
* <property name"operand" description=""
* value="and"/> <!-- possible values are and/or -->
* <!-- Must be a full name to ensure type control !!! -->
* <property name="typeMatch" description="a regex to match on implements/extends classname"
* value="javax.servlet.Filter"/>
* <!-- Define after how many occurences one should log a violation -->
* <property name="threshold" description="Defines how many occurences are legal"
* value="2"/>
* <!-- TODO: Add a parameter to allow "ignore" pattern based on name -->
* </pre>
*
* @author Ryan Gutafson, rgustav@users.sourceforge.net
* @author Romain PELISSE, belaran@gmail.com
*
*/
public class GenericClassCounterRule extends AbstractJavaRule {
private static final StringMultiProperty NAME_MATCH_DESCRIPTOR = new StringMultiProperty("nameMatch",
"A series of regex, separated by ',' to match on the classname", new String[] { "" }, 1.0f, ',');
private static final StringProperty OPERAND_DESCRIPTOR = new StringProperty("operand",
"or/and value to refined match criteria", new String(), 2.0f);
private static final StringMultiProperty TYPE_MATCH_DESCRIPTOR = new StringMultiProperty("typeMatch",
"A series of regex, separated by ',' to match on implements/extends classname", new String[] { "" }, 3.0f,
',');
// TODO - this should be an IntegerProperty instead?
private static final StringProperty THRESHOLD_DESCRIPTOR = new StringProperty("threshold",
"Defines how many occurences are legal", new String(), 4.0f);
private List<Pattern> namesMatch = new ArrayList<>(0);
private List<Pattern> typesMatch = new ArrayList<>(0);
private List<Node> matches = new ArrayList<>(0);
private List<String> simpleClassname = new ArrayList<>(0);
// When the rule is finished, this field will be used.
@SuppressWarnings("PMD")
private String operand;
private int threshold;
private static String counterLabel;
public GenericClassCounterRule() {
definePropertyDescriptor(NAME_MATCH_DESCRIPTOR);
definePropertyDescriptor(OPERAND_DESCRIPTOR);
definePropertyDescriptor(TYPE_MATCH_DESCRIPTOR);
definePropertyDescriptor(THRESHOLD_DESCRIPTOR);
}
private List<String> arrayAsList(String[] array) {
List<String> list = new ArrayList<>(array.length);
int nbItem = 0;
while (nbItem < array.length) {
list.add(array[nbItem++]);
}
return list;
}
protected void init() {
// Creating the attribute name for the rule context
counterLabel = this.getClass().getSimpleName() + ".number of match";
// Constructing the request from the input parameters
this.namesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(NAME_MATCH_DESCRIPTOR)));
this.operand = getProperty(OPERAND_DESCRIPTOR);
this.typesMatch = RegexHelper.compilePatternsFromList(arrayAsList(getProperty(TYPE_MATCH_DESCRIPTOR)));
String thresholdAsString = getProperty(THRESHOLD_DESCRIPTOR);
this.threshold = Integer.valueOf(thresholdAsString);
// Initializing list of match
this.matches = new ArrayList<>();
}
@Override
public void start(RuleContext ctx) {
// Adding the proper attribute to the context
ctx.setAttribute(counterLabel, new AtomicLong());
super.start(ctx);
}
@Override
public Object visit(ASTCompilationUnit node, Object data) {
init();
return super.visit(node, data);
}
@Override
public Object visit(ASTImportDeclaration node, Object data) {
// Is there any imported types that match ?
for (Pattern pattern : this.typesMatch) {
if (RegexHelper.isMatch(pattern, node.getImportedName())) {
if (simpleClassname == null) {
simpleClassname = new ArrayList<>(1);
}
simpleClassname.add(node.getImportedName());
}
// FIXME: use type resolution framework to deal with star import ?
}
return super.visit(node, data);
}
@Override
public Object visit(ASTClassOrInterfaceType classType, Object data) {
// Is extends/implements list using one of the previous match on import ?
// FIXME: use type resolution framework to deal with star import ?
for (String matchType : simpleClassname) {
if (searchForAMatch(matchType, classType)) {
addAMatch(classType, data);
}
}
// TODO: implements the "operand" functionnality
// Is there any names that actually match ?
for (Pattern pattern : this.namesMatch) {
if (RegexHelper.isMatch(pattern, classType.getImage())) {
addAMatch(classType, data);
}
}
return super.visit(classType, data);
}
private void addAMatch(Node node, Object data) {
// We have a match, we increment
RuleContext ctx = (RuleContext) data;
AtomicLong total = (AtomicLong) ctx.getAttribute(counterLabel);
total.incrementAndGet();
// And we keep a ref on the node for the report generation
this.matches.add(node);
}
private boolean searchForAMatch(String matchType, Node node) {
String xpathQuery = "//ClassOrInterfaceDeclaration[(./ExtendsList/ClassOrInterfaceType[@Image = '" + matchType
+ "']) or (./ImplementsList/ClassOrInterfaceType[@Image = '" + matchType + "'])]";
return node.hasDescendantMatchingXPath(xpathQuery);
}
@Override
public void end(RuleContext ctx) {
AtomicLong total = (AtomicLong) ctx.getAttribute(counterLabel);
// Do we have a violation ?
if (total.get() > this.threshold) {
for (Node node : this.matches) {
addViolation(ctx, node, new Object[] { total });
}
// Cleaning the context for the others rules
ctx.removeAttribute(counterLabel);
super.end(ctx);
}
}
}