/** * 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; } } } }