LoosePackageCouplingRule.java
001 package net.sourceforge.pmd.lang.java.rule.coupling;
002 
003 import java.util.ArrayList;
004 import java.util.Arrays;
005 import java.util.Collections;
006 import java.util.List;
007 
008 import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
009 import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
010 import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration;
011 import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
012 import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
013 
014 /**
015  * The loose package coupling Rule can be used to ensure coupling outside of
016  * a package hierarchy is minimized to all but an allowed set of classes from
017  * within the package hierarchy.
018  <p>
019  * For example, supposed you have the following package hierarchy:
020  <ul>
021  *   <li><code>org.sample</code></li>
022  *   <li><code>org.sample.impl</code></li>
023  *   <li><code>org.sample.util</code></li>
024  </ul>
025  * And the allowed class <code>org.sample.SampleInterface</code>.
026  <p>
027  * This rule can be used to ensure that all classes within the
028  <code>org.sample</em> package and its sub-packages are not used outside of
029  * the <code>org.sample</code> package hierarchy.  Further, the only allowed
030  * usage outside of a class in the <code>org.sample</code> hierarchy would be
031  * via <code>org.sample.SampleInterface</code>.
032  */
033 public class LoosePackageCouplingRule extends AbstractJavaRule {
034 
035     private static final StringMultiProperty PACKAGES_DESCRIPTOR = new StringMultiProperty("packages""Restricted packages",
036       new String[] {}1.0f',');
037 
038     private static final StringMultiProperty CLASSES_DESCRIPTOR = new StringMultiProperty("classes""Allowed classes",
039       new String[] {}2.0f',');
040 
041     // The package of this source file
042     private String thisPackage;
043 
044     // The restricted packages
045     private List<String> restrictedPackages;
046 
047     public LoosePackageCouplingRule() {
048   definePropertyDescriptor(PACKAGES_DESCRIPTOR);
049   definePropertyDescriptor(CLASSES_DESCRIPTOR);
050 
051   addRuleChainVisit(ASTCompilationUnit.class);
052   addRuleChainVisit(ASTPackageDeclaration.class);
053   addRuleChainVisit(ASTImportDeclaration.class);
054     }
055 
056     @Override
057     public Object visit(ASTCompilationUnit node, Object data) {
058   this.thisPackage = "";
059 
060   // Sort the restricted packages in reverse order.  This will ensure the
061   // child packages are in the list before their parent packages.
062   this.restrictedPackages = new ArrayList<String>(Arrays.asList(super.getProperty(PACKAGES_DESCRIPTOR)));
063   Collections.sort(restrictedPackages, Collections.reverseOrder());
064 
065   return data;
066     }
067 
068     @Override
069     public Object visit(ASTPackageDeclaration node, Object data) {
070   this.thisPackage = node.getPackageNameImage();
071   return data;
072     }
073 
074     @Override
075     public Object visit(ASTImportDeclaration node, Object data) {
076 
077   String importPackage = node.getPackageName();
078 
079   // Check each restricted package
080   for (String pkg : getRestrictedPackages()) {
081       // Is this import restricted?  Use the deepest sub-package which restricts this import.
082       if (isContainingPackage(pkg, importPackage)) {
083     // Is this source in a sub-package of restricted package?
084     if (pkg.equals(thisPackage|| isContainingPackage(pkg, thisPackage)) {
085         // Valid usage
086         break;
087     else {
088         // On demand imports automatically fail because they include everything
089         if (node.isImportOnDemand()) {
090       addViolation(data, node, new Object[] { node.getImportedName(), pkg });
091       break;
092         else {
093       if (!isAllowedClass(node)) {
094           addViolation(data, node, new Object[] { node.getImportedName(), pkg });
095           break;
096       }
097         }
098     }
099       }
100   }
101   return data;
102     }
103 
104     protected List<String> getRestrictedPackages() {
105   return restrictedPackages;
106     }
107 
108     // Is 1st package a containing package of the 2nd package?
109     protected boolean isContainingPackage(String pkg1, String pkg2) {
110   return pkg1.equals(pkg2)
111     || (pkg1.length() < pkg2.length() && pkg2.startsWith(pkg1&& pkg2.charAt(pkg1.length()) == '.');
112     }
113 
114     protected boolean isAllowedClass(ASTImportDeclaration node) {
115   String importedName = node.getImportedName();
116   for (String clazz : getProperty(CLASSES_DESCRIPTOR)) {
117       if (importedName.equals(clazz)) {
118     return true;
119       }
120 
121   }
122   return false;
123     }
124 }