Java tutorial
/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.rules; import static net.sourceforge.pmd.properties.PropertyDescriptorField.DEFAULT_VALUE; import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang3.StringUtils; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSetReference; import net.sourceforge.pmd.lang.rule.RuleReference; import net.sourceforge.pmd.properties.PropertyDescriptor; import net.sourceforge.pmd.properties.PropertyDescriptorField; import net.sourceforge.pmd.properties.PropertyTypeId; import net.sourceforge.pmd.properties.builders.PropertyDescriptorExternalBuilder; /** * Builds rules from rule XML nodes. * * @author Clment Fournier * @since 6.0.0 */ public class RuleFactory { private static final Logger LOG = Logger.getLogger(RuleFactory.class.getName()); private static final String DEPRECATED = "deprecated"; private static final String NAME = "name"; private static final String MESSAGE = "message"; private static final String EXTERNAL_INFO_URL = "externalInfoUrl"; private static final String MINIMUM_LANGUAGE_VERSION = "minimumLanguageVersion"; private static final String MAXIMUM_LANGUAGE_VERSION = "maximumLanguageVersion"; private static final String SINCE = "since"; private static final String PROPERTIES = "properties"; private static final String PRIORITY = "priority"; private static final String EXAMPLE = "example"; private static final String DESCRIPTION = "description"; private static final String PROPERTY = "property"; private static final String CLASS = "class"; private static final List<String> REQUIRED_ATTRIBUTES = Collections .unmodifiableList(Arrays.asList(NAME, CLASS)); /** * Decorates a referenced rule with the metadata that are overridden in the given rule element. * * <p>Declaring a property in the overriding element throws an exception (the property must exist in the referenced * rule). * * @param referencedRule Referenced rule * @param ruleSetReference the ruleset, where the referenced rule is defined * @param ruleElement Element overriding some metadata about the rule * * @return A rule reference to the referenced rule */ public RuleReference decorateRule(Rule referencedRule, RuleSetReference ruleSetReference, Element ruleElement) { RuleReference ruleReference = new RuleReference(referencedRule, ruleSetReference); if (ruleElement.hasAttribute(DEPRECATED)) { ruleReference.setDeprecated(Boolean.parseBoolean(ruleElement.getAttribute(DEPRECATED))); } if (ruleElement.hasAttribute(NAME)) { ruleReference.setName(ruleElement.getAttribute(NAME)); } if (ruleElement.hasAttribute(MESSAGE)) { ruleReference.setMessage(ruleElement.getAttribute(MESSAGE)); } if (ruleElement.hasAttribute(EXTERNAL_INFO_URL)) { ruleReference.setExternalInfoUrl(ruleElement.getAttribute(EXTERNAL_INFO_URL)); } for (int i = 0; i < ruleElement.getChildNodes().getLength(); i++) { Node node = ruleElement.getChildNodes().item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { switch (node.getNodeName()) { case DESCRIPTION: ruleReference.setDescription(parseTextNode(node)); break; case EXAMPLE: ruleReference.addExample(parseTextNode(node)); break; case PRIORITY: ruleReference.setPriority(RulePriority.valueOf(Integer.parseInt(parseTextNode(node)))); break; case PROPERTIES: setPropertyValues(ruleReference, (Element) node); break; default: throw new IllegalArgumentException("Unexpected element <" + node.getNodeName() + "> encountered as child of <rule> element for Rule " + ruleReference.getName()); } } } return ruleReference; } /** * Parses a rule element and returns a new rule instance. * * <p>Notes: The ruleset name is not set here. Exceptions raised from this method indicate invalid XML structure, * with regards to the expected schema, while RuleBuilder validates the semantics. * * @param ruleElement The rule element to parse * * @return A new instance of the rule described by this element * @throws IllegalArgumentException if the element doesn't describe a valid rule. */ public Rule buildRule(Element ruleElement) { checkRequiredAttributesArePresent(ruleElement); String name = ruleElement.getAttribute(NAME); RuleBuilder builder = new RuleBuilder(name, ruleElement.getAttribute(CLASS), ruleElement.getAttribute("language")); if (ruleElement.hasAttribute(MINIMUM_LANGUAGE_VERSION)) { builder.minimumLanguageVersion(ruleElement.getAttribute(MINIMUM_LANGUAGE_VERSION)); } if (ruleElement.hasAttribute(MAXIMUM_LANGUAGE_VERSION)) { builder.maximumLanguageVersion(ruleElement.getAttribute(MAXIMUM_LANGUAGE_VERSION)); } if (ruleElement.hasAttribute(SINCE)) { builder.since(ruleElement.getAttribute(SINCE)); } builder.message(ruleElement.getAttribute(MESSAGE)); builder.externalInfoUrl(ruleElement.getAttribute(EXTERNAL_INFO_URL)); builder.setDeprecated(hasAttributeSetTrue(ruleElement, DEPRECATED)); builder.usesDFA(hasAttributeSetTrue(ruleElement, "dfa")); builder.usesTyperesolution(hasAttributeSetTrue(ruleElement, "typeResolution")); // Disabled until it's safe // builder.usesMultifile(hasAttributeSetTrue(ruleElement, "multifile")); Element propertiesElement = null; final NodeList nodeList = ruleElement.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() != Node.ELEMENT_NODE) { continue; } switch (node.getNodeName()) { case DESCRIPTION: builder.description(parseTextNode(node)); break; case EXAMPLE: builder.addExample(parseTextNode(node)); break; case PRIORITY: builder.priority(Integer.parseInt(parseTextNode(node).trim())); break; case PROPERTIES: parsePropertiesForDefinitions(builder, node); propertiesElement = (Element) node; break; default: throw new IllegalArgumentException("Unexpected element <" + node.getNodeName() + "> encountered as child of <rule> element for Rule " + name); } } Rule rule; try { rule = builder.build(); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { LOG.log(Level.SEVERE, "Error instantiating a rule", e); throw new RuntimeException(e); } if (propertiesElement != null) { setPropertyValues(rule, propertiesElement); } return rule; } private void checkRequiredAttributesArePresent(Element ruleElement) { // add an attribute name here to make it required for (String att : REQUIRED_ATTRIBUTES) { if (!ruleElement.hasAttribute(att)) { throw new IllegalArgumentException("Missing '" + att + "' attribute"); } } } /** * Parses a properties element looking only for the values of the properties defined or overridden. * * @param propertiesNode Node to parse * * @return A map of property names to their value */ private Map<String, String> getPropertyValuesFrom(Element propertiesNode) { Map<String, String> overridenProperties = new HashMap<>(); for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) { Node node = propertiesNode.getChildNodes().item(i); if (node.getNodeType() == Node.ELEMENT_NODE && PROPERTY.equals(node.getNodeName())) { Entry<String, String> overridden = getPropertyValue((Element) node); overridenProperties.put(overridden.getKey(), overridden.getValue()); } } return overridenProperties; } /** * Parses the properties node and adds property definitions to the builder. Doesn't care for value overriding, that * will be handled after the rule instantiation. * * @param builder Rule builder * @param propertiesNode Node to parse */ private void parsePropertiesForDefinitions(RuleBuilder builder, Node propertiesNode) { for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) { Node node = propertiesNode.getChildNodes().item(i); if (node.getNodeType() == Node.ELEMENT_NODE && PROPERTY.equals(node.getNodeName()) && isPropertyDefinition((Element) node)) { PropertyDescriptor<?> descriptor = parsePropertyDefinition((Element) node); builder.defineProperty(descriptor); } } } /** * Gets a mapping of property name to its value from the given property element. * * @param propertyElement Property element * * @return An entry of property name to its value */ private Entry<String, String> getPropertyValue(Element propertyElement) { String name = propertyElement.getAttribute(PropertyDescriptorField.NAME.attributeName()); return new SimpleEntry<>(name, valueFrom(propertyElement)); } /** * Overrides the rule's properties with the values defined in the element. * * @param rule The rule * @param propertiesElt The {@literal <properties>} element */ private void setPropertyValues(Rule rule, Element propertiesElt) { Map<String, String> overridden = getPropertyValuesFrom(propertiesElt); for (Entry<String, String> e : overridden.entrySet()) { PropertyDescriptor<?> descriptor = rule.getPropertyDescriptor(e.getKey()); if (descriptor == null) { throw new IllegalArgumentException( "Cannot set non-existent property '" + e.getKey() + "' on Rule " + rule.getName()); } setRulePropertyCapture(rule, descriptor, e.getValue()); } } private <T> void setRulePropertyCapture(Rule rule, PropertyDescriptor<T> descriptor, String value) { rule.setProperty(descriptor, descriptor.valueFrom(value)); } /** * Finds out if the property element defines a property. * * @param node Property element * * @return True if this element defines a new property, false if this is just stating a value */ private static boolean isPropertyDefinition(Element node) { return node.hasAttribute(PropertyDescriptorField.TYPE.attributeName()); } /** * Parses a property definition node and returns the defined property descriptor. * * @param propertyElement Property node to parse * * @return The property descriptor */ private static PropertyDescriptor<?> parsePropertyDefinition(Element propertyElement) { String typeId = propertyElement.getAttribute(PropertyDescriptorField.TYPE.attributeName()); PropertyDescriptorExternalBuilder<?> pdFactory = PropertyTypeId.factoryFor(typeId); if (pdFactory == null) { throw new IllegalArgumentException("No property descriptor factory for type: " + typeId); } Map<PropertyDescriptorField, String> values = new HashMap<>(); NamedNodeMap atts = propertyElement.getAttributes(); /// populate a map of values for an individual descriptor for (int i = 0; i < atts.getLength(); i++) { Attr a = (Attr) atts.item(i); values.put(PropertyDescriptorField.getConstant(a.getName()), a.getValue()); } if (StringUtils.isBlank(values.get(DEFAULT_VALUE))) { NodeList children = propertyElement.getElementsByTagName(DEFAULT_VALUE.attributeName()); if (children.getLength() == 1) { values.put(DEFAULT_VALUE, children.item(0).getTextContent()); } else { throw new IllegalArgumentException("No value defined!"); } } // casting is not pretty but prevents the interface from having this method return pdFactory.build(values); } /** Gets the string value from a property node. */ private static String valueFrom(Element propertyNode) { String strValue = propertyNode.getAttribute(DEFAULT_VALUE.attributeName()); if (StringUtils.isNotBlank(strValue)) { return strValue; } final NodeList nodeList = propertyNode.getChildNodes(); for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (node.getNodeType() == Node.ELEMENT_NODE && "value".equals(node.getNodeName())) { return parseTextNode(node); } } return null; } private static boolean hasAttributeSetTrue(Element element, String attributeId) { return element.hasAttribute(attributeId) && "true".equalsIgnoreCase(element.getAttribute(attributeId)); } /** * Parse a String from a textually type node. * * @param node The node. * * @return The String. */ private static String parseTextNode(Node node) { final int nodeCount = node.getChildNodes().getLength(); if (nodeCount == 0) { return ""; } StringBuilder buffer = new StringBuilder(); for (int i = 0; i < nodeCount; i++) { Node childNode = node.getChildNodes().item(i); if (childNode.getNodeType() == Node.CDATA_SECTION_NODE || childNode.getNodeType() == Node.TEXT_NODE) { buffer.append(childNode.getNodeValue()); } } return buffer.toString(); } }