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 
292         if (name == null) {
293             return false;
294         }
295 
296         name = StringUtils.strip(name);
297         if (name.startsWith("http://") || name.startsWith("https://")) {
298             return true;
299         }
300 
301         return false;
302     }
303 
304     private static boolean isValidUrl(String name) {
305         if (isHttpUrl(name)) {
306             String url = StringUtils.strip(name);
307             try {
308                 HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
309                 connection.setRequestMethod("HEAD");
310                 connection.setConnectTimeout(ResourceLoader.TIMEOUT);
311                 connection.setReadTimeout(ResourceLoader.TIMEOUT);
312                 int responseCode = connection.getResponseCode();
313                 if (responseCode == 200) {
314                     return true;
315                 }
316             } catch (IOException e) {
317                 return false;
318             }
319         }
320         return false;
321     }
322 
323     private static boolean isFullRuleSetName(String name) {
324 
325         return name != null && name.endsWith(".xml");
326     }
327 
328     /**
329      * Parse a String comma separated list of RuleSet reference IDs into a List
330      * of RuleReferenceId instances.
331      * 
332      * @param referenceString A comma separated list of RuleSet reference IDs.
333      * @return The corresponding List of RuleSetReferenceId instances.
334      */
335     public static List<RuleSetReferenceId> parse(String referenceString) {
336         List<RuleSetReferenceId> references = new ArrayList<RuleSetReferenceId>();
337         if (referenceString != null && referenceString.trim().length() > 0) {
338 
339             if (referenceString.indexOf(',') == -1) {
340                 references.add(new RuleSetReferenceId(referenceString));
341             } else {
342                 for (String name : referenceString.split(",")) {
343                     references.add(new RuleSetReferenceId(name.trim()));
344                 }
345             }
346         }
347         return references;
348     }
349 
350     /**
351      * Is this an external RuleSet reference?
352      * 
353      * @return <code>true</code> if this is an external reference,
354      *         <code>false</code> otherwise.
355      */
356     public boolean isExternal() {
357         return external;
358     }
359 
360     /**
361      * Is this a reference to all Rules in a RuleSet, or a single Rule?
362      * 
363      * @return <code>true</code> if this is a reference to all Rules,
364      *         <code>false</code> otherwise.
365      */
366     public boolean isAllRules() {
367         return allRules;
368     }
369 
370     /**
371      * Get the RuleSet file name.
372      * 
373      * @return The RuleSet file name if this is an external reference,
374      *         <code>null</code> otherwise.
375      */
376     public String getRuleSetFileName() {
377         return ruleSetFileName;
378     }
379 
380     /**
381      * Get the Rule name.
382      * 
383      * @return The Rule name. The Rule name.
384      */
385     public String getRuleName() {
386         return ruleName;
387     }
388 
389     /**
390      * Try to load the RuleSet resource with the specified ClassLoader. Multiple
391      * attempts to get independent InputStream instances may be made, so
392      * subclasses must ensure they support this behavior. Delegates to an
393      * external RuleSetReferenceId if there is one associated with this
394      * instance.
395      *
396      * @param classLoader The ClassLoader to use.
397      * @return An InputStream to that resource.
398      * @throws RuleSetNotFoundException if unable to find a resource.
399      */
400     public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
401         if (externalRuleSetReferenceId == null) {
402             InputStream in = StringUtil.isEmpty(ruleSetFileName) ? null : ResourceLoader.loadResourceAsStream(
403                     ruleSetFileName, classLoader);
404             if (in == null) {
405                 throw new RuleSetNotFoundException("Can't find resource '" + ruleSetFileName + "' for rule '"
406                         + ruleName + "'" + ".  Make sure the resource is a valid file or URL and is on the CLASSPATH. "
407                         + "Here's the current classpath: " + System.getProperty("java.class.path"));
408             }
409             return in;
410         } else {
411             return externalRuleSetReferenceId.getInputStream(classLoader);
412         }
413     }
414 
415     /**
416      * Return the String form of this Rule reference.
417      * 
418      * @return Return the String form of this Rule reference, which is
419      *         <i>ruleSetFileName</i> for all Rule external references,
420      *         <i>ruleSetFileName/ruleName</i>, for a single Rule external
421      *         references, or <i>ruleName</i> otherwise.
422      */
423     public String toString() {
424         if (ruleSetFileName != null) {
425             if (allRules) {
426                 return ruleSetFileName;
427             } else {
428                 return ruleSetFileName + "/" + ruleName;
429             }
430 
431         } else {
432             if (allRules) {
433                 return "anonymous all Rule";
434             } else {
435                 return ruleName;
436             }
437         }
438     }
439 }