/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import net.sourceforge.pmd.util.ResourceLoader;
import net.sourceforge.pmd.util.StringUtil;
/**
* This class is used to parse a RuleSet reference value. Most commonly used for
* specifying a RuleSet to process, or in a Rule 'ref' attribute value in the
* RuleSet XML. The RuleSet reference can refer to either an external RuleSet or
* the current RuleSet when used as a Rule 'ref' attribute value. An individual
* Rule in the RuleSet can be indicated.
*
* For an external RuleSet, referring to the entire RuleSet, the format is
* <i>ruleSetName</i>, where the RuleSet name is either a resource file path to
* a RuleSet that ends with <code>'.xml'</code>, or a simple RuleSet name.
*
* A simple RuleSet name, is one which contains no path separators, and either
* contains a '-' or is entirely numeric release number. A simple name of the
* form <code>[language]-[name]</code> is short for the full RuleSet name
* <code>rulesets/[language]/[name].xml</code>. A numeric release simple name of
* the form <code>[release]</code> is short for the full PMD Release RuleSet
* name <code>rulesets/releases/[release].xml</code>.
*
* For an external RuleSet, referring to a single Rule, the format is
* <i>ruleSetName/ruleName</i>, where the RuleSet name is as described above. A
* Rule with the <i>ruleName</i> should exist in this external RuleSet.
*
* For the current RuleSet, the format is <i>ruleName</i>, where the Rule name
* is not RuleSet name (i.e. contains no path separators, '-' or '.xml' in it,
* and is not all numeric). A Rule with the <i>ruleName</i> should exist in the
* current RuleSet.
*
* <table>
* <caption>Examples</caption> <thead>
* <tr>
* <th>String</th>
* <th>RuleSet file name</th>
* <th>Rule</th>
* </tr>
* </thead> <tbody>
* <tr>
* <td>rulesets/java/basic.xml</td>
* <td>rulesets/java/basic.xml</td>
* <td>all</td>
* </tr>
* <tr>
* <td>java-basic</td>
* <td>rulesets/java/basic.xml</td>
* <td>all</td>
* </tr>
* <tr>
* <td>50</td>
* <td>rulesets/releases/50.xml</td>
* <td>all</td>
* </tr>
* <tr>
* <td>rulesets/java/basic.xml/EmptyCatchBlock</td>
* <td>rulesets/java/basic.xml</td>
* <td>EmptyCatchBlock</td>
* </tr>
* <tr>
* <td>EmptyCatchBlock</td>
* <td>null</td>
* <td>EmptyCatchBlock</td>
* </tr>
* </tbody>
* </table>
*/
public class RuleSetReferenceId {
private final boolean external;
private final String ruleSetFileName;
private final boolean allRules;
private final String ruleName;
private final RuleSetReferenceId externalRuleSetReferenceId;
/**
* Construct a RuleSetReferenceId for the given single ID string.
*
* @param id
* The id string.
* @throws IllegalArgumentException
* If the ID contains a comma character.
*/
public RuleSetReferenceId(final String id) {
this(id, null);
}
/**
* Construct a RuleSetReferenceId for the given single ID string. If an
* external RuleSetReferenceId is given, the ID must refer to a non-external
* Rule. The external RuleSetReferenceId will be responsible for producing
* the InputStream containing the Rule.
*
* @param id
* The id string.
* @param externalRuleSetReferenceId
* A RuleSetReferenceId to associate with this new instance.
* @throws IllegalArgumentException
* If the ID contains a comma character.
* @throws IllegalArgumentException
* If external RuleSetReferenceId is not external.
* @throws IllegalArgumentException
* If the ID is not Rule reference when there is an external
* RuleSetReferenceId.
*/
public RuleSetReferenceId(final String id, final RuleSetReferenceId externalRuleSetReferenceId) {
if (externalRuleSetReferenceId != null && !externalRuleSetReferenceId.isExternal()) {
throw new IllegalArgumentException("Cannot pair with non-external <" + externalRuleSetReferenceId + ">.");
}
if (id != null && id.indexOf(',') >= 0) {
throw new IllegalArgumentException(
"A single RuleSetReferenceId cannot contain ',' (comma) characters: " + id);
}
// Damn this parsing sucks, but my brain is just not working to let me
// write a simpler scheme.
if (isValidUrl(id)) {
// A full RuleSet name
external = true;
ruleSetFileName = StringUtils.strip(id);
allRules = true;
ruleName = null;
} else if (isFullRuleSetName(id)) {
// A full RuleSet name
external = true;
ruleSetFileName = id;
allRules = true;
ruleName = null;
} else {
String tempRuleName = getRuleName(id);
String tempRuleSetFileName = tempRuleName != null && id != null
? id.substring(0, id.length() - tempRuleName.length() - 1) : id;
if (isValidUrl(tempRuleSetFileName)) {
// remaining part is a xml ruleset file, so the tempRuleName is
// probably a real rule name
external = true;
ruleSetFileName = StringUtils.strip(tempRuleSetFileName);
ruleName = StringUtils.strip(tempRuleName);
allRules = tempRuleName == null;
} else if (isHttpUrl(id)) {
// it's a url, we can't determine whether it's a full ruleset or
// a single rule - so falling back to
// a full RuleSet name
external = true;
ruleSetFileName = StringUtils.strip(id);
allRules = true;
ruleName = null;
} else if (isFullRuleSetName(tempRuleSetFileName)) {
// remaining part is a xml ruleset file, so the tempRuleName is
// probably a real rule name
external = true;
ruleSetFileName = tempRuleSetFileName;
ruleName = tempRuleName;
allRules = tempRuleName == null;
} else {
// resolve the ruleset name - it's maybe a built in ruleset
String builtinRuleSet = resolveBuiltInRuleset(tempRuleSetFileName);
if (checkRulesetExists(builtinRuleSet)) {
external = true;
ruleSetFileName = builtinRuleSet;
ruleName = tempRuleName;
allRules = tempRuleName == null;
} else {
// well, we didn't find the ruleset, so it's probably not a
// internal ruleset.
// at this time, we don't know, whether the tempRuleName is
// a name of the rule
// or the file name of the ruleset file.
// It is assumed, that tempRuleName is actually the filename
// of the ruleset,
// if there are more separator characters in the remaining
// ruleset filename (tempRuleSetFileName).
// This means, the only reliable way to specify single rules
// within a custom rulesest file is
// only possible, if the ruleset file has a .xml file
// extension.
if (tempRuleSetFileName == null || tempRuleSetFileName.contains(File.separator)) {
external = true;
ruleSetFileName = id;
ruleName = null;
allRules = true;
} else {
external = externalRuleSetReferenceId != null ? externalRuleSetReferenceId.isExternal() : false;
ruleSetFileName = externalRuleSetReferenceId != null
? externalRuleSetReferenceId.getRuleSetFileName() : null;
ruleName = id;
allRules = false;
}
}
}
}
if (this.external && this.ruleName != null && !this.ruleName.equals(id) && externalRuleSetReferenceId != null) {
throw new IllegalArgumentException(
"Cannot pair external <" + this + "> with external <" + externalRuleSetReferenceId + ">.");
}
this.externalRuleSetReferenceId = externalRuleSetReferenceId;
}
/**
* Tries to load the given ruleset.
*
* @param name
* the ruleset name
* @return <code>true</code> if the ruleset could be loaded,
* <code>false</code> otherwise.
*/
private boolean checkRulesetExists(String name) {
boolean resourceFound = false;
if (name != null) {
try {
InputStream resource = ResourceLoader.loadResourceAsStream(name,
RuleSetReferenceId.class.getClassLoader());
if (resource != null) {
resourceFound = true;
IOUtils.closeQuietly(resource);
}
} catch (RuleSetNotFoundException e) {
resourceFound = false;
}
}
return resourceFound;
}
/**
* Assumes that the ruleset name given is e.g. "java-basic". Then it will
* return the full classpath name for the ruleset, in this example it would
* return "rulesets/java/basic.xml".
*
* @param name
* the ruleset name
* @return the full classpath to the ruleset
*/
private String resolveBuiltInRuleset(final String name) {
String result = null;
if (name != null) {
// Likely a simple RuleSet name
int index = name.indexOf('-');
if (index >= 0) {
// Standard short name
result = "rulesets/" + name.substring(0, index) + "/" + name.substring(index + 1) + ".xml";
} else {
// A release RuleSet?
if (name.matches("[0-9]+.*")) {
result = "rulesets/releases/" + name + ".xml";
} else {
// Appears to be a non-standard RuleSet name
result = name;
}
}
}
return result;
}
/**
* Extracts the rule name out of a ruleset path. E.g. for
* "/my/ruleset.xml/MyRule" it would return "MyRule". If no single rule is
* specified, <code>null</code> is returned.
*
* @param rulesetName
* the full rule set path
* @return the rule name or <code>null</code>.
*/
private String getRuleName(final String rulesetName) {
String result = null;
if (rulesetName != null) {
// Find last path separator if it exists... this might be a rule
// name
final int separatorIndex = Math.max(rulesetName.lastIndexOf('/'), rulesetName.lastIndexOf('\\'));
if (separatorIndex >= 0 && separatorIndex != rulesetName.length() - 1) {
result = rulesetName.substring(separatorIndex + 1);
}
}
return result;
}
private static boolean isHttpUrl(String name) {
String stripped = StringUtils.strip(name);
if (stripped == null) {
return false;
}
if (stripped.startsWith("http://") || stripped.startsWith("https://")) {
return true;
}
return false;
}
private static boolean isValidUrl(String name) {
if (isHttpUrl(name)) {
String url = StringUtils.strip(name);
try {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(ResourceLoader.TIMEOUT);
connection.setReadTimeout(ResourceLoader.TIMEOUT);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
return true;
}
} catch (IOException e) {
return false;
}
}
return false;
}
private static boolean isFullRuleSetName(String name) {
return name != null && name.endsWith(".xml");
}
/**
* Parse a String comma separated list of RuleSet reference IDs into a List
* of RuleReferenceId instances.
*
* @param referenceString
* A comma separated list of RuleSet reference IDs.
* @return The corresponding List of RuleSetReferenceId instances.
*/
public static List<RuleSetReferenceId> parse(String referenceString) {
List<RuleSetReferenceId> references = new ArrayList<>();
if (referenceString != null && referenceString.trim().length() > 0) {
if (referenceString.indexOf(',') == -1) {
references.add(new RuleSetReferenceId(referenceString));
} else {
for (String name : referenceString.split(",")) {
references.add(new RuleSetReferenceId(name.trim()));
}
}
}
return references;
}
/**
* Is this an external RuleSet reference?
*
* @return <code>true</code> if this is an external reference,
* <code>false</code> otherwise.
*/
public boolean isExternal() {
return external;
}
/**
* Is this a reference to all Rules in a RuleSet, or a single Rule?
*
* @return <code>true</code> if this is a reference to all Rules,
* <code>false</code> otherwise.
*/
public boolean isAllRules() {
return allRules;
}
/**
* Get the RuleSet file name.
*
* @return The RuleSet file name if this is an external reference,
* <code>null</code> otherwise.
*/
public String getRuleSetFileName() {
return ruleSetFileName;
}
/**
* Get the Rule name.
*
* @return The Rule name. The Rule name.
*/
public String getRuleName() {
return ruleName;
}
/**
* Try to load the RuleSet resource with the specified ClassLoader. Multiple
* attempts to get independent InputStream instances may be made, so
* subclasses must ensure they support this behavior. Delegates to an
* external RuleSetReferenceId if there is one associated with this
* instance.
*
* @param classLoader
* The ClassLoader to use.
* @return An InputStream to that resource.
* @throws RuleSetNotFoundException
* if unable to find a resource.
*/
public InputStream getInputStream(ClassLoader classLoader) throws RuleSetNotFoundException {
if (externalRuleSetReferenceId == null) {
InputStream in = StringUtil.isEmpty(ruleSetFileName) ? null
: ResourceLoader.loadResourceAsStream(ruleSetFileName, classLoader);
if (in == null) {
throw new RuleSetNotFoundException("Can't find resource '" + ruleSetFileName + "' for rule '" + ruleName
+ "'" + ". Make sure the resource is a valid file or URL and is on the CLASSPATH. "
+ "Here's the current classpath: " + System.getProperty("java.class.path"));
}
return in;
} else {
return externalRuleSetReferenceId.getInputStream(classLoader);
}
}
/**
* Return the String form of this Rule reference.
*
* @return Return the String form of this Rule reference, which is
* <i>ruleSetFileName</i> for all Rule external references,
* <i>ruleSetFileName/ruleName</i>, for a single Rule external
* references, or <i>ruleName</i> otherwise.
*/
@Override
public String toString() {
if (ruleSetFileName != null) {
if (allRules) {
return ruleSetFileName;
} else {
return ruleSetFileName + "/" + ruleName;
}
} else {
if (allRules) {
return "anonymous all Rule";
} else {
return ruleName;
}
}
}
}