/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.rule.xpath;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.sourceforge.pmd.PropertyDescriptor;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.ast.xpath.saxon.DocumentNode;
import net.sourceforge.pmd.lang.ast.xpath.saxon.ElementNode;
import net.sourceforge.pmd.lang.rule.properties.BooleanProperty;
import net.sourceforge.pmd.lang.rule.properties.EnumeratedProperty;
import net.sourceforge.pmd.lang.rule.properties.IntegerProperty;
import net.sourceforge.pmd.lang.rule.properties.PropertyDescriptorWrapper;
import net.sourceforge.pmd.lang.rule.properties.StringProperty;
import net.sourceforge.pmd.lang.xpath.Initializer;
import net.sf.saxon.om.ValueRepresentation;
import net.sf.saxon.sxpath.AbstractStaticContext;
import net.sf.saxon.sxpath.IndependentContext;
import net.sf.saxon.sxpath.XPathDynamicContext;
import net.sf.saxon.sxpath.XPathEvaluator;
import net.sf.saxon.sxpath.XPathExpression;
import net.sf.saxon.sxpath.XPathStaticContext;
import net.sf.saxon.sxpath.XPathVariable;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.value.BooleanValue;
import net.sf.saxon.value.Int64Value;
import net.sf.saxon.value.StringValue;
/**
* This is a Saxon based XPathRule query.
*/
public class SaxonXPathRuleQuery extends AbstractXPathRuleQuery {
// Mapping from Node name to applicable XPath queries
private XPathExpression xpathExpression;
private List<XPathVariable> xpathVariables;
private static final int MAX_CACHE_SIZE = 20;
private static final Map<Node, DocumentNode> CACHE = new LinkedHashMap<Node, DocumentNode>(MAX_CACHE_SIZE) {
private static final long serialVersionUID = -7653916493967142443L;
protected boolean removeEldestEntry(final Map.Entry<Node, DocumentNode> eldest) {
return size() > MAX_CACHE_SIZE;
}
};
/**
* {@inheritDoc}
*/
@Override
public boolean isSupportedVersion(String version) {
return XPATH_1_0_COMPATIBILITY.equals(version) || XPATH_2_0.equals(version);
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public List<Node> evaluate(Node node, RuleContext data) {
initializeXPathExpression();
List<Node> results = new ArrayList<>();
try {
// Get the DocumentNode for the AST
DocumentNode documentNode = getDocumentNode(node);
// Get the corresponding ElementNode for this node.
ElementNode rootElementNode = documentNode.nodeToElementNode.get(node);
// Create a dynamic context for this node
XPathDynamicContext xpathDynamicContext = xpathExpression.createDynamicContext(rootElementNode);
// Set variable values on the dynamic context
for (XPathVariable xpathVariable : xpathVariables) {
String name = xpathVariable.getVariableQName().getLocalName();
for (Map.Entry<PropertyDescriptor<?>, Object> entry : super.properties.entrySet()) {
if (name.equals(entry.getKey().name())) {
PropertyDescriptor<?> propertyDescriptor = entry.getKey();
if (propertyDescriptor instanceof PropertyDescriptorWrapper) {
propertyDescriptor = ((PropertyDescriptorWrapper) propertyDescriptor)
.getPropertyDescriptor();
}
Object value = entry.getValue();
ValueRepresentation valueRepresentation;
// TODO Need to handle null values?
// TODO Need to handle more PropertyDescriptors, is
// there an easy factory in Saxon we can use for this?
if (propertyDescriptor instanceof StringProperty) {
valueRepresentation = new StringValue((String) value);
} else if (propertyDescriptor instanceof BooleanProperty) {
valueRepresentation = BooleanValue.get(((Boolean) value).booleanValue());
} else if (propertyDescriptor instanceof IntegerProperty) {
valueRepresentation = Int64Value.makeIntegerValue((Integer) value);
} else if (propertyDescriptor instanceof EnumeratedProperty) {
if (value instanceof String) {
valueRepresentation = new StringValue((String) value);
} else {
throw new RuntimeException(
"Unable to create ValueRepresentaton for non-String EnumeratedProperty value: "
+ value);
}
} else {
throw new RuntimeException("Unable to create ValueRepresentaton for PropertyDescriptor: "
+ propertyDescriptor);
}
xpathDynamicContext.setVariable(xpathVariable, valueRepresentation);
}
}
}
List<ElementNode> nodes = xpathExpression.evaluate(xpathDynamicContext);
for (ElementNode elementNode : nodes) {
results.add((Node) elementNode.getUnderlyingNode());
}
} catch (XPathException e) {
throw new RuntimeException(super.xpath + " had problem: " + e.getMessage(), e);
}
return results;
}
private DocumentNode getDocumentNode(Node node) {
// Get the root AST node
Node root = node;
while (root.jjtGetParent() != null) {
root = root.jjtGetParent();
}
// Cache DocumentNode trees, so that different XPath queries can re-use them.
DocumentNode documentNode;
synchronized (CACHE) {
documentNode = CACHE.get(root);
if (documentNode == null) {
documentNode = new DocumentNode(root);
CACHE.put(root, documentNode);
}
}
return documentNode;
}
private void initializeXPathExpression() {
if (xpathExpression != null) {
return;
}
try {
XPathEvaluator xpathEvaluator = new XPathEvaluator();
XPathStaticContext xpathStaticContext = xpathEvaluator.getStaticContext();
// Enable XPath 1.0 compatibility
if (XPATH_1_0_COMPATIBILITY.equals(version)) {
((AbstractStaticContext) xpathStaticContext).setBackwardsCompatibilityMode(true);
}
// Register PMD functions
Initializer.initialize((IndependentContext) xpathStaticContext);
// Create XPathVariables for later use. It is a Saxon quirk that
// XPathVariables must be defined on the static context, and
// reused later to associate an actual value on the dynamic context.
xpathVariables = new ArrayList<>();
for (PropertyDescriptor<?> propertyDescriptor : super.properties.keySet()) {
String name = propertyDescriptor.name();
if (!"xpath".equals(name)) {
XPathVariable xpathVariable = xpathStaticContext.declareVariable(null, name);
xpathVariables.add(xpathVariable);
}
}
// TODO Come up with a way to make use of RuleChain. I had hacked up
// an approach which used Jaxen's stuff, but that only works for
// 1.0 compatibility mode. Rather do it right instead of kludging.
xpathExpression = xpathEvaluator.createExpression(super.xpath);
} catch (XPathException e) {
throw new RuntimeException(e);
}
}
}