//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // Microsoft //------------------------------------------------------------------------------ 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.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 predicates) { //convert predicates to boolean type List convertedPredicates = new List(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 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 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 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; } } } }