View Javadoc

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