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 }
|