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