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.InputStream;
7   import java.util.ArrayList;
8   import java.util.List;
9   
10  import net.sourceforge.pmd.util.ResourceLoader;
11  import net.sourceforge.pmd.util.StringUtil;
12  
13  /**
14   * This class is used to parse a RuleSet reference value.  Most commonly used for specifying a
15   * RuleSet to process, or in a Rule 'ref' attribute value in the RuleSet XML.  The RuleSet reference
16   * can refer to either an external RuleSet or the current RuleSet when used as a Rule 'ref'
17   * attribute value.  An individual Rule in the RuleSet can be indicated.
18   * 
19   * For an external RuleSet, referring to the entire RuleSet, the format is <i>ruleSetName</i>,
20   * where the RuleSet name is either a resource file path to a RuleSet that ends with
21   * <code>'.xml'</code>.</li>, or a simple RuleSet name.
22   * 
23   * A simple RuleSet name, is one which contains no path separators, and either contains a '-' or is
24   * entirely numeric release number.  A simple name of the form <code>[language]-[name]</code> is
25   * short for the full RuleSet name <code>rulesets/[language]/[name].xml</code>.  A numeric release
26   * simple name of the form <code>[release]</code> is short for the full PMD Release RuleSet name
27   * <code>rulesets/releases/[release].xml</code>.
28   * 
29   * For an external RuleSet, referring to a single Rule, the format is <i>ruleSetName/ruleName</i>,
30   * where the RuleSet name is as described above.  A Rule with the <i>ruleName</i> should exist
31   * in this external RuleSet.
32   * 
33   * For the current RuleSet, the format is <i>ruleName</i>, where the Rule name is not RuleSet name
34   * (i.e. contains no path separators, '-' or '.xml' in it, and is not all numeric).  A Rule with the
35   * <i>ruleName</i> should exist in the current RuleSet.
36   * 
37   * <table>
38   *    <caption>Examples</caption>
39   *    <thead>
40   *       <tr>
41   *    	    <th>String</th>
42   *    	    <th>RuleSet file name</th>
43   *    	    <th>Rule</th>
44   *       </tr>
45   *    </thead>
46   *    <tbody>
47   *       <tr>
48   *    	    <td>rulesets/java/basic.xml</td>
49   *    	    <td>rulesets/java/basic.xml</td>
50   *    	    <td>all</td>
51   *       </tr>
52   *       <tr>
53   *    	    <td>java-basic</td>
54   *    	    <td>rulesets/java/basic.xml</td>
55   *    	    <td>all</td>
56   *       </tr>
57   *       <tr>
58   *    	    <td>50</td>
59   *    	    <td>rulesets/releases/50.xml</td>
60   *    	    <td>all</td>
61   *       </tr>
62   *       <tr>
63   *    	    <td>rulesets/java/basic.xml/EmptyCatchBlock</td>
64   *    	    <td>rulesets/java/basic.xml</td>
65   *    	    <td>EmptyCatchBlock</td>
66   *       </tr>
67   *       <tr>
68   *    	    <td>EmptyCatchBlock</td>
69   *    	    <td>null</td>
70   *    	    <td>EmptyCatchBlock</td>
71   *       </tr>
72   *    </tbody>
73   * </table>
74   */
75  public class RuleSetReferenceId {
76      private final boolean external;
77      private final String ruleSetFileName;
78      private final boolean allRules;
79      private final String ruleName;
80      private final RuleSetReferenceId externalRuleSetReferenceId;
81  
82      /**
83       * Construct a RuleSetReferenceId for the given single ID string.
84       * @param id The id string.
85       * @throws IllegalArgumentException If the ID contains a comma character.
86       */
87      public RuleSetReferenceId(final String id) {
88  	this(id, null);
89      }
90  
91      /**
92       * Construct a RuleSetReferenceId for the given single ID string.
93       * If an external RuleSetReferenceId is given, the ID must refer to a non-external Rule.  The
94       * external RuleSetReferenceId will be responsible for producing the InputStream containing
95       * the Rule.
96       * 
97       * @param id The id string.
98       * @param externalRuleSetReferenceId A RuleSetReferenceId to associate with this new instance.
99       * @throws IllegalArgumentException If the ID contains a comma character.
100      * @throws IllegalArgumentException If external RuleSetReferenceId is not external.
101      * @throws IllegalArgumentException If the ID is not Rule reference when there is an external RuleSetReferenceId.
102      */
103     public RuleSetReferenceId(final String id, final RuleSetReferenceId externalRuleSetReferenceId) {
104 	if (externalRuleSetReferenceId != null && !externalRuleSetReferenceId.isExternal()) {
105 	    throw new IllegalArgumentException("Cannot pair with non-external <" + externalRuleSetReferenceId + ">.");
106 	}
107 	if (id != null && id.indexOf(',') >= 0) {
108 	    throw new IllegalArgumentException("A single RuleSetReferenceId cannot contain ',' (comma) characters: "
109 		    + id);
110 	}
111 
112 	// Damn this parsing sucks, but my brain is just not working to let me write a simpler scheme.
113 	if (StringUtil.isEmpty(id) || isFullRuleSetName(id)) {
114 	    // A full RuleSet name
115 	    external = true;
116 	    ruleSetFileName = id;
117 	    allRules = true;
118 	    ruleName = null;
119 	} else {
120 	    // Find last path separator if it exists...
121 	    final int separatorIndex = Math.max(id.lastIndexOf('/'), id.lastIndexOf('\\'));
122 	    if (separatorIndex >= 0 && separatorIndex != id.length() - 1) {
123 		final String name = id.substring(0, separatorIndex);
124 		external = true;
125 		if (isFullRuleSetName(name)) {
126 		    // A full RuleSet name
127 		    ruleSetFileName = name;
128 		} else {
129 		    // Likely a simple RuleSet name
130 		    int index = name.indexOf('-');
131 		    if (index >= 0) {
132 			// Standard short name
133 			ruleSetFileName = "rulesets/" + name.substring(0, index) + "/" + name.substring(index + 1)
134 				+ ".xml";
135 		    } else {
136 			// A release RuleSet?
137 			if (name.matches("[0-9]+.*")) {
138 			    ruleSetFileName = "rulesets/releases/" + name + ".xml";
139 			} else {
140 			    // Appears to be a non-standard RuleSet name
141 			    ruleSetFileName = name;
142 			}
143 		    }
144 		}
145 
146 		// Everything left should be a Rule name
147 		allRules = false;
148 		ruleName = id.substring(separatorIndex + 1);
149 	    } else {
150 		// Likely a simple RuleSet name
151 		int index = id.indexOf('-');
152 		if (index >= 0) {
153 		    // Standard short name
154 		    external = true;
155 		    ruleSetFileName = "rulesets/" + id.substring(0, index) + "/" + id.substring(index + 1) + ".xml";
156 		    allRules = true;
157 		    ruleName = null;
158 		} else {
159 		    // A release RuleSet?
160 		    if (id.matches("[0-9]+.*")) {
161 			external = true;
162 			ruleSetFileName = "rulesets/releases/" + id + ".xml";
163 			allRules = true;
164 			ruleName = null;
165 		    } else {
166 			// Must be a Rule name
167 			external = externalRuleSetReferenceId != null ? true : false;
168 			ruleSetFileName = externalRuleSetReferenceId != null ? externalRuleSetReferenceId
169 				.getRuleSetFileName() : null;
170 			allRules = false;
171 			ruleName = id;
172 		    }
173 		}
174 	    }
175 	}
176 
177 	if (this.external && this.ruleName != null && !this.ruleName.equals(id) && externalRuleSetReferenceId != null) {
178 	    throw new IllegalArgumentException("Cannot pair external <" + this + "> with external <"
179 		    + externalRuleSetReferenceId + ">.");
180 	}
181 	this.externalRuleSetReferenceId = externalRuleSetReferenceId;
182     }
183 
184     private static boolean isFullRuleSetName(String name) {
185 	return name.endsWith(".xml");
186     }
187 
188     /**
189      * Parse a String comma separated list of RuleSet reference IDs into a List of
190      * RuleReferenceId instances.
191      * @param referenceString A comma separated list of RuleSet reference IDs.
192      * @return The corresponding List of RuleSetReferenceId instances.
193      */
194     public static List<RuleSetReferenceId> parse(String referenceString) {
195 	List<RuleSetReferenceId> references = new ArrayList<RuleSetReferenceId>();
196 	if (referenceString.indexOf(',') == -1) {
197 	    references.add(new RuleSetReferenceId(referenceString));
198 	} else {
199 	    for (String name : referenceString.split(",")) {
200 		references.add(new RuleSetReferenceId(name));
201 	    }
202 	}
203 	return references;
204     }
205 
206     /**
207      * Is this an external RuleSet reference?
208      * @return <code>true</code> if this is an external reference, <code>false</code> otherwise.
209      */
210     public boolean isExternal() {
211 	return external;
212     }
213 
214     /**
215      * Is this a reference to all Rules in a RuleSet, or a single Rule? 
216      * @return <code>true</code> if this is a reference to all Rules, <code>false</code> otherwise.
217      */
218     public boolean isAllRules() {
219 	return allRules;
220     }
221 
222     /**
223      * Get the RuleSet file name.
224      * @return The RuleSet file name if this is an external reference, <code>null</code> otherwise.
225      */
226     public String getRuleSetFileName() {
227 	return ruleSetFileName;
228     }
229 
230     /**
231      * Get the Rule name.
232      * @return The Rule name.
233      * The Rule name.
234      */
235     public String getRuleName() {
236 	return ruleName;
237     }
238 
239     /**
240      * Try to load the RuleSet resource with the specified ClassLoader.  Multiple attempts to get
241      * independent InputStream instances may be made, so subclasses must ensure they support this
242      * behavior.  Delegates to an external RuleSetReferenceId if there is one associated with this
243      * instance.
244      *
245      * @param classLoader The ClassLoader to use.
246      * @return An InputStream to that resource.
247      * @throws RuleSetNotFoundException if unable to find a resource.
248      */
249     public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
250 	if (externalRuleSetReferenceId == null) {
251 	    InputStream in = StringUtil.isEmpty(ruleSetFileName) ? null : ResourceLoader.loadResourceAsStream(
252 		    ruleSetFileName, classLoader);
253 	    if (in == null) {
254 		throw new RuleSetNotFoundException(
255 			"Can't find resource "
256 				+ ruleSetFileName
257 				+ ".  Make sure the resource is a valid file or URL or is on the CLASSPATH.  Here's the current classpath: "
258 				+ System.getProperty("java.class.path"));
259 	    }
260 	    return in;
261 	} else {
262 	    return externalRuleSetReferenceId.getInputStream(classLoader);
263 	}
264     }
265 
266     /**
267      * Return the String form of this Rule reference.
268      * @return Return the String form of this Rule reference, which is <i>ruleSetFileName</i> for
269      * all Rule external references, <i>ruleSetFileName/ruleName</i>, for a single Rule
270      * external references, or <i>ruleName</i> otherwise.
271      */
272     public String toString() {
273 	if (ruleSetFileName != null) {
274 	    if (allRules) {
275 		return ruleSetFileName;
276 	    } else {
277 		return ruleSetFileName + "/" + ruleName;
278 	    }
279 
280 	} else {
281 	    if (allRules) {
282 		return "anonymous all Rule";
283 	    } else {
284 		return ruleName;
285 	    }
286 	}
287     }
288 }