/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.java.rule.coupling;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import net.sourceforge.pmd.PropertySource;
import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit;
import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration;
import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration;
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule;
import net.sourceforge.pmd.lang.rule.properties.StringMultiProperty;
import net.sourceforge.pmd.util.CollectionUtil;
/**
* The loose package coupling Rule can be used to ensure coupling outside of a
* package hierarchy is minimized to all but an allowed set of classes from
* within the package hierarchy.
* <p>
* For example, supposed you have the following package hierarchy:
* <ul>
* <li><code>org.sample</code></li>
* <li><code>org.sample.impl</code></li>
* <li><code>org.sample.util</code></li>
* </ul>
* And the allowed class <code>org.sample.SampleInterface</code>.
* <p>
* This rule can be used to ensure that all classes within the
* <code>org.sample</code> package and its sub-packages are not used outside of
* the <code>org.sample</code> package hierarchy. Further, the only allowed
* usage outside of a class in the <code>org.sample</code> hierarchy would be
* via <code>org.sample.SampleInterface</code>.
*/
public class LoosePackageCouplingRule extends AbstractJavaRule {
public static final StringMultiProperty PACKAGES_DESCRIPTOR = new StringMultiProperty("packages",
"Restricted packages", new String[] {}, 1.0f, ',');
public static final StringMultiProperty CLASSES_DESCRIPTOR = new StringMultiProperty("classes", "Allowed classes",
new String[] {}, 2.0f, ',');
// The package of this source file
private String thisPackage;
// The restricted packages
private List<String> restrictedPackages;
public LoosePackageCouplingRule() {
definePropertyDescriptor(PACKAGES_DESCRIPTOR);
definePropertyDescriptor(CLASSES_DESCRIPTOR);
addRuleChainVisit(ASTCompilationUnit.class);
addRuleChainVisit(ASTPackageDeclaration.class);
addRuleChainVisit(ASTImportDeclaration.class);
}
@Override
public Object visit(ASTCompilationUnit node, Object data) {
this.thisPackage = "";
// Sort the restricted packages in reverse order. This will ensure the
// child packages are in the list before their parent packages.
this.restrictedPackages = new ArrayList<>(Arrays.asList(super.getProperty(PACKAGES_DESCRIPTOR)));
Collections.sort(restrictedPackages, Collections.reverseOrder());
return data;
}
@Override
public Object visit(ASTPackageDeclaration node, Object data) {
this.thisPackage = node.getPackageNameImage();
return data;
}
@Override
public Object visit(ASTImportDeclaration node, Object data) {
String importPackage = node.getPackageName();
// Check each restricted package
for (String pkg : getRestrictedPackages()) {
// Is this import restricted? Use the deepest sub-package which
// restricts this import.
if (isContainingPackage(pkg, importPackage)) {
// Is this source in a sub-package of restricted package?
if (pkg.equals(thisPackage) || isContainingPackage(pkg, thisPackage)) {
// Valid usage
break;
} else {
// On demand imports automatically fail because they include
// everything
if (node.isImportOnDemand()) {
addViolation(data, node, new Object[] { node.getImportedName(), pkg });
break;
} else {
if (!isAllowedClass(node)) {
addViolation(data, node, new Object[] { node.getImportedName(), pkg });
break;
}
}
}
}
}
return data;
}
protected List<String> getRestrictedPackages() {
return restrictedPackages;
}
// Is 1st package a containing package of the 2nd package?
protected boolean isContainingPackage(String pkg1, String pkg2) {
return pkg1.equals(pkg2)
|| pkg1.length() < pkg2.length() && pkg2.startsWith(pkg1) && pkg2.charAt(pkg1.length()) == '.';
}
protected boolean isAllowedClass(ASTImportDeclaration node) {
String importedName = node.getImportedName();
for (String clazz : getProperty(CLASSES_DESCRIPTOR)) {
if (importedName.equals(clazz)) {
return true;
}
}
return false;
}
public boolean checksNothing() {
return CollectionUtil.isEmpty(getProperty(PACKAGES_DESCRIPTOR))
&& CollectionUtil.isEmpty(getProperty(CLASSES_DESCRIPTOR));
}
/**
* @see PropertySource#dysfunctionReason()
*/
@Override
public String dysfunctionReason() {
return checksNothing() ? "No packages or classes specified" : null;
}
}