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.Map.Entry;
12  import java.util.Stack;
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 	    return;
103 	} else if (initializationStatus == InitializationStatus.PARTIAL && navigator == null) {
104 	    LOG.severe("XPathRule is not initialized because no navigator was provided. "
105 	            + "Please make sure to implement getXPathHandler in the handler of the language. "
106 	            + "See also AbstractLanguageVersionHandler.");
107 	    return;
108 	}
109 
110 	//
111 	// Attempt to use the RuleChain with this XPath query.  To do so, the queries
112 	// should generally look like //TypeA or //TypeA | //TypeB.  We will look at the
113 	// parsed XPath AST using the Jaxen APIs to make this determination.
114 	// If the query is not exactly what we are looking for, do not use the RuleChain.
115 	//
116 	nodeNameToXPaths = new HashMap<String, List<XPath>>();
117 
118 	BaseXPath originalXPath = createXPath(xpath, navigator);
119 	indexXPath(originalXPath, AST_ROOT);
120 
121 	boolean useRuleChain = true;
122 	Stack<Expr> pending = new Stack<Expr>();
123 	pending.push(originalXPath.getRootExpr());
124 	while (!pending.isEmpty()) {
125 	    Expr node = pending.pop();
126 
127 	    // Need to prove we can handle this part of the query
128 	    boolean valid = false;
129 
130 	    // Must be a LocationPath... that is something like //Type
131 	    if (node instanceof LocationPath) {
132 		LocationPath locationPath = (LocationPath) node;
133 		if (locationPath.isAbsolute()) {
134 		    // Should be at least two steps
135 		    List<Step> steps = locationPath.getSteps();
136 		    if (steps.size() >= 2) {
137 			Step step1 = steps.get(0);
138 			Step step2 = steps.get(1);
139 			// First step should be an AllNodeStep using the descendant or self axis
140 			if (step1 instanceof AllNodeStep && ((AllNodeStep) step1).getAxis() == Axis.DESCENDANT_OR_SELF) {
141 			    // Second step should be a NameStep using the child axis.
142 			    if (step2 instanceof NameStep && ((NameStep) step2).getAxis() == Axis.CHILD) {
143 				// Construct a new expression that is appropriate for RuleChain use
144 				XPathFactory xpathFactory = new DefaultXPathFactory();
145 
146 				// Instead of an absolute location path, we'll be using a relative path
147 				LocationPath relativeLocationPath = xpathFactory.createRelativeLocationPath();
148 				// The first step will be along the self axis
149 				Step allNodeStep = xpathFactory.createAllNodeStep(Axis.SELF);
150 				// Retain all predicates from the original name step
151 				for (Iterator<Predicate> i = step2.getPredicates().iterator(); i.hasNext();) {
152 				    allNodeStep.addPredicate(i.next());
153 				}
154 				relativeLocationPath.addStep(allNodeStep);
155 
156 				// Retain the remaining steps from the original location path
157 				for (int i = 2; i < steps.size(); i++) {
158 				    relativeLocationPath.addStep(steps.get(i));
159 				}
160 
161 				BaseXPath xpath = createXPath(relativeLocationPath.getText(), navigator);
162 				indexXPath(xpath, ((NameStep) step2).getLocalName());
163 				valid = true;
164 			    }
165 			}
166 		    }
167 		}
168 	    } else if (node instanceof UnionExpr) { // Or a UnionExpr, that is something like //TypeA | //TypeB
169 		UnionExpr unionExpr = (UnionExpr) node;
170 		pending.push(unionExpr.getLHS());
171 		pending.push(unionExpr.getRHS());
172 		valid = true;
173 	    }
174 	    if (!valid) {
175 		useRuleChain = false;
176 		break;
177 	    }
178 	}
179 
180 	if (useRuleChain) {
181 	    // Use the RuleChain for all the nodes extracted from the xpath queries
182 	    super.ruleChainVisits.addAll(nodeNameToXPaths.keySet());
183 	} else {
184 	    // Use original XPath if we cannot use the RuleChain
185 	    nodeNameToXPaths.clear();
186 	    indexXPath(originalXPath, AST_ROOT);
187 	    if (LOG.isLoggable(Level.FINE)) {
188 		LOG.log(Level.FINE, "Unable to use RuleChain for for XPath: " + xpath);
189 	    }
190 	}
191 
192 	if (navigator == null) {
193 	    this.initializationStatus = InitializationStatus.PARTIAL;
194 	    // Clear the node data, because we did not have a Navigator
195 	    nodeNameToXPaths = null;
196 	} else {
197 	    this.initializationStatus = InitializationStatus.FULL;
198 	}
199 
200     }
201 
202     private void indexXPath(XPath xpath, String nodeName) {
203 	List<XPath> xpaths = nodeNameToXPaths.get(nodeName);
204 	if (xpaths == null) {
205 	    xpaths = new ArrayList<XPath>();
206 	    nodeNameToXPaths.put(nodeName, xpaths);
207 	}
208 	xpaths.add(xpath);
209     }
210 
211     private BaseXPath createXPath(String xpathQueryString, Navigator navigator) throws JaxenException {
212 
213     	BaseXPath xpath = new BaseXPath(xpathQueryString, navigator);
214     	if (properties.size() > 1) {
215     		SimpleVariableContext vc = new SimpleVariableContext();
216     		for (Entry<PropertyDescriptor<?>, Object> e : properties.entrySet()) {
217     			String propName = e.getKey().name();
218     			if (!"xpath".equals(propName)) {
219     				Object value = e.getValue();
220     				vc.setVariableValue(propName, value != null ? value.toString() : null);
221     			}
222     		}
223     		xpath.setVariableContext(vc);
224     	}
225     	return xpath;
226     }
227 }