View Javadoc

    1   /**
    2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
    3    */
    4   package net.sourceforge.pmd.lang.rule.xpath;
    5
    6   import java.util.ArrayList;
    7   import java.util.HashMap;
    8   import java.util.Iterator;
    9   import java.util.List;
    10  import java.util.Map;
    11  import java.util.Stack;
    12  import java.util.Map.Entry;
    13  import java.util.logging.Level;
    14  import java.util.logging.Logger;
    15
    16  import net.sourceforge.pmd.PropertyDescriptor;
    17  import net.sourceforge.pmd.RuleContext;
    18  import net.sourceforge.pmd.lang.ast.Node;
    19
    20  import org.jaxen.BaseXPath;
    21  import org.jaxen.JaxenException;
    22  import org.jaxen.Navigator;
    23  import org.jaxen.SimpleVariableContext;
    24  import org.jaxen.XPath;
    25  import org.jaxen.expr.AllNodeStep;
    26  import org.jaxen.expr.DefaultXPathFactory;
    27  import org.jaxen.expr.Expr;
    28  import org.jaxen.expr.LocationPath;
    29  import org.jaxen.expr.NameStep;
    30  import org.jaxen.expr.Predicate;
    31  import org.jaxen.expr.Step;
    32  import org.jaxen.expr.UnionExpr;
    33  import org.jaxen.expr.XPathFactory;
    34  import org.jaxen.saxpath.Axis;
    35
    36  /**
    37   * This is a Jaxen based XPathRule query.
    38   */
    39  public class JaxenXPathRuleQuery extends AbstractXPathRuleQuery {
    40
    41      private static final Logger LOG = Logger.getLogger(JaxenXPathRuleQuery.class.getName());
    42
    43      private static enum InitializationStatus {
    44  	NONE, PARTIAL, FULL
    45      };
    46
    47      // Mapping from Node name to applicable XPath queries
    48      private InitializationStatus initializationStatus = InitializationStatus.NONE;
    49      private Map<String, List<XPath>> nodeNameToXPaths;
    50
    51      private static final String AST_ROOT = "_AST_ROOT_";
    52
    53      /**
    54       * {@inheritDoc}
    55       */
    56      @Override
    57      public boolean isSupportedVersion(String version) {
    58  	return XPATH_1_0.equals(version);
    59      }
    60
    61      /**
    62       * {@inheritDoc}
    63       */
    64      @Override
    65      @SuppressWarnings("unchecked")
    66      public List<Node> evaluate(Node node, RuleContext data) {
    67  	List<Node> results = new ArrayList<Node>();
    68  	try {
    69  	    initializeXPathExpression(data.getLanguageVersion().getLanguageVersionHandler().getXPathHandler()
    70  		    .getNavigator());
    71  	    List<XPath> xpaths = nodeNameToXPaths.get(node.toString());
    72  	    if (xpaths == null) {
    73  		xpaths = nodeNameToXPaths.get(AST_ROOT);
    74  	    }
    75  	    for (XPath xpath : xpaths) {
    76  		List<Node> nodes = xpath.selectNodes(node);
    77  		results.addAll(nodes);
    78  	    }
    79  	} catch (JaxenException ex) {
    80  	    throw new RuntimeException(ex);
    81  	}
    82  	return results;
    83      }
    84
    85      /**
    86       * {@inheritDoc}
    87       */
    88      @Override
    89      public List<String> getRuleChainVisits() {
    90  	try {
    91  	    // No Navigator available in this context
    92  	    initializeXPathExpression(null);
    93  	    return super.getRuleChainVisits();
    94  	} catch (JaxenException ex) {
    95  	    throw new RuntimeException(ex);
    96  	}
    97      }
    98
    99      @SuppressWarnings("unchecked")
    100     private void initializeXPathExpression(Navigator navigator) throws JaxenException {
    101 	if (initializationStatus == InitializationStatus.FULL
    102 		|| (initializationStatus == InitializationStatus.PARTIAL && navigator == null)) {
    103 	    return;
    104 	}
    105
    106 	//
    107 	// Attempt to use the RuleChain with this XPath query.  To do so, the queries
    108 	// should generally look like //TypeA or //TypeA | //TypeB.  We will look at the
    109 	// parsed XPath AST using the Jaxen APIs to make this determination.
    110 	// If the query is not exactly what we are looking for, do not use the RuleChain.
    111 	//
    112 	nodeNameToXPaths = new HashMap<String, List<XPath>>();
    113
    114 	BaseXPath originalXPath = createXPath(xpath, navigator);
    115 	indexXPath(originalXPath, AST_ROOT);
    116
    117 	boolean useRuleChain = true;
    118 	Stack<Expr> pending = new Stack<Expr>();
    119 	pending.push(originalXPath.getRootExpr());
    120 	while (!pending.isEmpty()) {
    121 	    Expr node = pending.pop();
    122
    123 	    // Need to prove we can handle this part of the query
    124 	    boolean valid = false;
    125
    126 	    // Must be a LocationPath... that is something like //Type
    127 	    if (node instanceof LocationPath) {
    128 		LocationPath locationPath = (LocationPath) node;
    129 		if (locationPath.isAbsolute()) {
    130 		    // Should be at least two steps
    131 		    List<Step> steps = locationPath.getSteps();
    132 		    if (steps.size() >= 2) {
    133 			Step step1 = steps.get(0);
    134 			Step step2 = steps.get(1);
    135 			// First step should be an AllNodeStep using the descendant or self axis
    136 			if (step1 instanceof AllNodeStep && ((AllNodeStep) step1).getAxis() == Axis.DESCENDANT_OR_SELF) {
    137 			    // Second step should be a NameStep using the child axis.
    138 			    if (step2 instanceof NameStep && ((NameStep) step2).getAxis() == Axis.CHILD) {
    139 				// Construct a new expression that is appropriate for RuleChain use
    140 				XPathFactory xpathFactory = new DefaultXPathFactory();
    141
    142 				// Instead of an absolute location path, we'll be using a relative path
    143 				LocationPath relativeLocationPath = xpathFactory.createRelativeLocationPath();
    144 				// The first step will be along the self axis
    145 				Step allNodeStep = xpathFactory.createAllNodeStep(Axis.SELF);
    146 				// Retain all predicates from the original name step
    147 				for (Iterator<Predicate> i = step2.getPredicates().iterator(); i.hasNext();) {
    148 				    allNodeStep.addPredicate(i.next());
    149 				}
    150 				relativeLocationPath.addStep(allNodeStep);
    151
    152 				// Retain the remaining steps from the original location path
    153 				for (int i = 2; i < steps.size(); i++) {
    154 				    relativeLocationPath.addStep(steps.get(i));
    155 				}
    156
    157 				BaseXPath xpath = createXPath(relativeLocationPath.getText(), navigator);
    158 				indexXPath(xpath, ((NameStep) step2).getLocalName());
    159 				valid = true;
    160 			    }
    161 			}
    162 		    }
    163 		}
    164 	    } else if (node instanceof UnionExpr) { // Or a UnionExpr, that is something like //TypeA | //TypeB
    165 		UnionExpr unionExpr = (UnionExpr) node;
    166 		pending.push(unionExpr.getLHS());
    167 		pending.push(unionExpr.getRHS());
    168 		valid = true;
    169 	    }
    170 	    if (!valid) {
    171 		useRuleChain = false;
    172 		break;
    173 	    }
    174 	}
    175
    176 	if (useRuleChain) {
    177 	    // Use the RuleChain for all the nodes extracted from the xpath queries
    178 	    super.ruleChainVisits.addAll(nodeNameToXPaths.keySet());
    179 	} else {
    180 	    // Use original XPath if we cannot use the RuleChain
    181 	    nodeNameToXPaths.clear();
    182 	    indexXPath(originalXPath, AST_ROOT);
    183 	    if (LOG.isLoggable(Level.FINE)) {
    184 		LOG.log(Level.FINE, "Unable to use RuleChain for for XPath: " + xpath);
    185 	    }
    186 	}
    187
    188 	if (navigator == null) {
    189 	    this.initializationStatus = InitializationStatus.PARTIAL;
    190 	    // Clear the node data, because we did not have a Navigator
    191 	    nodeNameToXPaths = null;
    192 	} else {
    193 	    this.initializationStatus = InitializationStatus.FULL;
    194 	}
    195
    196     }
    197
    198     private void indexXPath(XPath xpath, String nodeName) {
    199 	List<XPath> xpaths = nodeNameToXPaths.get(nodeName);
    200 	if (xpaths == null) {
    201 	    xpaths = new ArrayList<XPath>();
    202 	    nodeNameToXPaths.put(nodeName, xpaths);
    203 	}
    204 	xpaths.add(xpath);
    205     }
    206
    207     private BaseXPath createXPath(String xpathQueryString, Navigator navigator) throws JaxenException {
    208
    209     	BaseXPath xpath = new BaseXPath(xpathQueryString, navigator);
    210     	if (properties.size() > 1) {
    211     		SimpleVariableContext vc = new SimpleVariableContext();
    212     		for (Entry<PropertyDescriptor<?>, Object> e : properties.entrySet()) {
    213     			String propName = e.getKey().name();
    214     			if (!"xpath".equals(propName)) {
    215     				Object value = e.getValue();
    216     				vc.setVariableValue(propName, value != null ? value.toString() : null);
    217     			}
    218     		}
    219     		xpath.setVariableContext(vc);
    220     	}
    221     	return xpath;
    222     }
    223 }