001: /**
002: * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003: */package net.sourceforge.pmd.rules.design;
004:
005: import java.util.ArrayList;
006: import java.util.List;
007: import java.util.concurrent.atomic.AtomicLong;
008: import java.util.regex.Pattern;
009:
010: import net.sourceforge.pmd.AbstractJavaRule;
011: import net.sourceforge.pmd.PropertyDescriptor;
012: import net.sourceforge.pmd.RuleContext;
013: import net.sourceforge.pmd.ast.ASTClassOrInterfaceType;
014: import net.sourceforge.pmd.ast.ASTCompilationUnit;
015: import net.sourceforge.pmd.ast.ASTImportDeclaration;
016: import net.sourceforge.pmd.ast.SimpleNode;
017: import net.sourceforge.pmd.properties.StringProperty;
018: import net.sourceforge.pmd.rules.regex.RegexHelper;
019:
020: import org.jaxen.JaxenException;
021:
022: /**
023: * <p>A generic rule that can be configured to "count" classes of certain
024: * type based on either their name (full name, prefix, suffixes anything can
025: * be matched with a regex), and/or
026: * their type.</p>
027: *
028: * <p>Example of configurations:
029: * <!-- Property order is MANDATORY !!! -->
030: * <!-- Several regexes may be provided to ensure a match... -->
031: * <property name="nameMatch" description="a regex on which to match"
032: * value="^Abstract.*Bean*$,^*EJB*$"/>
033: * <!-- An operand to refine match strategy TODO: Not implemented yet !!! -->
034: * <property name"operand" description=""
035: * value="and"/> <!-- possible values are and/or -->
036: * <!-- Must be a full name to ensure type control !!! -->
037: * <property name="typeMatch" description="a regex to match on implements/extends classname"
038: * value="javax.servlet.Filter"/>
039: * <!-- Define after how many occurences one should log a violation -->
040: * <property name="threshold" description="Defines how many occurences are legal"
041: * value="2"/>
042: * <!-- TODO: Add a parameter to allow "ignore" pattern based on name -->
043: * </p>
044: *
045: * @author Ryan Gutafson, rgustav@users.sourceforge.net
046: * @author Romain PELISSE, belaran@gmail.com
047: *
048: */
049: public class GenericClassCounterRule extends AbstractJavaRule {
050:
051: private static final PropertyDescriptor nameMatchDescriptor = new StringProperty(
052: "nameMatch",
053: "A series of regex, separeted by ',' to match on the classname",
054: new String[] { "" }, 1.0f, ',');
055:
056: private static final PropertyDescriptor operandDescriptor = new StringProperty(
057: "operand", "or/and value to refined match criteria",
058: new String(), 2.0f);
059:
060: private static final PropertyDescriptor typeMatchDescriptor = new StringProperty(
061: "typeMatch",
062: "A series of regex, separeted by ',' to match on implements/extends classname",
063: new String[] { "" }, 3.0f, ',');
064:
065: private static final PropertyDescriptor thresholdDescriptor = new StringProperty(
066: "threshold", "Defines how many occurences are legal",
067: new String(), 4.0f);
068:
069: private List<Pattern> namesMatch = new ArrayList<Pattern>(0);
070: private List<Pattern> typesMatch = new ArrayList<Pattern>(0);
071: private List<SimpleNode> matches = new ArrayList<SimpleNode>(0);
072: private List<String> simpleClassname = new ArrayList<String>(0);
073:
074: @SuppressWarnings("PMD")
075: // When the rule is finished, this field will be used.
076: private String operand;
077: private int threshold;
078:
079: private static String COUNTER_LABEL;
080:
081: /**
082: * Default empty constructor
083: */
084: public GenericClassCounterRule() {
085: super ();
086: }
087:
088: private List<String> arrayAsList(String[] array) {
089: List<String> list = new ArrayList<String>(array.length);
090: int nbItem = 0;
091: while (nbItem < array.length)
092: list.add(array[nbItem++]);
093: return list;
094: }
095:
096: protected void init() {
097: // Creating the attribute name for the rule context
098: COUNTER_LABEL = this .getClass().getSimpleName()
099: + ".number of match";
100: // Constructing the request from the input parameters
101: this .namesMatch = RegexHelper
102: .compilePatternsFromList(arrayAsList(getStringProperties(nameMatchDescriptor)));
103: this .operand = getStringProperty(operandDescriptor);
104: this .typesMatch = RegexHelper
105: .compilePatternsFromList(arrayAsList(getStringProperties(typeMatchDescriptor)));
106: String thresholdAsString = getStringProperty(thresholdDescriptor);
107: this .threshold = Integer.valueOf(thresholdAsString);
108: // Initializing list of match
109: this .matches = new ArrayList<SimpleNode>();
110:
111: }
112:
113: @Override
114: public void start(RuleContext ctx) {
115: // Adding the proper attribute to the context
116: ctx.setAttribute(COUNTER_LABEL, new AtomicLong());
117: super .start(ctx);
118: }
119:
120: @Override
121: public Object visit(ASTCompilationUnit node, Object data) {
122: init();
123: return super .visit(node, data);
124: }
125:
126: @Override
127: public Object visit(ASTImportDeclaration node, Object data) {
128: // Is there any imported types that match ?
129: for (Pattern pattern : this .typesMatch) {
130: if (RegexHelper.isMatch(pattern, node.getImportedName())) {
131: if (simpleClassname == null)
132: simpleClassname = new ArrayList<String>(1);
133: simpleClassname.add(node.getImportedName());
134: }
135: // FIXME: use type resolution framework to deal with star import ?
136: }
137: return super .visit(node, data);
138: }
139:
140: @Override
141: public Object visit(ASTClassOrInterfaceType classType, Object data) {
142: // Is extends/implements list using one of the previous match on import ?
143: // FIXME: use type resolution framework to deal with star import ?
144: for (String matchType : simpleClassname) {
145: if (searchForAMatch(matchType, classType)) {
146: addAMatch(classType, data);
147: }
148: }
149: // TODO: implements the "operand" functionnality
150: // Is there any names that actually match ?
151: for (Pattern pattern : this .namesMatch)
152: if (RegexHelper.isMatch(pattern, classType.getImage()))
153: addAMatch(classType, data);
154: return super .visit(classType, data);
155: }
156:
157: private void addAMatch(SimpleNode node, Object data) {
158: // We have a match, we increment
159: RuleContext ctx = (RuleContext) data;
160: AtomicLong total = (AtomicLong) ctx.getAttribute(COUNTER_LABEL);
161: total.incrementAndGet();
162: // And we keep a ref on the node for the report generation
163: this .matches.add(node);
164: }
165:
166: @SuppressWarnings("unchecked")
167: private boolean searchForAMatch(String matchType, SimpleNode node) {
168: boolean status = false;
169: String xpathQuery = "//ClassOrInterfaceDeclaration["
170: + "(./ExtendsList/ClassOrInterfaceType[@Image = '"
171: + matchType + "'])" + "or"
172: + "(./ImplementsList/ClassOrInterfaceType[@Image = '"
173: + matchType + "'])" + "]";
174: try {
175: List list = node.findChildNodesWithXPath(xpathQuery);
176: if (list != null && list.size() > 0) {
177: // We got a match !
178: status = true;
179: }
180: } catch (JaxenException e) {
181: // Most likely, a should never happen exception...
182: e.printStackTrace();
183: }
184: return status;
185: }
186:
187: @Override
188: public void end(RuleContext ctx) {
189: AtomicLong total = (AtomicLong) ctx.getAttribute(COUNTER_LABEL);
190: // Do we have a violation ?
191: if (total.get() > this .threshold)
192: for (SimpleNode node : this .matches)
193: addViolation(ctx, node, new Object[] { total });
194: // Cleaning the context for the others rules
195: ctx.removeAttribute(COUNTER_LABEL);
196: super.start(ctx);
197: }
198: }
|