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