/** * 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; } }