001 /**
002 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
003 */
004 package net.sourceforge.pmd.lang.rule.xpath;
005
006 import java.util.ArrayList;
007 import java.util.HashMap;
008 import java.util.Iterator;
009 import java.util.List;
010 import java.util.Map;
011 import java.util.Stack;
012 import java.util.Map.Entry;
013 import java.util.logging.Level;
014 import java.util.logging.Logger;
015
016 import net.sourceforge.pmd.PropertyDescriptor;
017 import net.sourceforge.pmd.RuleContext;
018 import net.sourceforge.pmd.lang.ast.Node;
019
020 import org.jaxen.BaseXPath;
021 import org.jaxen.JaxenException;
022 import org.jaxen.Navigator;
023 import org.jaxen.SimpleVariableContext;
024 import org.jaxen.XPath;
025 import org.jaxen.expr.AllNodeStep;
026 import org.jaxen.expr.DefaultXPathFactory;
027 import org.jaxen.expr.Expr;
028 import org.jaxen.expr.LocationPath;
029 import org.jaxen.expr.NameStep;
030 import org.jaxen.expr.Predicate;
031 import org.jaxen.expr.Step;
032 import org.jaxen.expr.UnionExpr;
033 import org.jaxen.expr.XPathFactory;
034 import org.jaxen.saxpath.Axis;
035
036 /**
037 * This is a Jaxen based XPathRule query.
038 */
039 public class JaxenXPathRuleQuery extends AbstractXPathRuleQuery {
040
041 private static final Logger LOG = Logger.getLogger(JaxenXPathRuleQuery.class.getName());
042
043 private static enum InitializationStatus {
044 NONE, PARTIAL, FULL
045 };
046
047 // Mapping from Node name to applicable XPath queries
048 private InitializationStatus initializationStatus = InitializationStatus.NONE;
049 private Map<String, List<XPath>> nodeNameToXPaths;
050
051 private static final String AST_ROOT = "_AST_ROOT_";
052
053 /**
054 * {@inheritDoc}
055 */
056 @Override
057 public boolean isSupportedVersion(String version) {
058 return XPATH_1_0.equals(version);
059 }
060
061 /**
062 * {@inheritDoc}
063 */
064 @Override
065 @SuppressWarnings("unchecked")
066 public List<Node> evaluate(Node node, RuleContext data) {
067 List<Node> results = new ArrayList<Node>();
068 try {
069 initializeXPathExpression(data.getLanguageVersion().getLanguageVersionHandler().getXPathHandler()
070 .getNavigator());
071 List<XPath> xpaths = nodeNameToXPaths.get(node.toString());
072 if (xpaths == null) {
073 xpaths = nodeNameToXPaths.get(AST_ROOT);
074 }
075 for (XPath xpath : xpaths) {
076 List<Node> nodes = xpath.selectNodes(node);
077 results.addAll(nodes);
078 }
079 } catch (JaxenException ex) {
080 throw new RuntimeException(ex);
081 }
082 return results;
083 }
084
085 /**
086 * {@inheritDoc}
087 */
088 @Override
089 public List<String> getRuleChainVisits() {
090 try {
091 // No Navigator available in this context
092 initializeXPathExpression(null);
093 return super.getRuleChainVisits();
094 } catch (JaxenException ex) {
095 throw new RuntimeException(ex);
096 }
097 }
098
099 @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 BaseXPath xpath = new BaseXPath(xpathQueryString, navigator);
209 if (properties.size() > 1) {
210 SimpleVariableContext vc = new SimpleVariableContext();
211 for (Entry<PropertyDescriptor<?>, Object> e : properties.entrySet()) {
212 if (!"xpath".equals(e.getKey())) {
213 Object value = e.getValue();
214 vc.setVariableValue(e.getKey().name(), value != null ? value.toString() : null);
215 }
216 }
217 xpath.setVariableContext(vc);
218 }
219 return xpath;
220 }
221 }
|