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 }