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
21   * a 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", "Restricted packages",
41  	    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<String>(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 restricts this import.
87  	    if (isContainingPackage(pkg, importPackage)) {
88  		// Is this source in a sub-package of restricted package?
89  		if (pkg.equals(thisPackage) || isContainingPackage(pkg, thisPackage)) {
90  		    // Valid usage
91  		    break;
92  		} else {
93  		    // On demand imports automatically fail because they include everything
94  		    if (node.isImportOnDemand()) {
95  			addViolation(data, node, new Object[] { node.getImportedName(), pkg });
96  			break;
97  		    } else {
98  			if (!isAllowedClass(node)) {
99  			    addViolation(data, node, new Object[] { node.getImportedName(), pkg });
100 			    break;
101 			}
102 		    }
103 		}
104 	    }
105 	}
106 	return data;
107     }
108 
109     protected List<String> getRestrictedPackages() {
110 	return restrictedPackages;
111     }
112 
113     // Is 1st package a containing package of the 2nd package?
114     protected boolean isContainingPackage(String pkg1, String pkg2) {
115 	return pkg1.equals(pkg2)
116 		|| (pkg1.length() < pkg2.length() && pkg2.startsWith(pkg1) && pkg2.charAt(pkg1.length()) == '.');
117     }
118 
119     protected boolean isAllowedClass(ASTImportDeclaration node) {
120 	String importedName = node.getImportedName();
121 	for (String clazz : getProperty(CLASSES_DESCRIPTOR)) {
122 	    if (importedName.equals(clazz)) {
123 		return true;
124 	    }
125 
126 	}
127 	return false;
128     }
129 
130 	public boolean checksNothing() {
131 
132 		return
133 			CollectionUtil.isEmpty(getProperty(PACKAGES_DESCRIPTOR)) &&
134 			CollectionUtil.isEmpty(getProperty(CLASSES_DESCRIPTOR)) ;
135 	}
136 
137 	/**
138 	 * @see PropertySource#dysfunctionReason()
139 	 */
140 	@Override
141 	public String dysfunctionReason() {
142 		return checksNothing() ?
143 				"No packages or classes specified" :
144 				null;
145 	}
146 }