View Javadoc

    1   /**
    2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
    3    */
    4   package net.sourceforge.pmd;
    5
    6   import java.io.InputStream;
    7   import java.util.ArrayList;
    8   import java.util.List;
    9
    10  import net.sourceforge.pmd.util.ResourceLoader;
    11  import net.sourceforge.pmd.util.StringUtil;
    12
    13  /**
    14   * This class is used to parse a RuleSet reference value.  Most commonly used for specifying a
    15   * RuleSet to process, or in a Rule 'ref' attribute value in the RuleSet XML.  The RuleSet reference
    16   * can refer to either an external RuleSet or the current RuleSet when used as a Rule 'ref'
    17   * attribute value.  An individual Rule in the RuleSet can be indicated.
    18   *
    19   * For an external RuleSet, referring to the entire RuleSet, the format is <i>ruleSetName</i>,
    20   * where the RuleSet name is either a resource file path to a RuleSet that ends with
    21   * <code>'.xml'</code>.</li>, or a simple RuleSet name.
    22   *
    23   * A simple RuleSet name, is one which contains no path separators, and either contains a '-' or is
    24   * entirely numeric release number.  A simple name of the form <code>[language]-[name]</code> is
    25   * short for the full RuleSet name <code>rulesets/[language]/[name].xml</code>.  A numeric release
    26   * simple name of the form <code>[release]</code> is short for the full PMD Release RuleSet name
    27   * <code>rulesets/releases/[release].xml</code>.
    28   *
    29   * For an external RuleSet, referring to a single Rule, the format is <i>ruleSetName/ruleName</i>,
    30   * where the RuleSet name is as described above.  A Rule with the <i>ruleName</i> should exist
    31   * in this external RuleSet.
    32   *
    33   * For the current RuleSet, the format is <i>ruleName</i>, where the Rule name is not RuleSet name
    34   * (i.e. contains no path separators, '-' or '.xml' in it, and is not all numeric).  A Rule with the
    35   * <i>ruleName</i> should exist in the current RuleSet.
    36   *
    37   * <table>
    38   *    <caption>Examples</caption>
    39   *    <thead>
    40   *       <tr>
    41   *    	    <th>String</th>
    42   *    	    <th>RuleSet file name</th>
    43   *    	    <th>Rule</th>
    44   *       </tr>
    45   *    </thead>
    46   *    <tbody>
    47   *       <tr>
    48   *    	    <td>rulesets/java/basic.xml</td>
    49   *    	    <td>rulesets/java/basic.xml</td>
    50   *    	    <td>all</td>
    51   *       </tr>
    52   *       <tr>
    53   *    	    <td>java-basic</td>
    54   *    	    <td>rulesets/java/basic.xml</td>
    55   *    	    <td>all</td>
    56   *       </tr>
    57   *       <tr>
    58   *    	    <td>50</td>
    59   *    	    <td>rulesets/releases/50.xml</td>
    60   *    	    <td>all</td>
    61   *       </tr>
    62   *       <tr>
    63   *    	    <td>rulesets/java/basic.xml/EmptyCatchBlock</td>
    64   *    	    <td>rulesets/java/basic.xml</td>
    65   *    	    <td>EmptyCatchBlock</td>
    66   *       </tr>
    67   *       <tr>
    68   *    	    <td>EmptyCatchBlock</td>
    69   *    	    <td>null</td>
    70   *    	    <td>EmptyCatchBlock</td>
    71   *       </tr>
    72   *    </tbody>
    73   * </table>
    74   */
    75  public class RuleSetReferenceId {
    76      private final boolean external;
    77      private final String ruleSetFileName;
    78      private final boolean allRules;
    79      private final String ruleName;
    80      private final RuleSetReferenceId externalRuleSetReferenceId;
    81
    82      /**
    83       * Construct a RuleSetReferenceId for the given single ID string.
    84       * @param id The id string.
    85       * @throws IllegalArgumentException If the ID contains a comma character.
    86       */
    87      public RuleSetReferenceId(final String id) {
    88  	this(id, null);
    89      }
    90
    91      /**
    92       * Construct a RuleSetReferenceId for the given single ID string.
    93       * If an external RuleSetReferenceId is given, the ID must refer to a non-external Rule.  The
    94       * external RuleSetReferenceId will be responsible for producing the InputStream containing
    95       * the Rule.
    96       *
    97       * @param id The id string.
    98       * @param externalRuleSetReferenceId A RuleSetReferenceId to associate with this new instance.
    99       * @throws IllegalArgumentException If the ID contains a comma character.
    100      * @throws IllegalArgumentException If external RuleSetReferenceId is not external.
    101      * @throws IllegalArgumentException If the ID is not Rule reference when there is an external RuleSetReferenceId.
    102      */
    103     public RuleSetReferenceId(final String id, final RuleSetReferenceId externalRuleSetReferenceId) {
    104 	if (externalRuleSetReferenceId != null && !externalRuleSetReferenceId.isExternal()) {
    105 	    throw new IllegalArgumentException("Cannot pair with non-external <" + externalRuleSetReferenceId + ">.");
    106 	}
    107 	if (id != null && id.indexOf(',') >= 0) {
    108 	    throw new IllegalArgumentException("A single RuleSetReferenceId cannot contain ',' (comma) characters: "
    109 		    + id);
    110 	}
    111
    112 	// Damn this parsing sucks, but my brain is just not working to let me write a simpler scheme.
    113 	if (StringUtil.isEmpty(id) || isFullRuleSetName(id)) {
    114 	    // A full RuleSet name
    115 	    external = true;
    116 	    ruleSetFileName = id;
    117 	    allRules = true;
    118 	    ruleName = null;
    119 	} else {
    120 	    // Find last path separator if it exists...
    121 	    final int separatorIndex = Math.max(id.lastIndexOf('/'), id.lastIndexOf('\\'));
    122 	    if (separatorIndex >= 0 && separatorIndex != id.length() - 1) {
    123 		final String name = id.substring(0, separatorIndex);
    124 		external = true;
    125 		if (isFullRuleSetName(name)) {
    126 		    // A full RuleSet name
    127 		    ruleSetFileName = name;
    128 		} else {
    129 		    // Likely a simple RuleSet name
    130 		    int index = name.indexOf('-');
    131 		    if (index >= 0) {
    132 			// Standard short name
    133 			ruleSetFileName = "rulesets/" + name.substring(0, index) + "/" + name.substring(index + 1)
    134 				+ ".xml";
    135 		    } else {
    136 			// A release RuleSet?
    137 			if (name.matches("[0-9]+.*")) {
    138 			    ruleSetFileName = "rulesets/releases/" + name + ".xml";
    139 			} else {
    140 			    // Appears to be a non-standard RuleSet name
    141 			    ruleSetFileName = name;
    142 			}
    143 		    }
    144 		}
    145
    146 		// Everything left should be a Rule name
    147 		allRules = false;
    148 		ruleName = id.substring(separatorIndex + 1);
    149 	    } else {
    150 		// Likely a simple RuleSet name
    151 		int index = id.indexOf('-');
    152 		if (index >= 0) {
    153 		    // Standard short name
    154 		    external = true;
    155 		    ruleSetFileName = "rulesets/" + id.substring(0, index) + "/" + id.substring(index + 1) + ".xml";
    156 		    allRules = true;
    157 		    ruleName = null;
    158 		} else {
    159 		    // A release RuleSet?
    160 		    if (id.matches("[0-9]+.*")) {
    161 			external = true;
    162 			ruleSetFileName = "rulesets/releases/" + id + ".xml";
    163 			allRules = true;
    164 			ruleName = null;
    165 		    } else {
    166 			// Must be a Rule name
    167 			external = externalRuleSetReferenceId != null ? true : false;
    168 			ruleSetFileName = externalRuleSetReferenceId != null ? externalRuleSetReferenceId
    169 				.getRuleSetFileName() : null;
    170 			allRules = false;
    171 			ruleName = id;
    172 		    }
    173 		}
    174 	    }
    175 	}
    176
    177 	if (this.external && this.ruleName != null && !this.ruleName.equals(id) && externalRuleSetReferenceId != null) {
    178 	    throw new IllegalArgumentException("Cannot pair external <" + this + "> with external <"
    179 		    + externalRuleSetReferenceId + ">.");
    180 	}
    181 	this.externalRuleSetReferenceId = externalRuleSetReferenceId;
    182     }
    183
    184     private static boolean isFullRuleSetName(String name) {
    185 	return name.endsWith(".xml");
    186     }
    187
    188     /**
    189      * Parse a String comma separated list of RuleSet reference IDs into a List of
    190      * RuleReferenceId instances.
    191      * @param referenceString A comma separated list of RuleSet reference IDs.
    192      * @return The corresponding List of RuleSetReferenceId instances.
    193      */
    194     public static List<RuleSetReferenceId> parse(String referenceString) {
    195 	List<RuleSetReferenceId> references = new ArrayList<RuleSetReferenceId>();
    196 	if (referenceString.indexOf(',') == -1) {
    197 	    references.add(new RuleSetReferenceId(referenceString));
    198 	} else {
    199 	    for (String name : referenceString.split(",")) {
    200 		references.add(new RuleSetReferenceId(name));
    201 	    }
    202 	}
    203 	return references;
    204     }
    205
    206     /**
    207      * Is this an external RuleSet reference?
    208      * @return <code>true</code> if this is an external reference, <code>false</code> otherwise.
    209      */
    210     public boolean isExternal() {
    211 	return external;
    212     }
    213
    214     /**
    215      * Is this a reference to all Rules in a RuleSet, or a single Rule?
    216      * @return <code>true</code> if this is a reference to all Rules, <code>false</code> otherwise.
    217      */
    218     public boolean isAllRules() {
    219 	return allRules;
    220     }
    221
    222     /**
    223      * Get the RuleSet file name.
    224      * @return The RuleSet file name if this is an external reference, <code>null</code> otherwise.
    225      */
    226     public String getRuleSetFileName() {
    227 	return ruleSetFileName;
    228     }
    229
    230     /**
    231      * Get the Rule name.
    232      * @return The Rule name.
    233      * The Rule name.
    234      */
    235     public String getRuleName() {
    236 	return ruleName;
    237     }
    238
    239     /**
    240      * Try to load the RuleSet resource with the specified ClassLoader.  Multiple attempts to get
    241      * independent InputStream instances may be made, so subclasses must ensure they support this
    242      * behavior.  Delegates to an external RuleSetReferenceId if there is one associated with this
    243      * instance.
    244      *
    245      * @param classLoader The ClassLoader to use.
    246      * @return An InputStream to that resource.
    247      * @throws RuleSetNotFoundException if unable to find a resource.
    248      */
    249     public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
    250 	if (externalRuleSetReferenceId == null) {
    251 	    InputStream in = StringUtil.isEmpty(ruleSetFileName) ? null : ResourceLoader.loadResourceAsStream(
    252 		    ruleSetFileName, classLoader);
    253 	    if (in == null) {
    254 		throw new RuleSetNotFoundException(
    255 			"Can't find resource "
    256 				+ ruleSetFileName
    257 				+ ".  Make sure the resource is a valid file or URL or is on the CLASSPATH.  Here's the current classpath: "
    258 				+ System.getProperty("java.class.path"));
    259 	    }
    260 	    return in;
    261 	} else {
    262 	    return externalRuleSetReferenceId.getInputStream(classLoader);
    263 	}
    264     }
    265
    266     /**
    267      * Return the String form of this Rule reference.
    268      * @return Return the String form of this Rule reference, which is <i>ruleSetFileName</i> for
    269      * all Rule external references, <i>ruleSetFileName/ruleName</i>, for a single Rule
    270      * external references, or <i>ruleName</i> otherwise.
    271      */
    272     public String toString() {
    273 	if (ruleSetFileName != null) {
    274 	    if (allRules) {
    275 		return ruleSetFileName;
    276 	    } else {
    277 		return ruleSetFileName + "/" + ruleName;
    278 	    }
    279
    280 	} else {
    281 	    if (allRules) {
    282 		return "anonymous all Rule";
    283 	    } else {
    284 		return ruleName;
    285 	    }
    286 	}
    287     }
    288 }