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 != null && referenceString.trim().length() > 0) {
255 
256 	    if (referenceString.indexOf(',') == -1) {
257 		references.add(new RuleSetReferenceId(referenceString));
258 	    } else {
259 		for (String name : referenceString.split(",")) {
260 		    references.add(new RuleSetReferenceId(name.trim()));
261 		}
262 	    }
263 	}
264 	return references;
265     }
266 
267     /**
268      * Is this an external RuleSet reference?
269      * @return <code>true</code> if this is an external reference, <code>false</code> otherwise.
270      */
271     public boolean isExternal() {
272 	return external;
273     }
274 
275     /**
276      * Is this a reference to all Rules in a RuleSet, or a single Rule? 
277      * @return <code>true</code> if this is a reference to all Rules, <code>false</code> otherwise.
278      */
279     public boolean isAllRules() {
280 	return allRules;
281     }
282 
283     /**
284      * Get the RuleSet file name.
285      * @return The RuleSet file name if this is an external reference, <code>null</code> otherwise.
286      */
287     public String getRuleSetFileName() {
288 	return ruleSetFileName;
289     }
290 
291     /**
292      * Get the Rule name.
293      * @return The Rule name.
294      * The Rule name.
295      */
296     public String getRuleName() {
297 	return ruleName;
298     }
299 
300     /**
301      * Try to load the RuleSet resource with the specified ClassLoader.  Multiple attempts to get
302      * independent InputStream instances may be made, so subclasses must ensure they support this
303      * behavior.  Delegates to an external RuleSetReferenceId if there is one associated with this
304      * instance.
305      *
306      * @param classLoader The ClassLoader to use.
307      * @return An InputStream to that resource.
308      * @throws RuleSetNotFoundException if unable to find a resource.
309      */
310     public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
311         if (externalRuleSetReferenceId == null) {
312             InputStream in = StringUtil.isEmpty(ruleSetFileName) ? null : ResourceLoader.loadResourceAsStream(
313                     ruleSetFileName, classLoader);
314             if (in == null) {
315                 throw new RuleSetNotFoundException(
316                         "Can't find resource '" + ruleSetFileName + "' for rule '" + ruleName + "'"
317                                 + ".  Make sure the resource is a valid file or URL and is on the CLASSPATH. "
318                                 + "Here's the current classpath: "
319                                 + System.getProperty("java.class.path"));
320             }
321             return in;
322         } else {
323             return externalRuleSetReferenceId.getInputStream(classLoader);
324         }
325     }
326 
327     /**
328      * Return the String form of this Rule reference.
329      * @return Return the String form of this Rule reference, which is <i>ruleSetFileName</i> for
330      * all Rule external references, <i>ruleSetFileName/ruleName</i>, for a single Rule
331      * external references, or <i>ruleName</i> otherwise.
332      */
333     public String toString() {
334 	if (ruleSetFileName != null) {
335 	    if (allRules) {
336 		return ruleSetFileName;
337 	    } else {
338 		return ruleSetFileName + "/" + ruleName;
339 	    }
340 
341 	} else {
342 	    if (allRules) {
343 		return "anonymous all Rule";
344 	    } else {
345 		return ruleName;
346 	    }
347 	}
348     }
349 }