JaxenXPathRuleQuery.java
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 navigatorthrows 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 = (LocationPathnode;
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 && ((AllNodeStepstep1).getAxis() == Axis.DESCENDANT_OR_SELF) {
137           // Second step should be a NameStep using the child axis.
138           if (step2 instanceof NameStep && ((NameStepstep2).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, ((NameStepstep2).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 = (UnionExprnode;
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 navigatorthrows 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 }