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));
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
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 }