536cd135cc
Former-commit-id: 5624ac747d633e885131e8349322922b6a59baaa
404 lines
19 KiB
C#
404 lines
19 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="XPathPatternBuilder.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
// <owner current="true" primary="true">Microsoft</owner>
|
|
//------------------------------------------------------------------------------
|
|
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Xml.XPath;
|
|
using System.Xml.Schema;
|
|
using System.Xml.Xsl.Qil;
|
|
using System.Xml.Xsl.XPath;
|
|
|
|
namespace System.Xml.Xsl.Xslt {
|
|
using T = XmlQueryTypeFactory;
|
|
|
|
internal class XPathPatternBuilder : XPathPatternParser.IPatternBuilder {
|
|
private XPathPredicateEnvironment predicateEnvironment;
|
|
private XPathBuilder predicateBuilder;
|
|
private bool inTheBuild;
|
|
private XPathQilFactory f;
|
|
private QilNode fixupNode;
|
|
private IXPathEnvironment environment;
|
|
|
|
public XPathPatternBuilder(IXPathEnvironment environment) {
|
|
Debug.Assert(environment != null);
|
|
this.environment = environment;
|
|
this.f = environment.Factory;
|
|
this.predicateEnvironment = new XPathPredicateEnvironment(environment);
|
|
this.predicateBuilder = new XPathBuilder(predicateEnvironment);
|
|
|
|
this.fixupNode = f.Unknown(T.NodeNotRtfS);
|
|
}
|
|
|
|
public QilNode FixupNode {
|
|
get { return fixupNode; }
|
|
}
|
|
|
|
public virtual void StartBuild() {
|
|
Debug.Assert(! inTheBuild, "XPathBuilder is buisy!");
|
|
inTheBuild = true;
|
|
return;
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
public void AssertFilter(QilLoop filter) {
|
|
Debug.Assert(filter.NodeType == QilNodeType.Filter, "XPathPatternBuilder expected to generate list of Filters on top level");
|
|
Debug.Assert(filter.Variable.XmlType.IsSubtypeOf(T.NodeNotRtf));
|
|
Debug.Assert(filter.Variable.Binding.NodeType == QilNodeType.Unknown); // fixupNode
|
|
Debug.Assert(filter.Body.XmlType.IsSubtypeOf(T.Boolean));
|
|
}
|
|
|
|
private void FixupFilterBinding(QilLoop filter, QilNode newBinding) {
|
|
AssertFilter(filter);
|
|
filter.Variable.Binding = newBinding;
|
|
}
|
|
|
|
public virtual QilNode EndBuild(QilNode result) {
|
|
Debug.Assert(inTheBuild, "StartBuild() wasn't called");
|
|
if (result == null) {
|
|
// Special door to clean builder state in exception handlers
|
|
}
|
|
|
|
// All these variables will be positive for "false() and (. = position() + last())"
|
|
// since QilPatternFactory eliminates the right operand of 'and'
|
|
Debug.Assert(predicateEnvironment.numFixupCurrent >= 0, "Context fixup error");
|
|
Debug.Assert(predicateEnvironment.numFixupPosition >= 0, "Context fixup error");
|
|
Debug.Assert(predicateEnvironment.numFixupLast >= 0, "Context fixup error");
|
|
inTheBuild = false;
|
|
return result;
|
|
}
|
|
|
|
public QilNode Operator(XPathOperator op, QilNode left, QilNode right) {
|
|
Debug.Assert(op == XPathOperator.Union);
|
|
Debug.Assert(left != null);
|
|
Debug.Assert(right != null);
|
|
// It is important to not create nested lists here
|
|
Debug.Assert(right.NodeType == QilNodeType.Filter, "LocationPathPattern must be compiled into a filter");
|
|
if (left.NodeType == QilNodeType.Sequence) {
|
|
((QilList)left).Add(right);
|
|
return left;
|
|
} else {
|
|
Debug.Assert(left.NodeType == QilNodeType.Filter, "LocationPathPattern must be compiled into a filter");
|
|
return f.Sequence(left, right);
|
|
}
|
|
}
|
|
|
|
private static QilLoop BuildAxisFilter(QilPatternFactory f, QilIterator itr, XPathAxis xpathAxis, XPathNodeType nodeType, string name, string nsUri) {
|
|
QilNode nameTest = (
|
|
name != null && nsUri != null ? f.Eq(f.NameOf(itr), f.QName(name, nsUri)) : // ns:bar || bar
|
|
nsUri != null ? f.Eq(f.NamespaceUriOf(itr), f.String(nsUri)) : // ns:*
|
|
name != null ? f.Eq(f.LocalNameOf(itr), f.String(name)) : // *:foo
|
|
/*name == nsUri == null*/ f.True() // *
|
|
);
|
|
|
|
XmlNodeKindFlags intersection = XPathBuilder.AxisTypeMask(itr.XmlType.NodeKinds, nodeType, xpathAxis);
|
|
|
|
QilNode typeTest = (
|
|
intersection == 0 ? f.False() : // input & required doesn't intersect
|
|
intersection == itr.XmlType.NodeKinds ? f.True() : // input is subset of required
|
|
/*else*/ f.IsType(itr, T.NodeChoice(intersection))
|
|
);
|
|
|
|
QilLoop filter = f.BaseFactory.Filter(itr, f.And(typeTest, nameTest));
|
|
filter.XmlType = T.PrimeProduct(T.NodeChoice(intersection), filter.XmlType.Cardinality);
|
|
|
|
return filter;
|
|
}
|
|
|
|
public QilNode Axis(XPathAxis xpathAxis, XPathNodeType nodeType, string prefix, string name) {
|
|
Debug.Assert(
|
|
xpathAxis == XPathAxis.Child ||
|
|
xpathAxis == XPathAxis.Attribute ||
|
|
xpathAxis == XPathAxis.DescendantOrSelf ||
|
|
xpathAxis == XPathAxis.Root
|
|
);
|
|
QilLoop result;
|
|
double priority;
|
|
switch (xpathAxis) {
|
|
case XPathAxis.DescendantOrSelf :
|
|
Debug.Assert(nodeType == XPathNodeType.All && prefix == null && name == null, " // is the only d-o-s axes that we can have in pattern");
|
|
return f.Nop(this.fixupNode); // We using Nop as a flag that DescendantOrSelf exis was used between steps.
|
|
case XPathAxis.Root :
|
|
QilIterator i;
|
|
result = f.BaseFactory.Filter(i = f.For(fixupNode), f.IsType(i, T.Document));
|
|
priority = 0.5;
|
|
break;
|
|
default :
|
|
string nsUri = prefix == null ? null : this.environment.ResolvePrefix(prefix);
|
|
result = BuildAxisFilter(f, f.For(fixupNode), xpathAxis, nodeType, name, nsUri);
|
|
switch (nodeType) {
|
|
case XPathNodeType.Element :
|
|
case XPathNodeType.Attribute :
|
|
if (name != null) {
|
|
priority = 0;
|
|
} else {
|
|
if (prefix != null) {
|
|
priority = -0.25;
|
|
} else {
|
|
priority = -0.5;
|
|
}
|
|
}
|
|
break;
|
|
case XPathNodeType.ProcessingInstruction :
|
|
priority = name != null ? 0 : -0.5;
|
|
break;
|
|
default:
|
|
priority = -0.5;
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
SetPriority(result, priority);
|
|
SetLastParent(result, result);
|
|
return result;
|
|
}
|
|
|
|
// a/b/c -> self::c[parent::b[parent::a]]
|
|
// a/b//c -> self::c[ancestor::b[parent::a]]
|
|
// a/b -> self::b[parent::a]
|
|
// -> JoinStep(Axis('a'), Axis('b'))
|
|
// -> Filter('b' & Parent(Filter('a')))
|
|
// a//b
|
|
// -> JoinStep(Axis('a'), JoingStep(Axis(DescendantOrSelf), Axis('b')))
|
|
// -> JoinStep(Filter('a'), JoingStep(Nop(null), Filter('b')))
|
|
// -> JoinStep(Filter('a'), Nop(Filter('b')))
|
|
// -> Filter('b' & Ancestor(Filter('a')))
|
|
public QilNode JoinStep(QilNode left, QilNode right) {
|
|
Debug.Assert(left != null);
|
|
Debug.Assert(right != null);
|
|
if (left.NodeType == QilNodeType.Nop) {
|
|
QilUnary nop = (QilUnary)left;
|
|
Debug.Assert(nop.Child == this.fixupNode);
|
|
nop.Child = right; // We use Nop as a flag that DescendantOrSelf axis was used between steps.
|
|
return nop;
|
|
}
|
|
Debug.Assert(GetLastParent(left) == left, "Left is always single axis and never the step");
|
|
Debug.Assert(left.NodeType == QilNodeType.Filter);
|
|
CleanAnnotation(left);
|
|
QilLoop parentFilter = (QilLoop) left;
|
|
bool ancestor = false; {
|
|
if (right.NodeType == QilNodeType.Nop) {
|
|
ancestor = true;
|
|
QilUnary nop = (QilUnary)right;
|
|
Debug.Assert(nop.Child != null);
|
|
right = nop.Child;
|
|
}
|
|
}
|
|
Debug.Assert(right.NodeType == QilNodeType.Filter);
|
|
QilLoop lastParent = GetLastParent(right);
|
|
FixupFilterBinding(parentFilter, ancestor ? f.Ancestor(lastParent.Variable) : f.Parent(lastParent.Variable));
|
|
lastParent.Body = f.And(lastParent.Body, f.Not(f.IsEmpty(parentFilter)));
|
|
SetPriority(right, 0.5);
|
|
SetLastParent(right, parentFilter);
|
|
return right;
|
|
}
|
|
|
|
QilNode IXPathBuilder<QilNode>.Predicate(QilNode node, QilNode condition, bool isReverseStep) {
|
|
Debug.Assert(false, "Should not call to this function.");
|
|
return null;
|
|
}
|
|
|
|
//The structure of result is a Filter, variable is current node, body is the match condition.
|
|
//Previous predicate build logic in XPathPatternBuilder is match from right to left, which have 2^n complexiy when have lots of position predicates. TFS #368771
|
|
//Now change the logic to: If predicates contains position/last predicates, given the current node, filter out all the nodes that match the predicates,
|
|
//and then check if current node is in the result set.
|
|
public QilNode BuildPredicates(QilNode nodeset, List<QilNode> predicates) {
|
|
//convert predicates to boolean type
|
|
List<QilNode> convertedPredicates = new List<QilNode>(predicates.Count);
|
|
foreach (var predicate in predicates) {
|
|
convertedPredicates.Add(XPathBuilder.PredicateToBoolean(predicate, f, predicateEnvironment));
|
|
}
|
|
|
|
QilLoop nodeFilter = (QilLoop)nodeset;
|
|
QilIterator current = nodeFilter.Variable;
|
|
|
|
//If no last() and position() in predicates, use nodeFilter.Variable to fixup current
|
|
//because all the predicates only based on the input variable, no matter what other predicates are.
|
|
if (predicateEnvironment.numFixupLast == 0 && predicateEnvironment.numFixupPosition == 0) {
|
|
foreach (var predicate in convertedPredicates) {
|
|
nodeFilter.Body = f.And(nodeFilter.Body, predicate);
|
|
}
|
|
nodeFilter.Body = predicateEnvironment.fixupVisitor.Fixup(nodeFilter.Body, current, null);
|
|
}
|
|
//If any preidcate contains last() or position() node, then the current node is based on previous predicates,
|
|
//for instance, a[...][2] is match second node after filter 'a[...]' instead of second 'a'.
|
|
else {
|
|
//filter out the siblings
|
|
QilIterator parentIter = f.For(f.Parent(current));
|
|
QilNode sibling = f.Content(parentIter);
|
|
//generate filter based on input filter
|
|
QilLoop siblingFilter = (QilLoop)nodeset.DeepClone(f.BaseFactory);
|
|
siblingFilter.Variable.Binding = sibling;
|
|
siblingFilter = (QilLoop)f.Loop(parentIter, siblingFilter);
|
|
|
|
//build predicates from left to right to get all the matching nodes
|
|
QilNode matchingSet = siblingFilter;
|
|
foreach (var predicate in convertedPredicates) {
|
|
matchingSet = XPathBuilder.BuildOnePredicate(matchingSet, predicate, /*isReverseStep*/false,
|
|
f, predicateEnvironment.fixupVisitor,
|
|
ref predicateEnvironment.numFixupCurrent, ref predicateEnvironment.numFixupPosition, ref predicateEnvironment.numFixupLast);
|
|
}
|
|
|
|
//check if the matching nodes contains the current node
|
|
QilIterator matchNodeIter = f.For(matchingSet);
|
|
QilNode filterCurrent = f.Filter(matchNodeIter, f.Is(matchNodeIter, current));
|
|
nodeFilter.Body = f.Not(f.IsEmpty(filterCurrent));
|
|
//for passing type check, explict say the result is target type
|
|
nodeFilter.Body = f.And(f.IsType(current, nodeFilter.XmlType), nodeFilter.Body);
|
|
}
|
|
|
|
SetPriority(nodeset, 0.5);
|
|
return nodeset;
|
|
}
|
|
|
|
public QilNode Function(string prefix, string name, IList<QilNode> args) {
|
|
Debug.Assert(prefix.Length == 0);
|
|
QilIterator i = f.For(fixupNode);
|
|
QilNode matches;
|
|
|
|
if (name == "id") {
|
|
Debug.Assert(
|
|
args.Count == 1 && args[0].NodeType == QilNodeType.LiteralString,
|
|
"Function id() must have one literal string argument"
|
|
);
|
|
matches = f.Id(i, args[0]);
|
|
} else {
|
|
Debug.Assert(name == "key", "Unexpected function");
|
|
Debug.Assert(
|
|
args.Count == 2 &&
|
|
args[0].NodeType == QilNodeType.LiteralString && args[1].NodeType == QilNodeType.LiteralString,
|
|
"Function key() must have two literal string arguments"
|
|
);
|
|
matches = environment.ResolveFunction(prefix, name, args, new XsltFunctionFocus(i));
|
|
}
|
|
|
|
QilIterator j;
|
|
QilLoop result = f.BaseFactory.Filter(i, f.Not(f.IsEmpty(f.Filter(j = f.For(matches), f.Is(j, i)))));
|
|
SetPriority(result, 0.5);
|
|
SetLastParent(result, result);
|
|
return result;
|
|
}
|
|
|
|
public QilNode String(string value) { return f.String(value); } // As argument of id() or key() function
|
|
public QilNode Number(double value) { return UnexpectedToken("Literal number"); }
|
|
public QilNode Variable(string prefix, string name) { return UnexpectedToken("Variable"); }
|
|
|
|
private QilNode UnexpectedToken(string tokenName) {
|
|
string prompt = string.Format(CultureInfo.InvariantCulture, "Internal Error: {0} is not allowed in XSLT pattern outside of predicate.", tokenName);
|
|
Debug.Assert(false, prompt);
|
|
throw new Exception(prompt);
|
|
}
|
|
|
|
// -------------------------------------- Priority / Parent ---------------------------------------
|
|
|
|
private class Annotation {
|
|
public double Priority;
|
|
public QilLoop Parent;
|
|
}
|
|
|
|
public static void SetPriority(QilNode node, double priority) {
|
|
Annotation ann = (Annotation)node.Annotation ?? new Annotation();
|
|
ann.Priority = priority;
|
|
node.Annotation = ann;
|
|
}
|
|
|
|
public static double GetPriority(QilNode node) {
|
|
return ((Annotation)node.Annotation).Priority;
|
|
}
|
|
|
|
private static void SetLastParent(QilNode node, QilLoop parent) {
|
|
Debug.Assert(parent.NodeType == QilNodeType.Filter);
|
|
Annotation ann = (Annotation)node.Annotation ?? new Annotation();
|
|
ann.Parent = parent;
|
|
node.Annotation = ann;
|
|
}
|
|
|
|
private static QilLoop GetLastParent(QilNode node) {
|
|
return ((Annotation)node.Annotation).Parent;
|
|
}
|
|
|
|
public static void CleanAnnotation(QilNode node) {
|
|
node.Annotation = null;
|
|
}
|
|
|
|
// -------------------------------------- GetPredicateBuilder() ---------------------------------------
|
|
|
|
public IXPathBuilder<QilNode> GetPredicateBuilder(QilNode ctx) {
|
|
QilLoop context = (QilLoop) ctx;
|
|
Debug.Assert(context != null, "Predicate always has step so it can't have context == null");
|
|
Debug.Assert(context.Variable.NodeType == QilNodeType.For, "It shouldn't be Let, becaus predicates in PatternBuilder don't produce cached tuples.");
|
|
return predicateBuilder;
|
|
}
|
|
|
|
private class XPathPredicateEnvironment : IXPathEnvironment {
|
|
private readonly IXPathEnvironment baseEnvironment;
|
|
private readonly XPathQilFactory f;
|
|
public readonly XPathBuilder.FixupVisitor fixupVisitor;
|
|
private readonly QilNode fixupCurrent, fixupPosition, fixupLast;
|
|
|
|
// Number of unresolved fixup nodes
|
|
public int numFixupCurrent, numFixupPosition, numFixupLast;
|
|
|
|
public XPathPredicateEnvironment(IXPathEnvironment baseEnvironment) {
|
|
this.baseEnvironment = baseEnvironment;
|
|
this.f = baseEnvironment.Factory;
|
|
this.fixupCurrent = f.Unknown(T.NodeNotRtf);
|
|
this.fixupPosition = f.Unknown(T.DoubleX);
|
|
this.fixupLast = f.Unknown(T.DoubleX);
|
|
this.fixupVisitor = new XPathBuilder.FixupVisitor(f, fixupCurrent, fixupPosition, fixupLast);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------------
|
|
IXPathEnvironment interface
|
|
*/
|
|
public XPathQilFactory Factory { get { return f; } }
|
|
|
|
public QilNode ResolveVariable(string prefix, string name) {
|
|
return baseEnvironment.ResolveVariable(prefix, name);
|
|
}
|
|
public QilNode ResolveFunction(string prefix, string name, IList<QilNode> args, IFocus env) {
|
|
return baseEnvironment.ResolveFunction(prefix, name, args, env);
|
|
}
|
|
public string ResolvePrefix(string prefix) {
|
|
return baseEnvironment.ResolvePrefix(prefix);
|
|
}
|
|
|
|
public QilNode GetCurrent() { numFixupCurrent++; return fixupCurrent; }
|
|
public QilNode GetPosition() { numFixupPosition++; return fixupPosition; }
|
|
public QilNode GetLast() { numFixupLast++; return fixupLast; }
|
|
}
|
|
|
|
private class XsltFunctionFocus : IFocus {
|
|
private QilIterator current;
|
|
|
|
public XsltFunctionFocus(QilIterator current) {
|
|
Debug.Assert(current != null);
|
|
this.current = current;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------------
|
|
IFocus interface
|
|
*/
|
|
public QilNode GetCurrent() {
|
|
return current;
|
|
}
|
|
|
|
public QilNode GetPosition() {
|
|
Debug.Fail("GetPosition() must not be called");
|
|
return null;
|
|
}
|
|
|
|
public QilNode GetLast() {
|
|
Debug.Fail("GetLast() must not be called");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|