/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.rule.xpath; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.Iterator; 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.jaxen.BaseXPath; import org.jaxen.JaxenException; import org.jaxen.Navigator; import org.jaxen.SimpleVariableContext; import org.jaxen.XPath; import org.jaxen.expr.AllNodeStep; import org.jaxen.expr.DefaultXPathFactory; import org.jaxen.expr.Expr; import org.jaxen.expr.LocationPath; import org.jaxen.expr.NameStep; import org.jaxen.expr.Predicate; import org.jaxen.expr.Step; import org.jaxen.expr.UnionExpr; import org.jaxen.expr.XPathFactory; import org.jaxen.saxpath.Axis; import net.sourceforge.pmd.PropertyDescriptor; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.lang.ast.Node; /** * This is a Jaxen based XPathRule query. */ public class JaxenXPathRuleQuery extends AbstractXPathRuleQuery { private static final Logger LOG = Logger.getLogger(JaxenXPathRuleQuery.class.getName()); private enum InitializationStatus { NONE, PARTIAL, FULL } // Mapping from Node name to applicable XPath queries private InitializationStatus initializationStatus = InitializationStatus.NONE; private Map<String, List<XPath>> nodeNameToXPaths; private static final String AST_ROOT = "_AST_ROOT_"; /** * {@inheritDoc} */ @Override public boolean isSupportedVersion(String version) { return XPATH_1_0.equals(version); } /** * {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public List<Node> evaluate(Node node, RuleContext data) { List<Node> results = new ArrayList<>(); try { initializeXPathExpression( data.getLanguageVersion().getLanguageVersionHandler().getXPathHandler().getNavigator()); List<XPath> xpaths = nodeNameToXPaths.get(node.toString()); if (xpaths == null) { xpaths = nodeNameToXPaths.get(AST_ROOT); } for (XPath xpath : xpaths) { List<Node> nodes = xpath.selectNodes(node); results.addAll(nodes); } } catch (JaxenException ex) { throw new RuntimeException(ex); } return results; } /** * {@inheritDoc} */ @Override public List<String> getRuleChainVisits() { try { // No Navigator available in this context initializeXPathExpression(null); return super.getRuleChainVisits(); } catch (JaxenException ex) { throw new RuntimeException(ex); } } @SuppressWarnings("unchecked") private void initializeXPathExpression(Navigator navigator) throws JaxenException { if (initializationStatus == InitializationStatus.FULL) { return; } else if (initializationStatus == InitializationStatus.PARTIAL && navigator == null) { if (LOG.isLoggable(Level.SEVERE)) { LOG.severe("XPathRule is not initialized because no navigator was provided. " + "Please make sure to implement getXPathHandler in the handler of the language. " + "See also AbstractLanguageVersionHandler."); } return; } // // Attempt to use the RuleChain with this XPath query. To do so, the // queries // should generally look like //TypeA or //TypeA | //TypeB. We will look // at the // parsed XPath AST using the Jaxen APIs to make this determination. // If the query is not exactly what we are looking for, do not use the // RuleChain. // nodeNameToXPaths = new HashMap<>(); BaseXPath originalXPath = createXPath(xpath, navigator); indexXPath(originalXPath, AST_ROOT); boolean useRuleChain = true; Deque<Expr> pending = new ArrayDeque<>(); pending.push(originalXPath.getRootExpr()); while (!pending.isEmpty()) { Expr node = pending.pop(); // Need to prove we can handle this part of the query boolean valid = false; // Must be a LocationPath... that is something like //Type if (node instanceof LocationPath) { LocationPath locationPath = (LocationPath) node; if (locationPath.isAbsolute()) { // Should be at least two steps List<Step> steps = locationPath.getSteps(); if (steps.size() >= 2) { Step step1 = steps.get(0); Step step2 = steps.get(1); // First step should be an AllNodeStep using the // descendant or self axis if (step1 instanceof AllNodeStep && ((AllNodeStep) step1).getAxis() == Axis.DESCENDANT_OR_SELF) { // Second step should be a NameStep using the child // axis. if (step2 instanceof NameStep && ((NameStep) step2).getAxis() == Axis.CHILD) { // Construct a new expression that is // appropriate for RuleChain use XPathFactory xpathFactory = new DefaultXPathFactory(); // Instead of an absolute location path, we'll // be using a relative path LocationPath relativeLocationPath = xpathFactory.createRelativeLocationPath(); // The first step will be along the self axis Step allNodeStep = xpathFactory.createAllNodeStep(Axis.SELF); // Retain all predicates from the original name // step for (Iterator<Predicate> i = step2.getPredicates().iterator(); i.hasNext();) { allNodeStep.addPredicate(i.next()); } relativeLocationPath.addStep(allNodeStep); // Retain the remaining steps from the original // location path for (int i = 2; i < steps.size(); i++) { relativeLocationPath.addStep(steps.get(i)); } BaseXPath xpath = createXPath(relativeLocationPath.getText(), navigator); indexXPath(xpath, ((NameStep) step2).getLocalName()); valid = true; } } } } } else if (node instanceof UnionExpr) { // Or a UnionExpr, that is // something like //TypeA | // //TypeB UnionExpr unionExpr = (UnionExpr) node; pending.push(unionExpr.getLHS()); pending.push(unionExpr.getRHS()); valid = true; } if (!valid) { useRuleChain = false; break; } } if (useRuleChain) { // Use the RuleChain for all the nodes extracted from the xpath // queries super.ruleChainVisits.addAll(nodeNameToXPaths.keySet()); } else { // Use original XPath if we cannot use the RuleChain nodeNameToXPaths.clear(); indexXPath(originalXPath, AST_ROOT); if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Unable to use RuleChain for for XPath: " + xpath); } } if (navigator == null) { this.initializationStatus = InitializationStatus.PARTIAL; // Clear the node data, because we did not have a Navigator nodeNameToXPaths = null; } else { this.initializationStatus = InitializationStatus.FULL; } } private void indexXPath(XPath xpath, String nodeName) { List<XPath> xpaths = nodeNameToXPaths.get(nodeName); if (xpaths == null) { xpaths = new ArrayList<>(); nodeNameToXPaths.put(nodeName, xpaths); } xpaths.add(xpath); } private BaseXPath createXPath(String xpathQueryString, Navigator navigator) throws JaxenException { BaseXPath xpath = new BaseXPath(xpathQueryString, navigator); if (properties.size() > 1) { SimpleVariableContext vc = new SimpleVariableContext(); for (Entry<PropertyDescriptor<?>, Object> e : properties.entrySet()) { String propName = e.getKey().name(); if (!"xpath".equals(propName)) { Object value = e.getValue(); vc.setVariableValue(propName, value != null ? value.toString() : null); } } xpath.setVariableContext(vc); } return xpath; } }