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.File;
    7   import java.io.InputStream;
    8   import java.util.ArrayList;
    9   import java.util.List;
    10
    11  import org.apache.commons.io.IOUtils;
    12
    13  import net.sourceforge.pmd.util.ResourceLoader;
    14  import net.sourceforge.pmd.util.StringUtil;
    15
    16  /**
    17   * This class is used to parse a RuleSet reference value.  Most commonly used for specifying a
    18   * RuleSet to process, or in a Rule 'ref' attribute value in the RuleSet XML.  The RuleSet reference
    19   * can refer to either an external RuleSet or the current RuleSet when used as a Rule 'ref'
    20   * attribute value.  An individual Rule in the RuleSet can be indicated.
    21   *
    22   * For an external RuleSet, referring to the entire RuleSet, the format is <i>ruleSetName</i>,
    23   * where the RuleSet name is either a resource file path to a RuleSet that ends with
    24   * <code>'.xml'</code>.</li>, or a simple RuleSet name.
    25   *
    26   * A simple RuleSet name, is one which contains no path separators, and either contains a '-' or is
    27   * entirely numeric release number.  A simple name of the form <code>[language]-[name]</code> is
    28   * short for the full RuleSet name <code>rulesets/[language]/[name].xml</code>.  A numeric release
    29   * simple name of the form <code>[release]</code> is short for the full PMD Release RuleSet name
    30   * <code>rulesets/releases/[release].xml</code>.
    31   *
    32   * For an external RuleSet, referring to a single Rule, the format is <i>ruleSetName/ruleName</i>,
    33   * where the RuleSet name is as described above.  A Rule with the <i>ruleName</i> should exist
    34   * in this external RuleSet.
    35   *
    36   * For the current RuleSet, the format is <i>ruleName</i>, where the Rule name is not RuleSet name
    37   * (i.e. contains no path separators, '-' or '.xml' in it, and is not all numeric).  A Rule with the
    38   * <i>ruleName</i> should exist in the current RuleSet.
    39   *
    40   * <table>
    41   *    <caption>Examples</caption>
    42   *    <thead>
    43   *       <tr>
    44   *    	    <th>String</th>
    45   *    	    <th>RuleSet file name</th>
    46   *    	    <th>Rule</th>
    47   *       </tr>
    48   *    </thead>
    49   *    <tbody>
    50   *       <tr>
    51   *    	    <td>rulesets/java/basic.xml</td>
    52   *    	    <td>rulesets/java/basic.xml</td>
    53   *    	    <td>all</td>
    54   *       </tr>
    55   *       <tr>
    56   *    	    <td>java-basic</td>
    57   *    	    <td>rulesets/java/basic.xml</td>
    58   *    	    <td>all</td>
    59   *       </tr>
    60   *       <tr>
    61   *    	    <td>50</td>
    62   *    	    <td>rulesets/releases/50.xml</td>
    63   *    	    <td>all</td>
    64   *       </tr>
    65   *       <tr>
    66   *    	    <td>rulesets/java/basic.xml/EmptyCatchBlock</td>
    67   *    	    <td>rulesets/java/basic.xml</td>
    68   *    	    <td>EmptyCatchBlock</td>
    69   *       </tr>
    70   *       <tr>
    71   *    	    <td>EmptyCatchBlock</td>
    72   *    	    <td>null</td>
    73   *    	    <td>EmptyCatchBlock</td>
    74   *       </tr>
    75   *    </tbody>
    76   * </table>
    77   */
    78  public class RuleSetReferenceId {
    79      private final boolean external;
    80      private final String ruleSetFileName;
    81      private final boolean allRules;
    82      private final String ruleName;
    83      private final RuleSetReferenceId externalRuleSetReferenceId;
    84
    85      /**
    86       * Construct a RuleSetReferenceId for the given single ID string.
    87       * @param id The id string.
    88       * @throws IllegalArgumentException If the ID contains a comma character.
    89       */
    90      public RuleSetReferenceId(final String id) {
    91  	this(id, null);
    92      }
    93
    94      /**
    95       * Construct a RuleSetReferenceId for the given single ID string.
    96       * If an external RuleSetReferenceId is given, the ID must refer to a non-external Rule.  The
    97       * external RuleSetReferenceId will be responsible for producing the InputStream containing
    98       * the Rule.
    99       *
    100      * @param id The id string.
    101      * @param externalRuleSetReferenceId A RuleSetReferenceId to associate with this new instance.
    102      * @throws IllegalArgumentException If the ID contains a comma character.
    103      * @throws IllegalArgumentException If external RuleSetReferenceId is not external.
    104      * @throws IllegalArgumentException If the ID is not Rule reference when there is an external RuleSetReferenceId.
    105      */
    106     public RuleSetReferenceId(final String id, final RuleSetReferenceId externalRuleSetReferenceId) {
    107         if (externalRuleSetReferenceId != null && !externalRuleSetReferenceId.isExternal()) {
    108             throw new IllegalArgumentException("Cannot pair with non-external <" + externalRuleSetReferenceId + ">.");
    109         }
    110         if (id != null && id.indexOf(',') >= 0) {
    111             throw new IllegalArgumentException("A single RuleSetReferenceId cannot contain ',' (comma) characters: "
    112                     + id);
    113         }
    114
    115         // Damn this parsing sucks, but my brain is just not working to let me
    116         // write a simpler scheme.
    117
    118         if (isFullRuleSetName(id)) {
    119             // A full RuleSet name
    120             external = true;
    121             ruleSetFileName = id;
    122             allRules = true;
    123             ruleName = null;
    124         } else {
    125             String tempRuleName = getRuleName(id);
    126             String tempRuleSetFileName = tempRuleName != null && id != null ?
    127                     id.substring(0, id.length() - tempRuleName.length() - 1) : id;
    128
    129             if (isFullRuleSetName(tempRuleSetFileName)) {
    130                 // remaining part is a xml ruleset file, so the tempRuleName is probably a real rule name
    131                 external = true;
    132                 ruleSetFileName = tempRuleSetFileName;
    133                 ruleName = tempRuleName;
    134                 allRules = tempRuleName == null;
    135             } else {
    136                 // resolve the ruleset name - it's maybe a built in ruleset
    137                 String builtinRuleSet = resolveBuiltInRuleset(tempRuleSetFileName);
    138                 if (checkRulesetExists(builtinRuleSet)) {
    139                     external = true;
    140                     ruleSetFileName = builtinRuleSet;
    141                     ruleName = tempRuleName;
    142                     allRules = tempRuleName == null;
    143                 } else {
    144                     // well, we didn't find the ruleset, so it's probably not a internal ruleset.
    145                     // at this time, we don't know, whether the tempRuleName is a name of the rule
    146                     // or the file name of the ruleset file.
    147                     // It is assumed, that tempRuleName is actually the filename of the ruleset,
    148                     // if there are more separator characters in the remaining ruleset filename (tempRuleSetFileName).
    149                     // This means, the only reliable way to specify single rules within a custom rulesest file is
    150                     // only possible, if the ruleset file has a .xml file extension.
    151                     if (tempRuleSetFileName == null || tempRuleSetFileName.contains(File.separator)) {
    152                         external = true;
    153                         ruleSetFileName = id;
    154                         ruleName = null;
    155                         allRules = true;
    156                     } else {
    157                         external = externalRuleSetReferenceId != null ? externalRuleSetReferenceId.isExternal() : false;
    158                         ruleSetFileName = externalRuleSetReferenceId != null ? externalRuleSetReferenceId.getRuleSetFileName() : null;
    159                         ruleName = id;
    160                         allRules = false;
    161                     }
    162                 }
    163             }
    164         }
    165
    166         if (this.external && this.ruleName != null && !this.ruleName.equals(id) && externalRuleSetReferenceId != null) {
    167             throw new IllegalArgumentException("Cannot pair external <" + this + "> with external <"
    168                     + externalRuleSetReferenceId + ">.");
    169         }
    170         this.externalRuleSetReferenceId = externalRuleSetReferenceId;
    171     }
    172
    173     /**
    174      * Tries to load the given ruleset.
    175      * @param name the ruleset name
    176      * @return <code>true</code> if the ruleset could be loaded, <code>false</code> otherwise.
    177      */
    178     private boolean checkRulesetExists(String name) {
    179         boolean resourceFound = false;
    180         if (name != null) {
    181             try {
    182                 InputStream resource = ResourceLoader.loadResourceAsStream(name, RuleSetReferenceId.class.getClassLoader());
    183                 if (resource != null) {
    184                     resourceFound = true;
    185                     IOUtils.closeQuietly(resource);
    186                 }
    187             } catch (RuleSetNotFoundException e) {
    188                 resourceFound = false;
    189             }
    190         }
    191         return resourceFound;
    192     }
    193
    194     /**
    195      * Assumes that the ruleset name given is e.g. "java-basic". Then
    196      * it will return the full classpath name for the ruleset, in this example
    197      * it would return "rulesets/java/basic.xml".
    198      *
    199      * @param name the ruleset name
    200      * @return the full classpath to the ruleset
    201      */
    202     private String resolveBuiltInRuleset(final String name) {
    203         String result = null;
    204         if (name != null) {
    205             // Likely a simple RuleSet name
    206             int index = name.indexOf('-');
    207             if (index >= 0) {
    208                 // Standard short name
    209                 result = "rulesets/" + name.substring(0, index) + "/" + name.substring(index + 1)
    210                         + ".xml";
    211             } else {
    212                 // A release RuleSet?
    213                 if (name.matches("[0-9]+.*")) {
    214                     result = "rulesets/releases/" + name + ".xml";
    215                 } else {
    216                     // Appears to be a non-standard RuleSet name
    217                     result = name;
    218                 }
    219             }
    220         }
    221         return result;
    222     }
    223
    224     /**
    225      * Extracts the rule name out of a ruleset path. E.g. for "/my/ruleset.xml/MyRule" it
    226      * would return "MyRule". If no single rule is specified, <code>null</code> is returned.
    227      * @param rulesetName the full rule set path
    228      * @return the rule name or <code>null</code>.
    229      */
    230     private String getRuleName(final String rulesetName) {
    231         String result = null;
    232         if (rulesetName != null) {
    233             // Find last path separator if it exists... this might be a rule name
    234             final int separatorIndex = Math.max(rulesetName.lastIndexOf('/'), rulesetName.lastIndexOf('\\'));
    235             if (separatorIndex >= 0 && separatorIndex != rulesetName.length() - 1) {
    236                 result = rulesetName.substring(separatorIndex + 1);
    237             }
    238         }
    239         return result;
    240     }
    241
    242     private static boolean isFullRuleSetName(String name) {
    243         return name != null && name.endsWith(".xml");
    244     }
    245
    246     /**
    247      * Parse a String comma separated list of RuleSet reference IDs into a List of
    248      * RuleReferenceId instances.
    249      * @param referenceString A comma separated list of RuleSet reference IDs.
    250      * @return The corresponding List of RuleSetReferenceId instances.
    251      */
    252     public static List<RuleSetReferenceId> parse(String referenceString) {
    253 	List<RuleSetReferenceId> references = new ArrayList<RuleSetReferenceId>();
    254 	if (referenceString.indexOf(',') == -1) {
    255 	    references.add(new RuleSetReferenceId(referenceString));
    256 	} else {
    257 	    for (String name : referenceString.split(",")) {
    258 		references.add(new RuleSetReferenceId(name));
    259 	    }
    260 	}
    261 	return references;
    262     }
    263
    264     /**
    265      * Is this an external RuleSet reference?
    266      * @return <code>true</code> if this is an external reference, <code>false</code> otherwise.
    267      */
    268     public boolean isExternal() {
    269 	return external;
    270     }
    271
    272     /**
    273      * Is this a reference to all Rules in a RuleSet, or a single Rule?
    274      * @return <code>true</code> if this is a reference to all Rules, <code>false</code> otherwise.
    275      */
    276     public boolean isAllRules() {
    277 	return allRules;
    278     }
    279
    280     /**
    281      * Get the RuleSet file name.
    282      * @return The RuleSet file name if this is an external reference, <code>null</code> otherwise.
    283      */
    284     public String getRuleSetFileName() {
    285 	return ruleSetFileName;
    286     }
    287
    288     /**
    289      * Get the Rule name.
    290      * @return The Rule name.
    291      * The Rule name.
    292      */
    293     public String getRuleName() {
    294 	return ruleName;
    295     }
    296
    297     /**
    298      * Try to load the RuleSet resource with the specified ClassLoader.  Multiple attempts to get
    299      * independent InputStream instances may be made, so subclasses must ensure they support this
    300      * behavior.  Delegates to an external RuleSetReferenceId if there is one associated with this
    301      * instance.
    302      *
    303      * @param classLoader The ClassLoader to use.
    304      * @return An InputStream to that resource.
    305      * @throws RuleSetNotFoundException if unable to find a resource.
    306      */
    307     public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
    308         if (externalRuleSetReferenceId == null) {
    309             InputStream in = StringUtil.isEmpty(ruleSetFileName) ? null : ResourceLoader.loadResourceAsStream(
    310                     ruleSetFileName, classLoader);
    311             if (in == null) {
    312                 throw new RuleSetNotFoundException(
    313                         "Can't find resource " + ruleSetFileName
    314                                 + ".  Make sure the resource is a valid file or URL and is on the CLASSPATH. "
    315                                 + "Here's the current classpath: "
    316                                 + System.getProperty("java.class.path"));
    317             }
    318             return in;
    319         } else {
    320             return externalRuleSetReferenceId.getInputStream(classLoader);
    321         }
    322     }
    323
    324     /**
    325      * Return the String form of this Rule reference.
    326      * @return Return the String form of this Rule reference, which is <i>ruleSetFileName</i> for
    327      * all Rule external references, <i>ruleSetFileName/ruleName</i>, for a single Rule
    328      * external references, or <i>ruleName</i> otherwise.
    329      */
    330     public String toString() {
    331 	if (ruleSetFileName != null) {
    332 	    if (allRules) {
    333 		return ruleSetFileName;
    334 	    } else {
    335 		return ruleSetFileName + "/" + ruleName;
    336 	    }
    337
    338 	} else {
    339 	    if (allRules) {
    340 		return "anonymous all Rule";
    341 	    } else {
    342 		return ruleName;
    343 	    }
    344 	}
    345     }
    346 }