View Javadoc
1   /**
2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3    */
4   package net.sourceforge.pmd.lang.java.rule.coupling;
5   
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.Collections;
9   import java.util.List;
10  
11  import net.sourceforge.pmd.PropertySource;
12  import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
13  import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
14  import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration;
15  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
16  import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
17  import net.sourceforge.pmd.util.CollectionUtil;
18  
19  /**
20   * The loose package coupling Rule can be used to ensure coupling outside of a
21   * package hierarchy is minimized to all but an allowed set of classes from
22   * within the package hierarchy.
23   * <p>
24   * For example, supposed you have the following package hierarchy:
25   * <ul>
26   * <li><code>org.sample</code></li>
27   * <li><code>org.sample.impl</code></li>
28   * <li><code>org.sample.util</code></li>
29   * </ul>
30   * And the allowed class <code>org.sample.SampleInterface</code>.
31   * <p>
32   * This rule can be used to ensure that all classes within the
33   * <code>org.sample</code> package and its sub-packages are not used outside of
34   * the <code>org.sample</code> package hierarchy. Further, the only allowed
35   * usage outside of a class in the <code>org.sample</code> hierarchy would be
36   * via <code>org.sample.SampleInterface</code>.
37   */
38  public class LoosePackageCouplingRule extends AbstractJavaRule {
39  
40      public static final StringMultiProperty PACKAGES_DESCRIPTOR = new StringMultiProperty("packages",
41              "Restricted packages", new String[] {}, 1.0f, ',');
42  
43      public static final StringMultiProperty CLASSES_DESCRIPTOR = new StringMultiProperty("classes", "Allowed classes",
44              new String[] {}, 2.0f, ',');
45  
46      // The package of this source file
47      private String thisPackage;
48  
49      // The restricted packages
50      private List<String> restrictedPackages;
51  
52      public LoosePackageCouplingRule() {
53          definePropertyDescriptor(PACKAGES_DESCRIPTOR);
54          definePropertyDescriptor(CLASSES_DESCRIPTOR);
55  
56          addRuleChainVisit(ASTCompilationUnit.class);
57          addRuleChainVisit(ASTPackageDeclaration.class);
58          addRuleChainVisit(ASTImportDeclaration.class);
59      }
60  
61      @Override
62      public Object visit(ASTCompilationUnit node, Object data) {
63          this.thisPackage = "";
64  
65          // Sort the restricted packages in reverse order. This will ensure the
66          // child packages are in the list before their parent packages.
67          this.restrictedPackages = new ArrayList<>(Arrays.asList(super.getProperty(PACKAGES_DESCRIPTOR)));
68          Collections.sort(restrictedPackages, Collections.reverseOrder());
69  
70          return data;
71      }
72  
73      @Override
74      public Object visit(ASTPackageDeclaration node, Object data) {
75          this.thisPackage = node.getPackageNameImage();
76          return data;
77      }
78  
79      @Override
80      public Object visit(ASTImportDeclaration node, Object data) {
81  
82          String importPackage = node.getPackageName();
83  
84          // Check each restricted package
85          for (String pkg : getRestrictedPackages()) {
86              // Is this import restricted? Use the deepest sub-package which
87              // restricts this import.
88              if (isContainingPackage(pkg, importPackage)) {
89                  // Is this source in a sub-package of restricted package?
90                  if (pkg.equals(thisPackage) || isContainingPackage(pkg, thisPackage)) {
91                      // Valid usage
92                      break;
93                  } else {
94                      // On demand imports automatically fail because they include
95                      // everything
96                      if (node.isImportOnDemand()) {
97                          addViolation(data, node, new Object[] { node.getImportedName(), pkg });
98                          break;
99                      } else {
100                         if (!isAllowedClass(node)) {
101                             addViolation(data, node, new Object[] { node.getImportedName(), pkg });
102                             break;
103                         }
104                     }
105                 }
106             }
107         }
108         return data;
109     }
110 
111     protected List<String> getRestrictedPackages() {
112         return restrictedPackages;
113     }
114 
115     // Is 1st package a containing package of the 2nd package?
116     protected boolean isContainingPackage(String pkg1, String pkg2) {
117         return pkg1.equals(pkg2) || pkg1.length() < pkg2.length() && pkg2.startsWith(pkg1)
118                 && pkg2.charAt(pkg1.length()) == '.';
119     }
120 
121     protected boolean isAllowedClass(ASTImportDeclaration node) {
122         String importedName = node.getImportedName();
123         for (String clazz : getProperty(CLASSES_DESCRIPTOR)) {
124             if (importedName.equals(clazz)) {
125                 return true;
126             }
127 
128         }
129         return false;
130     }
131 
132     public boolean checksNothing() {
133 
134         return CollectionUtil.isEmpty(getProperty(PACKAGES_DESCRIPTOR))
135                 && CollectionUtil.isEmpty(getProperty(CLASSES_DESCRIPTOR));
136     }
137 
138     /**
139      * @see PropertySource#dysfunctionReason()
140      */
141     @Override
142     public String dysfunctionReason() {
143         return checksNothing() ? "No packages or classes specified" : null;
144     }
145 }