RuleSetReferenceId.java
001 /**
002  * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003  */
004 package net.sourceforge.pmd;
005 
006 import java.io.InputStream;
007 import java.util.ArrayList;
008 import java.util.List;
009 
010 import net.sourceforge.pmd.util.ResourceLoader;
011 import net.sourceforge.pmd.util.StringUtil;
012 
013 /**
014  * This class is used to parse a RuleSet reference value.  Most commonly used for specifying a
015  * RuleSet to process, or in a Rule 'ref' attribute value in the RuleSet XML.  The RuleSet reference
016  * can refer to either an external RuleSet or the current RuleSet when used as a Rule 'ref'
017  * attribute value.  An individual Rule in the RuleSet can be indicated.
018  
019  * For an external RuleSet, referring to the entire RuleSet, the format is <i>ruleSetName</i>,
020  * where the RuleSet name is either a resource file path to a RuleSet that ends with
021  <code>'.xml'</code>.</li>, or a simple RuleSet name.
022  
023  * A simple RuleSet name, is one which contains no path separators, and either contains a '-' or is
024  * entirely numeric release number.  A simple name of the form <code>[language]-[name]</code> is
025  * short for the full RuleSet name <code>rulesets/[language]/[name].xml</code>.  A numeric release
026  * simple name of the form <code>[release]</code> is short for the full PMD Release RuleSet name
027  <code>rulesets/releases/[release].xml</code>.
028  
029  * For an external RuleSet, referring to a single Rule, the format is <i>ruleSetName/ruleName</i>,
030  * where the RuleSet name is as described above.  A Rule with the <i>ruleName</i> should exist
031  * in this external RuleSet.
032  
033  * For the current RuleSet, the format is <i>ruleName</i>, where the Rule name is not RuleSet name
034  * (i.e. contains no path separators, '-' or '.xml' in it, and is not all numeric).  A Rule with the
035  <i>ruleName</i> should exist in the current RuleSet.
036  
037  <table>
038  *    <caption>Examples</caption>
039  *    <thead>
040  *       <tr>
041  *          <th>String</th>
042  *          <th>RuleSet file name</th>
043  *          <th>Rule</th>
044  *       </tr>
045  *    </thead>
046  *    <tbody>
047  *       <tr>
048  *          <td>rulesets/java/basic.xml</td>
049  *          <td>rulesets/java/basic.xml</td>
050  *          <td>all</td>
051  *       </tr>
052  *       <tr>
053  *          <td>java-basic</td>
054  *          <td>rulesets/java/basic.xml</td>
055  *          <td>all</td>
056  *       </tr>
057  *       <tr>
058  *          <td>50</td>
059  *          <td>rulesets/releases/50.xml</td>
060  *          <td>all</td>
061  *       </tr>
062  *       <tr>
063  *          <td>rulesets/java/basic.xml/EmptyCatchBlock</td>
064  *          <td>rulesets/java/basic.xml</td>
065  *          <td>EmptyCatchBlock</td>
066  *       </tr>
067  *       <tr>
068  *          <td>EmptyCatchBlock</td>
069  *          <td>null</td>
070  *          <td>EmptyCatchBlock</td>
071  *       </tr>
072  *    </tbody>
073  </table>
074  */
075 public class RuleSetReferenceId {
076     private final boolean external;
077     private final String ruleSetFileName;
078     private final boolean allRules;
079     private final String ruleName;
080     private final RuleSetReferenceId externalRuleSetReferenceId;
081 
082     /**
083      * Construct a RuleSetReferenceId for the given single ID string.
084      @param id The id string.
085      @throws IllegalArgumentException If the ID contains a comma character.
086      */
087     public RuleSetReferenceId(final String id) {
088   this(id, null);
089     }
090 
091     /**
092      * Construct a RuleSetReferenceId for the given single ID string.
093      * If an external RuleSetReferenceId is given, the ID must refer to a non-external Rule.  The
094      * external RuleSetReferenceId will be responsible for producing the InputStream containing
095      * the Rule.
096      
097      @param id The id string.
098      @param externalRuleSetReferenceId A RuleSetReferenceId to associate with this new instance.
099      @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 >= && 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 classLoaderthrows RuleSetNotFoundException {
250   if (externalRuleSetReferenceId == null) {
251       InputStream in = StringUtil.isEmpty(ruleSetFileNamenull : 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 }