diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2cc6a8fbd4..88c532f8f2 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -805,6 +805,11 @@ + + + + + diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContent.cs b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs new file mode 100644 index 0000000000..d8b6adf00a --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableContent.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents a content that can be navigated via XPath. + /// + interface INavigableContent + { + /// + /// Gets the unique identifier of the navigable content. + /// + /// The root node identifier should be -1. + int Id { get; } + + /// + /// Gets the unique identifier of parent of the navigable content. + /// + /// The top-level content parent identifiers should be -1 ie the identifier + /// of the root node, whose parent identifier should in turn be -1. + int ParentId { get; } + + /// + /// Gets the type of the navigable content. + /// + INavigableContentType Type { get; } + + /// + /// Gets the unique identifiers of the children of the navigable content. + /// + IList ChildIds { get; } + + /// + /// Gets the value of a field of the navigable content for XPath navigation use. + /// + /// The field index. + /// The value of the field for XPath navigation use. + /// + /// Fields are attributes or elements depending on their relative index value compared + /// to source.LastAttributeIndex. + /// For attributes, the value must be a string. + /// For elements, the value should an XPathNavigator instance if the field is xml + /// and has content (is not empty), null to indicate that the element is empty, or a string + /// which can be empty, whitespace... depending on what the data type wants to expose. + /// + object Value(int index); + + // TODO implement the following one + + ///// + ///// Gets the value of a field of the navigable content, for a specified language. + ///// + ///// The field index. + ///// The language key. + ///// The value of the field for the specified language. + ///// ... + //object Value(int index, string languageKey); + } +} diff --git a/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs new file mode 100644 index 0000000000..94b225467c --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableContentType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents the type of a content that can be navigated via XPath. + /// + interface INavigableContentType + { + /// + /// Gets the name of the content type. + /// + string Name { get; } + + /// + /// Gets the field types of the content type. + /// + /// This includes the attributes and the properties. + INavigableFieldType[] FieldTypes { get; } + } +} diff --git a/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs new file mode 100644 index 0000000000..32a6f64751 --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableFieldType.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents the type of a field of a content that can be navigated via XPath. + /// + /// A field can be an attribute or a property. + interface INavigableFieldType + { + /// + /// Gets the name of the field type. + /// + string Name { get; } + + /// + /// Gets a method to convert the field value to a string. + /// + /// This is for built-in properties, ie attributes. User-defined properties have their + /// own way to convert their value for XPath. + Func XmlStringConverter { get; } + } +} diff --git a/src/Umbraco.Core/Xml/XPath/INavigableSource.cs b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs new file mode 100644 index 0000000000..68dc9e906a --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/INavigableSource.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Represents a source of content that can be navigated via XPath. + /// + interface INavigableSource + { + /// + /// Gets a content identified by its unique identifier. + /// + /// The unique identifier. + /// The content identified by the unique identifier, or null. + /// When id is -1 (root content) implementations should return null. + INavigableContent Get(int id); + + /// + /// Gets the index of the last attribute in the fields collections. + /// + int LastAttributeIndex { get; } + + /// + /// Gets the content at the root of the source. + /// + /// That content should have unique identifier -1 and should not be gettable, + /// ie Get(-1) should return null. Its ParentId should be -1. It should provide + /// values for the attribute fields. + INavigableContent Root { get; } + } +} diff --git a/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs new file mode 100644 index 0000000000..a17727d4ac --- /dev/null +++ b/src/Umbraco.Core/Xml/XPath/NavigableNavigator.cs @@ -0,0 +1,1149 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.XPath; + +namespace Umbraco.Core.Xml.XPath +{ + /// + /// Provides a cursor model for navigating Umbraco data as if it were XML. + /// + class NavigableNavigator : XPathNavigator + { + // "The XmlNameTable stores atomized strings of any local name, namespace URI, + // and prefix used by the XPathNavigator. This means that when the same Name is + // returned multiple times (like "book"), the same String object is returned for + // that Name. This makes it possible to write efficient code that does object + // comparisons on these strings, instead of expensive string comparisons." + // + // "When an element or attribute name occurs multiple times in an XML document, + // it is stored only once in the NameTable. The names are stored as common + // language runtime (CLR) object types. This enables you to do object comparisons + // on these strings rather than a more expensive string comparison. These + // string objects are referred to as atomized strings." + // + // But... "Any instance members are not guaranteed to be thread safe." + // + // see http://msdn.microsoft.com/en-us/library/aa735772%28v=vs.71%29.aspx + // see http://www.hanselman.com/blog/XmlAndTheNametable.aspx + // see http://blogs.msdn.com/b/mfussell/archive/2004/04/30/123673.aspx + // + // "Additionally, all LocalName, NameSpaceUri and Prefix strings must be added to + // a NameTable, given by the NameTable property. When the LocalName, NamespaceURI, + // and Prefix properties are returned, the string returned should come from the + // NameTable. Comparisons between names are done by object comparisons rather + // than by string comparisons, which are significantly slower."" + // + // So what shall we do? Well, here we have no namespace, no prefix, and all + // local names come from cached instances of INavigableContentType or + // INavigableFieldType and are already unique. So... create a one nametable + // because we need one, and share it amongst all clones. + + private readonly XmlNameTable _nameTable; + private readonly INavigableSource _source; + private readonly int _lastAttributeIndex; // last index of attributes in the fields collection + private State _state; + + #region Constructor + + /// + /// Initializes a new instance of the class with a content source. + /// + private NavigableNavigator(INavigableSource source) + { + _source = source; + _lastAttributeIndex = source.LastAttributeIndex; + } + + /// + /// Initializes a new instance of the class with a content source, + /// and an optional root content. + /// + /// The content source. + /// The root content. + /// When no root content is supplied then the root of the source is used. + public NavigableNavigator(INavigableSource source, INavigableContent content = null) + : this(source) + { + _nameTable = new NameTable(); + _lastAttributeIndex = source.LastAttributeIndex; + _state = new State(content ?? source.Root, null, null, 0, StatePosition.Root); + } + + /// + /// Initializes a new instance of the class with a content source, a name table and a state. + /// + /// The content source. + /// The name table. + /// The state. + /// Privately used for cloning a navigator. + private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state) + : this(source) + { + _nameTable = nameTable; + _state = state; + } + + #endregion + + #region Diagnostics + + // diagnostics code will not be compiled nor called into Release configuration. + // in Debug configuration, uncomment lines in Debug() to write to console or to log. + +#if DEBUG + private const string Tabs = " "; + private int _tabs; + private readonly int _uid = GetUid(); + private static int _uidg; + private readonly static object Uidl = new object(); + private static int GetUid() + { + lock (Uidl) + { + return _uidg++; + } + } +#endif + + [Conditional("DEBUG")] + void DebugEnter(string name) + { +#if DEBUG + Debug(""); + DebugState(":"); + Debug(name); + _tabs = Math.Min(Tabs.Length, _tabs + 2); +#endif + } + + [Conditional("DEBUG")] + void DebugCreate(NavigableNavigator nav) + { +#if DEBUG + Debug("Create: [NavigableNavigator::{0}]", nav._uid); +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn() + { +#if DEBUG +// ReSharper disable IntroduceOptionalParameters.Local + DebugReturn("(void)"); +// ReSharper restore IntroduceOptionalParameters.Local +#endif + } + + [Conditional("DEBUG")] + private void DebugReturn(bool value) + { +#if DEBUG + DebugReturn(value ? "true" : "false"); +#endif + } + + [Conditional("DEBUG")] + void DebugReturn(string format, params object[] args) + { +#if DEBUG + Debug("=> " + format, args); + if (_tabs > 0) _tabs -= 2; +#endif + } + + [Conditional("DEBUG")] + void DebugState(string s = " =>") + { +#if DEBUG + string position; + + switch (_state.Position) + { + case StatePosition.Attribute: + position = string.Format("At attribute '{0}/@{1}'.", + _state.Content.Type.Name, + _state.FieldIndex < 0 ? "id" : _state.CurrentFieldType.Name); + break; + case StatePosition.Element: + position = string.Format("At element '{0}'.", + _state.Content.Type.Name); + break; + case StatePosition.PropertyElement: + position = string.Format("At property '{0}/{1}'.", + _state.Content.Type.Name, _state.Content.Type.FieldTypes[this._state.FieldIndex].Name); + break; + case StatePosition.PropertyText: + position = string.Format("At property '{0}/{1}' text.", + _state.Content.Type.Name, _state.CurrentFieldType.Name); + break; + case StatePosition.PropertyXml: + position = string.Format("In property '{0}/{1}' xml fragment.", + _state.Content.Type.Name, _state.CurrentFieldType.Name); + break; + case StatePosition.Root: + position = "At root."; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + Debug("State{0} {1}", s, position); +#endif + } + +#if DEBUG + void Debug(string format, params object[] args) + { + // remove comments to write + + format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format; +#pragma warning disable 168 + var msg = string.Format(format, args); // unused if not writing, hence #pragma +#pragma warning restore 168 + //LogHelper.Debug(msg); // beware! this can quicky overflow log4net + //Console.WriteLine(msg); + } +#endif + + #endregion + + /// + /// Gets the underlying content object. + /// + public override object UnderlyingObject + { + get { return _state.Content; } + } + + /// + /// Creates a new XPathNavigator positioned at the same node as this XPathNavigator. + /// + /// A new XPathNavigator positioned at the same node as this XPathNavigator. + public override XPathNavigator Clone() + { + DebugEnter("Clone"); + var nav = new NavigableNavigator(_source, _nameTable, _state.Clone()); + DebugCreate(nav); + DebugReturn("[XPathNavigator]"); + return nav; + } + + /// + /// Creates a new XPathNavigator using the same source but positioned at a new root. + /// + /// A new XPathNavigator using the same source and positioned at a new root. + /// The new root can be above this navigator's root. + public XPathNavigator CloneWithNewRoot(string id) + { + DebugEnter("CloneWithNewRoot"); + + int contentId; + State state = null; + + if (id != null && id.Trim() == "-1") + { + state = new State(_source.Root, null, null, 0, StatePosition.Root); + } + else if (int.TryParse(id, out contentId)) + { + var content = _source.Get(contentId); + if (content != null) + { + state = new State(content, null, null, 0, StatePosition.Root); + } + } + + NavigableNavigator clone = null; + + if (state != null) + { + clone = new NavigableNavigator(_source, _nameTable, state); + DebugCreate(clone); + DebugReturn("[XPathNavigator]"); + } + else + { + DebugReturn("[null]"); + } + + return clone; + } + + /// + /// Gets a value indicating whether the current node is an empty element without an end element tag. + /// + public override bool IsEmptyElement + { + get + { + DebugEnter("IsEmptyElement"); + bool isEmpty; + + switch (_state.Position) + { + case StatePosition.Element: + isEmpty = (_state.Content.ChildIds == null || _state.Content.ChildIds.Count == 0) // no content child + && _state.FieldsCount - 1 == _lastAttributeIndex; // no property element child + break; + case StatePosition.PropertyElement: + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + isEmpty = _state.Content.Value(_state.FieldIndex) == null; + break; + case StatePosition.PropertyXml: + isEmpty = _state.XmlFragmentNavigator.IsEmptyElement; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.Root: + throw new InvalidOperationException("Not an element."); + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isEmpty); + return isEmpty; + } + } + + /// + /// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator. + /// + /// The XPathNavigator to compare to this XPathNavigator. + /// true if the two XPathNavigator objects have the same position; otherwise, false. + public override bool IsSamePosition(XPathNavigator nav) + { + DebugEnter("IsSamePosition"); + bool isSame; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + isSame = _state.XmlFragmentNavigator.IsSamePosition(nav); + break; + case StatePosition.Attribute: + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + var other = nav as NavigableNavigator; + isSame = other != null && other._source == _source && _state.IsSamePosition(other._state); + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(isSame); + return isSame; + } + + /// + /// Gets the qualified name of the current node. + /// + public override string Name + { + get + { + DebugEnter("Name"); + string name; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + name = _state.XmlFragmentNavigator.Name; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + name = _state.FieldIndex == -1 ? "id" : _state.CurrentFieldType.Name; + break; + case StatePosition.Element: + name = _state.Content.Type.Name; + break; + case StatePosition.PropertyText: + name = string.Empty; + break; + case StatePosition.Root: + name = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Gets the Name of the current node without any namespace prefix. + /// + public override string LocalName + { + get + { + DebugEnter("LocalName"); + var name = Name; + DebugReturn("\"{0}\"", name); + return name; + } + } + + /// + /// Moves the XPathNavigator to the same position as the specified XPathNavigator. + /// + /// The XPathNavigator positioned on the node that you want to move to. + /// Returns true if the XPathNavigator is successful moving to the same position as the specified XPathNavigator; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveTo(XPathNavigator nav) + { + DebugEnter("MoveTo"); + + var other = nav as NavigableNavigator; + var succ = false; + + if (other != null && other._source == _source) + { + _state = other._state.Clone(); + DebugState(); + succ = true; + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first attribute of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first attribute of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstAttribute() + { + DebugEnter("MoveToFirstAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToFirstAttribute(); + break; + case StatePosition.Element: + _state.FieldIndex = -1; + _state.Position = StatePosition.Attribute; + DebugState(); + succ = true; + break; + case StatePosition.Attribute: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the first child node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the first child node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstChild() + { + DebugEnter("MoveToFirstChild"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToFirstChild(); + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + succ = false; + break; + case StatePosition.Element: + var firstPropertyIndex = _lastAttributeIndex + 1; + if (_state.FieldsCount > firstPropertyIndex) + { + _state.Position = StatePosition.PropertyElement; + _state.FieldIndex = firstPropertyIndex; + DebugState(); + succ = true; + } + else succ = MoveToFirstChildElement(); + break; + case StatePosition.PropertyElement: + succ = MoveToFirstChildProperty(); + break; + case StatePosition.Root: + _state.Position = StatePosition.Element; + DebugState(); + succ = true; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + private bool MoveToFirstChildElement() + { + var children = _state.Content.ChildIds; + + if (children != null && children.Count > 0) + { + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + var child = children.Select(id => _source.Get(id)).FirstOrDefault(c => c != null); + if (child != null) + { + _state.Position = StatePosition.Element; + _state.FieldIndex = -1; + _state = new State(child, _state, children, 0, StatePosition.Element); + DebugState(); + return true; + } + } + + return false; + } + + private bool MoveToFirstChildProperty() + { + var valueForXPath = _state.Content.Value(_state.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + + var nav = valueForXPath as XPathNavigator; + if (nav != null) + { + nav = nav.Clone(); // never use the one we got + nav.MoveToFirstChild(); + _state.XmlFragmentNavigator = nav; + _state.Position = StatePosition.PropertyXml; + DebugState(); + return true; + } + + if (valueForXPath == null) + return false; + + if (valueForXPath is string) + { + _state.Position = StatePosition.PropertyText; + DebugState(); + return true; + } + + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + } + + /// + /// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the first namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToFirstNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified. + /// + /// An XPathNamespaceScope value describing the namespace scope. + /// Returns true if the XPathNavigator is successful moving to the next namespace node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) + { + DebugEnter("MoveToNextNamespace"); + DebugReturn(false); + return false; + } + + /// + /// Moves to the node that has an attribute of type ID whose value matches the specified String. + /// + /// A String representing the ID value of the node to which you want to move. + /// true if the XPathNavigator is successful moving; otherwise, false. + /// If false, the position of the navigator is unchanged. + public override bool MoveToId(string id) + { + DebugEnter("MoveToId"); + var succ = false; + + // don't look into fragments, just look for element identifiers + // not sure we actually need to implement it... think of it as + // as exercise of style, always better than throwing NotImplemented. + + int contentId; + if (/*id != null &&*/ id.Trim() == "-1") // id cannot be null + { + _state = new State(_source.Root, null, _source.Root.ChildIds, 0, StatePosition.Element); + succ = true; + } + else if (int.TryParse(id, out contentId)) + { + var content = _source.Get(contentId); + if (content != null) + { + var state = _state; + while (state.Parent != null) + state = state.Parent; + var navRootId = state.Content.Id; // navigator may be rooted below source root + + var s = new Stack(); + while (content != null && content.ParentId != navRootId) + { + s.Push(content); + content = _source.Get(content.ParentId); + } + if (content != null) + { + _state = new State(_source.Root, null, _source.Root.ChildIds, _source.Root.ChildIds.IndexOf(content.Id), StatePosition.Element); + while (content != null) + { + _state = new State(content, _state, content.ChildIds, _state.Content.ChildIds.IndexOf(content.Id), StatePosition.Element); + content = s.Count == 0 ? null : s.Pop(); + } + DebugState(); + succ = true; + } + } + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next sibling node of the current node. + /// + /// true if the XPathNavigator is successful moving to the next sibling node; + /// otherwise, false if there are no more siblings or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNext() + { + DebugEnter("MoveToNext"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToNext(); + break; + case StatePosition.Element: + succ = false; + while (_state.Siblings != null && _state.SiblingIndex < _state.Siblings.Count - 1) + { + // Siblings may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + var node = _source.Get(_state.Siblings[++_state.SiblingIndex]); + if (node == null) continue; + + _state.Content = node; + DebugState(); + succ = true; + break; + } + break; + case StatePosition.PropertyElement: + if (_state.FieldIndex == _state.FieldsCount - 1) + { + // after property elements may come some children elements + // if successful, will push a new state + succ = MoveToFirstChildElement(); + } + else + { + ++_state.FieldIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.PropertyText: + case StatePosition.Attribute: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the previous sibling node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the previous sibling node; + /// otherwise, false if there is no previous sibling node or if the XPathNavigator is currently + /// positioned on an attribute node. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToPrevious() + { + DebugEnter("MoveToPrevious"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToPrevious(); + break; + case StatePosition.Element: + succ = false; + while (_state.Siblings != null && _state.SiblingIndex > 0) + { + // children may contain IDs that does not correspond to some content in source + // because children contains all child IDs including unpublished children - and + // then if we're not previewing, the source will return null. + var content = _source.Get(_state.Siblings[--_state.SiblingIndex]); + if (content == null) continue; + + _state.Content = content; + DebugState(); + succ = true; + break; + } + if (succ == false && _state.SiblingIndex == 0 && _state.FieldsCount - 1 > _lastAttributeIndex) + { + // before children elements may come some property elements + if (MoveToParentElement()) // pops the state + { + _state.FieldIndex = _state.FieldsCount - 1; + DebugState(); + succ = true; + } + } + break; + case StatePosition.PropertyElement: + succ = false; + if (_state.FieldIndex > _lastAttributeIndex) + { + --_state.FieldIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the next attribute. + /// + /// Returns true if the XPathNavigator is successful moving to the next attribute; + /// false if there are no more attributes. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToNextAttribute() + { + DebugEnter("MoveToNextAttribute"); + bool succ; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + succ = _state.XmlFragmentNavigator.MoveToNextAttribute(); + break; + case StatePosition.Attribute: + if (_state.FieldIndex == _lastAttributeIndex) + succ = false; + else + { + ++_state.FieldIndex; + DebugState(); + succ = true; + } + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + case StatePosition.PropertyText: + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + /// + /// Moves the XPathNavigator to the parent node of the current node. + /// + /// Returns true if the XPathNavigator is successful moving to the parent node of the current node; + /// otherwise, false. If false, the position of the XPathNavigator is unchanged. + public override bool MoveToParent() + { + DebugEnter("MoveToParent"); + bool succ; + + switch (_state.Position) + { + case StatePosition.Attribute: + case StatePosition.PropertyElement: + _state.Position = StatePosition.Element; + _state.FieldIndex = -1; + DebugState(); + succ = true; + break; + case StatePosition.Element: + succ = MoveToParentElement(); + if (!succ) + { + _state.Position = StatePosition.Root; + succ = true; + } + break; + case StatePosition.PropertyText: + _state.Position = StatePosition.PropertyElement; + DebugState(); + succ = true; + break; + case StatePosition.PropertyXml: + if (!_state.XmlFragmentNavigator.MoveToParent()) + throw new InvalidOperationException("Could not move to parent in fragment."); + if (_state.XmlFragmentNavigator.NodeType == XPathNodeType.Root) + { + _state.XmlFragmentNavigator = null; + _state.Position = StatePosition.PropertyElement; + DebugState(); + } + succ = true; + break; + case StatePosition.Root: + succ = false; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn(succ); + return succ; + } + + private bool MoveToParentElement() + { + var p = _state.Parent; + if (p != null) + { + _state = p; + DebugState(); + return true; + } + + return false; + } + + /// + /// Moves the XPathNavigator to the root node that the current node belongs to. + /// + public override void MoveToRoot() + { + DebugEnter("MoveToRoot"); + + while (_state.Parent != null) + _state = _state.Parent; + DebugState(); + + DebugReturn(); + } + + /// + /// Gets the base URI for the current node. + /// + public override string BaseURI + { + get { return string.Empty; } + } + + /// + /// Gets the XmlNameTable of the XPathNavigator. + /// + public override XmlNameTable NameTable + { + get { return _nameTable; } + } + + /// + /// Gets the namespace URI of the current node. + /// + public override string NamespaceURI + { + get { return string.Empty; } + } + + /// + /// Gets the XPathNodeType of the current node. + /// + public override XPathNodeType NodeType + { + get + { + DebugEnter("NodeType"); + XPathNodeType type; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + type = _state.XmlFragmentNavigator.NodeType; + break; + case StatePosition.Attribute: + type = XPathNodeType.Attribute; + break; + case StatePosition.Element: + case StatePosition.PropertyElement: + type = XPathNodeType.Element; + break; + case StatePosition.PropertyText: + type = XPathNodeType.Text; + break; + case StatePosition.Root: + type = XPathNodeType.Root; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\'{0}\'", type); + return type; + } + } + + /// + /// Gets the namespace prefix associated with the current node. + /// + public override string Prefix + { + get { return string.Empty; } + } + + /// + /// Gets the string value of the item. + /// + /// Does not fully behave as per the specs, as we report empty value on content elements, and we start + /// reporting values only on property elements. This is because, otherwise, we would dump the whole database + /// and it probably does not make sense at Umbraco level. + public override string Value + { + get + { + DebugEnter("Value"); + string value; + + switch (_state.Position) + { + case StatePosition.PropertyXml: + value = _state.XmlFragmentNavigator.Value; + break; + case StatePosition.Attribute: + case StatePosition.PropertyText: + case StatePosition.PropertyElement: + if (_state.FieldIndex == -1) + { + value = _state.Content.Id.ToString(CultureInfo.InvariantCulture); + } + else + { + var valueForXPath = _state.Content.Value(_state.FieldIndex); + + // value should be + // - an XPathNavigator over a non-empty XML fragment + // - a non-Xml-whitespace string + // - null + + var nav = valueForXPath as XPathNavigator; + var s = valueForXPath as string; + if (valueForXPath == null) + { + value = string.Empty; + } + else if (nav != null) + { + nav = nav.Clone(); // never use the one we got + value = nav.Value; + } + else if (s != null) + { + value = s; + } + else + { + throw new InvalidOperationException("XPathValue must be an XPathNavigator or a string."); + } + } + break; + case StatePosition.Element: + case StatePosition.Root: + value = string.Empty; + break; + default: + throw new InvalidOperationException("Invalid position."); + } + + DebugReturn("\"{0}\"", value); + return value; + } + } + + #region State management + + // the possible state positions + internal enum StatePosition + { + Root, + Element, + Attribute, + PropertyElement, + PropertyText, + PropertyXml + }; + + // gets the state + // for unit tests only + internal State InternalState { get { return _state; } } + + // represents the XPathNavigator state + internal class State + { + public StatePosition Position { get; set; } + + // initialize a new state + private State(StatePosition position) + { + Position = position; + FieldIndex = -1; + } + + // initialize a new state + // used for creating the very first state + // and also when moving to a child element + public State(INavigableContent content, State parent, IList siblings, int siblingIndex, StatePosition position) + : this(position) + { + Content = content; + Parent = parent; + Siblings = siblings; + SiblingIndex = siblingIndex; + } + + // initialize a clone state + private State(State other, bool recurse = false) + { + Position = other.Position; + + _content = other._content; + SiblingIndex = other.SiblingIndex; + Siblings = other.Siblings; + FieldsCount = other.FieldsCount; + FieldIndex = other.FieldIndex; + + if (Position == StatePosition.PropertyXml) + XmlFragmentNavigator = other.XmlFragmentNavigator.Clone(); + + // NielsK did + //Parent = other.Parent; + // but that creates corrupted stacks of states when cloning + // because clones share the parents : have to clone the whole + // stack of states. Avoid recursion. + + if (recurse) return; + + var clone = this; + while (other.Parent != null) + { + clone.Parent = new State(other.Parent, true); + clone = clone.Parent; + other = other.Parent; + } + } + + public State Clone() + { + return new State(this); + } + + // the parent state + public State Parent { get; private set; } + + // the current content + private INavigableContent _content; + + // the current content + public INavigableContent Content + { + get + { + return _content; + } + set + { + FieldsCount = value == null ? 0 : value.Type.FieldTypes.Length; + _content = value; + } + } + + // the index of the current content within Siblings + public int SiblingIndex { get; set; } + + // the list of content identifiers for all children of the current content's parent + public IList Siblings { get; set; } + + // the number of fields of the current content + // properties include attributes and properties + public int FieldsCount { get; private set; } + + // the index of the current field + // index -1 means special attribute "id" + public int FieldIndex { get; set; } + + // the current field type + // beware, no check on the index + public INavigableFieldType CurrentFieldType { get { return Content.Type.FieldTypes[FieldIndex]; } } + + // gets or sets the xml fragment navigator + public XPathNavigator XmlFragmentNavigator { get; set; } + + // gets a value indicating whether this state is at the same position as another one. + public bool IsSamePosition(State other) + { + return other.Position == Position + && (Position != StatePosition.PropertyXml || other.XmlFragmentNavigator.IsSamePosition(XmlFragmentNavigator)) + && other.Content == Content + && other.FieldIndex == FieldIndex; + } + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs new file mode 100644 index 0000000000..c14894bf43 --- /dev/null +++ b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs @@ -0,0 +1,1015 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Xml; +using System.Xml.XPath; +using System.Xml.Xsl; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.Xml; +using Umbraco.Core.Xml.XPath; +using NUnit.Framework; + +namespace Umbraco.Tests.CoreXml +{ + [TestFixture] + public class NavigableNavigatorTests + { + [Test] + public void NewNavigatorIsAtRoot() + { + const string xml = @""; + var doc = XmlHelper.CreateXPathDocument(xml); + var nav = doc.CreateNavigator(); + + Assert.AreEqual(XPathNodeType.Root, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + + var source = new TestSource5(); + nav = new NavigableNavigator(source); + + Assert.AreEqual(XPathNodeType.Root, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + } + + [Test] + public void NativeXmlValues() + { + const string xml = @" + + + + + + + + blah + + blah + + + bam + + + +"; + var doc = XmlHelper.CreateXPathDocument(xml); + var nav = doc.CreateNavigator(); + + NavigatorValues(nav, true); + } + + [Test] + public void NavigableXmlValues() + { + var source = new TestSource6(); + var nav = new NavigableNavigator(source); + + NavigatorValues(nav, false); + } + + static void NavigatorValues(XPathNavigator nav, bool native) + { + // in non-native we can't have Value dump everything, else + // we'd dump the entire database? Makes not much sense. + + Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !! + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("root", nav.Name); + Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !! + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("wrap", nav.Name); + Assert.AreEqual(native ? "\r\n blah\r\n blah\r\n bam\r\n " : string.Empty, nav.Value); // !! + + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("item1", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); // !! + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2a", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); // !! + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2b", nav.Name); + Assert.AreEqual(string.Empty, nav.Value); + Assert.IsFalse(nav.MoveToFirstChild()); // !! + + // we have no way to tell the navigable that a value is CDATA + // so the rule is, if it's null it's not there, anything else is there + // and the filtering has to be done when building the content + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item2c", nav.Name); + Assert.AreEqual("\r\n ", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("\r\n ", nav.Value); + Assert.IsTrue(nav.MoveToParent()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item3", nav.Name); + Assert.AreEqual("blah", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("blah", nav.Value); + Assert.IsTrue(nav.MoveToParent()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item3a", nav.Name); + Assert.AreEqual("\r\n blah\r\n ", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("\r\n blah\r\n ", nav.Value); + Assert.IsTrue(nav.MoveToParent()); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item4", nav.Name); + Assert.AreEqual("bam", nav.Value); // ok since it's a property + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("subitem", nav.Name); + Assert.AreEqual("bam", nav.Value); // ok since we're in a fragment + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual(string.Empty, nav.Name); + Assert.AreEqual("bam", nav.Value); + Assert.IsFalse(nav.MoveToNext()); + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual("subitem", nav.Name); + Assert.IsFalse(nav.MoveToNext()); + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual("item4", nav.Name); + + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("item5", nav.Name); + Assert.AreEqual("\r\n ", nav.Value); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(XPathNodeType.Text, nav.NodeType); + Assert.AreEqual("\r\n ", nav.Value); + } + + [Test] + public void Navigate() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + nav.MoveToRoot(); + Assert.AreEqual("", nav.Name); // because we're at root + nav.MoveToFirstChild(); + Assert.AreEqual("root", nav.Name); + nav.MoveToFirstChild(); + Assert.AreEqual("type1", nav.Name); // our first content + nav.MoveToFirstAttribute(); + Assert.AreEqual("id", nav.Name); + Assert.AreEqual("1", nav.Value); + nav.MoveToNextAttribute(); + Assert.AreEqual("prop1", nav.Name); + Assert.AreEqual("1:p1", nav.Value); + nav.MoveToNextAttribute(); + Assert.AreEqual("prop2", nav.Name); + Assert.AreEqual("1:p2", nav.Value); + Assert.IsFalse(nav.MoveToNextAttribute()); + nav.MoveToParent(); + nav.MoveToFirstChild(); + Assert.AreEqual("prop3", nav.Name); + Assert.AreEqual("1:p3", nav.Value); + + Assert.IsFalse(nav.MoveToNext()); + } + + [Test] + public void NavigateMixed() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + nav.MoveToRoot(); + nav.MoveToFirstChild(); + Assert.AreEqual("root", nav.Name); + nav.MoveToFirstChild(); + Assert.AreEqual("type1", nav.Name); // our content + nav.MoveToFirstChild(); + Assert.AreEqual("prop1", nav.Name); // our property + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + nav.MoveToFirstChild(); + + // "poo" + + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("data", nav.Name); + + nav.MoveToFirstChild(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("item1", nav.Name); + + nav.MoveToNext(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("item2", nav.Name); + + nav.MoveToParent(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("data", nav.Name); + + nav.MoveToParent(); + Assert.AreEqual(XPathNodeType.Element, nav.NodeType); + Assert.AreEqual("prop1", nav.Name); + } + + [Test] + public void OuterXmlBasic() + { + const string xml = @""; + + var doc = XmlHelper.CreateXPathDocument(xml); + var nnav = doc.CreateNavigator(); + Assert.AreEqual(xml, nnav.OuterXml); + + var source = new TestSource0(); + var nav = new NavigableNavigator(source); + Assert.AreEqual(xml, nav.OuterXml); + } + + [Test] + public void OuterXml() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + const string xml = @" + + 1:p3 + +"; + + Assert.AreEqual(xml, nav.OuterXml); + } + + [Test] + public void OuterXmlMixed() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + nav.MoveToRoot(); + + const string outerXml = @" + + + + poo + + + + + +"; + + Assert.AreEqual(outerXml, nav.OuterXml); + } + + [Test] + public void Query() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("//type1"); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("type1", iterator.Current.Name); + + iterator = nav.Select("//* [@prop1='1:p1']"); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("type1", iterator.Current.Name); + } + + [Test] + public void QueryMixed() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + var doc = XmlHelper.CreateXPathDocument("poo"); + var docNav = doc.CreateNavigator(); + var docIter = docNav.Select("//item2 [@xx=33]"); + Assert.AreEqual(1, docIter.Count); + Assert.AreEqual("", docIter.Current.Name); + docIter.MoveNext(); + Assert.AreEqual("item2", docIter.Current.Name); + + var iterator = nav.Select("//item2 [@xx=33]"); + Assert.AreEqual(1, iterator.Count); + Assert.AreEqual("", iterator.Current.Name); + iterator.MoveNext(); + Assert.AreEqual("item2", iterator.Current.Name); + } + + [Test] + public void QueryWithVariables() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("//* [@prop1=$var]", new XPathVariable("var", "1:p1")); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("type1", iterator.Current.Name); + } + + [Test] + public void QueryMixedWithVariables() + { + var source = new TestSource2(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("//item2 [@xx=$var]", new XPathVariable("var", "33")); + Assert.AreEqual(1, iterator.Count); + iterator.MoveNext(); + Assert.AreEqual("item2", iterator.Current.Name); + } + + [Test] + public void MixedWithNoValue() + { + var source = new TestSource4(); + var nav = new NavigableNavigator(source); + + var doc = XmlHelper.CreateXPathDocument(@" + dang + + + "); + var docNav = doc.CreateNavigator(); + + docNav.MoveToRoot(); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.AreEqual("root", docNav.Name); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.AreEqual("type1", docNav.Name); + Assert.IsTrue(docNav.MoveToNext()); + Assert.AreEqual("type1", docNav.Name); + Assert.IsTrue(docNav.MoveToNext()); + Assert.AreEqual("type1", docNav.Name); + Assert.IsFalse(docNav.MoveToNext()); + + docNav.MoveToRoot(); + var docOuter = docNav.OuterXml; + + nav.MoveToRoot(); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("root", nav.Name); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual("type1", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("type1", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual("type1", nav.Name); + Assert.IsFalse(nav.MoveToNext()); + + nav.MoveToRoot(); + var outer = nav.OuterXml; + + Assert.AreEqual(docOuter, outer); + } + + [Test] + [Ignore("NavigableNavigator does not implement IHasXmlNode.")] + public void XmlNodeList() + { + var source = new TestSource1(); + var nav = new NavigableNavigator(source); + + var iterator = nav.Select("/*"); + + // but, that requires that the underlying navigator implements IHasXmlNode + // so it is possible to obtain nodes from the navigator - not possible yet + var nodes = XmlNodeListFactory.CreateNodeList(iterator); + + Assert.AreEqual(nodes.Count, 1); + var node = nodes[0]; + + Assert.AreEqual(3, node.Attributes.Count); + Assert.AreEqual("1", node.Attributes["id"].Value); + Assert.AreEqual("1:p1", node.Attributes["prop1"].Value); + Assert.AreEqual("1:p2", node.Attributes["prop2"].Value); + Assert.AreEqual(1, node.ChildNodes.Count); + Assert.AreEqual("prop3", node.FirstChild.Name); + Assert.AreEqual("1:p3", node.FirstChild.Value); + } + + [Test] + public void CloneIsSafe() + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + TestContent content; + + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual("root", nav.Name); // at -1 + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position); + Assert.AreEqual("prop1", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position); + Assert.AreEqual("prop2", nav.Name); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id); + + // at that point nav is at /root/1/3 + + var clone = nav.Clone() as NavigableNavigator; + + // move nav to /root/1/5 and ensure that clone stays at /root/1/3 + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(5, (nav.UnderlyingObject as TestContent).Id); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, clone.InternalState.Position); + Assert.AreEqual(3, (clone.UnderlyingObject as TestContent).Id); + + // move nav to /root/2 + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(2, (nav.UnderlyingObject as TestContent).Id); + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.PropertyElement, nav.InternalState.Position); + Assert.AreEqual("prop1", nav.Name); + Assert.AreEqual("p21", nav.Value); + + // move clone to .. /root/1 + Assert.IsTrue(clone.MoveToParent()); + + // clone has not been corrupted by nav + Assert.AreEqual(NavigableNavigator.StatePosition.Element, clone.InternalState.Position); + Assert.AreEqual(1, (clone.UnderlyingObject as TestContent).Id); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void SelectById(int id) + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + + var iter = nav.Select(string.Format("//* [@id={0}]", id)); + Assert.IsTrue(iter.MoveNext()); + var current = iter.Current as NavigableNavigator; + Assert.AreEqual(NavigableNavigator.StatePosition.Element, current.InternalState.Position); + Assert.AreEqual(id, (current.UnderlyingObject as TestContent).Id); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void SelectByIdWithVariable(int id) + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + + var iter = nav.Select("//* [@id=$id]", new XPathVariable("id", id.ToString(CultureInfo.InvariantCulture))); + Assert.IsTrue(iter.MoveNext()); + var current = iter.Current as NavigableNavigator; + Assert.AreEqual(NavigableNavigator.StatePosition.Element, current.InternalState.Position); + Assert.AreEqual(id, (current.UnderlyingObject as TestContent).Id); + } + + [Test] + public void MoveToId() + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source); + + // move to /root/1/3 + Assert.IsTrue(nav.MoveToId("3")); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id); + + // move to /root/1 + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // move to /root + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id); + + // move up + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // move to /root/1 + Assert.IsTrue(nav.MoveToId("1")); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // move to /root + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id); + + // move up + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // move to /root + Assert.IsTrue(nav.MoveToId("-1")); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(-1, (nav.UnderlyingObject as TestContent).Id); + + // move up + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // get lost + Assert.IsFalse(nav.MoveToId("666")); + } + + [Test] + public void RootedNavigator() + { + var source = new TestSource5(); + var nav = new NavigableNavigator(source, source.Get(1)); + + // go to (/root) /1 + Assert.IsTrue(nav.MoveToFirstChild()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // go to (/root) /1/prop1 + Assert.IsTrue(nav.MoveToFirstChild()); + // go to (/root) /1/prop2 + Assert.IsTrue(nav.MoveToNext()); + // go to (/root) /1/3 + Assert.IsTrue(nav.MoveToNext()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(3, (nav.UnderlyingObject as TestContent).Id); + + // go to (/root) /1 + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Element, nav.InternalState.Position); + Assert.AreEqual(1, (nav.UnderlyingObject as TestContent).Id); + + // go to (/root) ie root + Assert.IsTrue(nav.MoveToParent()); + Assert.AreEqual(NavigableNavigator.StatePosition.Root, nav.InternalState.Position); + Assert.IsFalse(nav.MoveToParent()); + + // can't go there + Assert.IsFalse(nav.MoveToId("2")); + } + + [Test] + public void WhiteSpacesAndEmptyValues() + { + + // "When Microsoft’s DOM builder receives a text node from the parser + // that contains only white space, it is thrown away." - so if it's ONLY + // spaces, it's nothing, but spaces are NOT trimmed. + + // For attributes, spaces are preserved even when there's only spaces. + + var doc = XmlHelper.CreateXPathDocument(@" + + + + + + ooo + ooo + + + + "); + + var docNav = doc.CreateNavigator(); + + Assert.AreEqual(@" + + + + + + + + + + + + + + ooo + + + ooo + + + + +", docNav.OuterXml); + + docNav.MoveToRoot(); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.IsTrue(docNav.MoveToFirstChild()); + Assert.IsTrue(docNav.MoveToFirstChild()); // prop + Assert.IsTrue(docNav.IsEmptyElement); + Assert.IsTrue(docNav.MoveToParent()); + Assert.IsTrue(docNav.MoveToNext()); + Assert.IsTrue(docNav.MoveToFirstChild()); // prop + Assert.IsFalse(docNav.IsEmptyElement); + Assert.AreEqual("", docNav.Value); // contains an empty text node + Assert.IsTrue(docNav.MoveToParent()); + Assert.IsTrue(docNav.MoveToNext()); + Assert.IsTrue(docNav.MoveToFirstChild()); // prop + Assert.IsFalse(docNav.IsEmptyElement); + Assert.AreEqual("", docNav.Value); // contains an empty text node + + var source = new TestSource8(); + var nav = new NavigableNavigator(source); + + // shows how whitespaces are handled by NavigableNavigator + Assert.AreEqual(@" + + + + + + + + + + + + + + + ooo + +", nav.OuterXml); + } + } + + #region Navigable implementation + + class TestPropertyType : INavigableFieldType + { + public TestPropertyType(string name, bool isXmlContent = false, Func xmlStringConverter = null) + { + Name = name; + IsXmlContent = isXmlContent; + XmlStringConverter = xmlStringConverter; + } + + public string Name { get; private set; } + public bool IsXmlContent { get; private set; } + public Func XmlStringConverter { get; private set; } + } + + class TestContentType : INavigableContentType + { + public TestContentType(TestSourceBase source, string name, params INavigableFieldType[] properties) + { + Source = source; + Name = name; + FieldTypes = properties; + } + + public TestSourceBase Source { get; private set; } + public string Name { get; private set; } + public INavigableFieldType[] FieldTypes { get; protected set; } + } + + class TestRootContentType : TestContentType + { + public TestRootContentType(TestSourceBase source, params INavigableFieldType[] properties) + : base(source, "root") + { + FieldTypes = properties; + } + + public TestContentType CreateType(string name, params INavigableFieldType[] properties) + { + return new TestContentType(Source, name, FieldTypes.Union(properties).ToArray()); + } + } + + class TestContent : INavigableContent + { + public TestContent(TestContentType type, int id, int parentId) + { + _type = type; + Id = id; + ParentId = parentId; + } + + private readonly TestContentType _type; + public int Id { get; private set; } + public int ParentId { get; private set; } + public INavigableContentType Type { get { return _type; } } + public IList ChildIds { get; private set; } + + public object Value(int id) + { + var fieldType = _type.FieldTypes[id]; + var value = FieldValues[id]; + var isAttr = id <= _type.Source.LastAttributeIndex; + + if (value == null) return null; + if (isAttr) return value.ToString(); + + if (fieldType.XmlStringConverter != null) return fieldType.XmlStringConverter(value); + + // though in reality we should use the converters, which should + // know whether the property is XML or not, instead of guessing. + XPathDocument doc; + if (XmlHelper.TryCreateXPathDocumentFromPropertyValue(value, out doc)) + return doc.CreateNavigator(); + + //var s = value.ToString(); + //return XmlHelper.IsXmlWhitespace(s) ? null : s; + return value.ToString(); + } + + // locals + public object[] FieldValues { get; private set; } + + public TestContent WithChildren(params int[] childIds) + { + ChildIds = childIds; + return this; + } + + public TestContent WithValues(params object[] values) + { + FieldValues = values == null ? new object[] {null} : values; + return this; + } + } + + class TestRootContent : TestContent + { + public TestRootContent(TestContentType type) + : base(type, -1, -1) + { } + } + + abstract class TestSourceBase : INavigableSource + { + protected readonly Dictionary Content = new Dictionary(); + + public INavigableContent Get(int id) + { + return Content.ContainsKey(id) ? Content[id] : null; + } + + public int LastAttributeIndex { get; protected set; } + + public INavigableContent Root { get; protected set; } + } + + #endregion + + #region Navigable sources + + class TestSource0 : TestSourceBase + { + public TestSource0() + { + LastAttributeIndex = -1; + var type = new TestRootContentType(this); + Root = new TestRootContent(type); + } + } + + class TestSource1 : TestSourceBase + { + public TestSource1() + { + // last attribute index is 1 - meaning properties 0 and 1 are attributes, 2+ are elements + // then, fieldValues must have adequate number of items + LastAttributeIndex = 1; + + var prop1 = new TestPropertyType("prop1"); + var prop2 = new TestPropertyType("prop2"); + var prop3 = new TestPropertyType("prop3"); + var type = new TestRootContentType(this, prop1, prop2); + var type1 = type.CreateType("type1", prop3); + + Content[1] = new TestContent(type1, 1, -1).WithValues("1:p1", "1:p2", "1:p3"); + + Root = new TestRootContent(type).WithValues("", "").WithChildren(1); + } + } + + class TestSource2 : TestSourceBase + { + public TestSource2() + { + LastAttributeIndex = -1; + + var prop1 = new TestPropertyType("prop1", true); + var type = new TestRootContentType(this); + var type1 = type.CreateType("type1", prop1); + + const string xml = "poo"; + Content[1] = new TestContent(type1, 1, 1).WithValues(xml); + + Root = new TestRootContent(type).WithChildren(1); + } + } + + class TestSource3 : TestSourceBase + { + public TestSource3() + { + LastAttributeIndex = 1; + + var prop1 = new TestPropertyType("prop1"); + var prop2 = new TestPropertyType("prop2"); + var prop3 = new TestPropertyType("prop3"); + var type = new TestRootContentType(this, prop1, prop2); + var type1 = type.CreateType("type1", prop3); + + Content[1] = new TestContent(type1, 1, 1).WithValues("1:p1", "1:p2", "1:p3").WithChildren(2); + Content[2] = new TestContent(type1, 2, 1).WithValues("2:p1", "2:p2", "2:p3"); + + Root = new TestRootContent(type).WithChildren(1); + } + } + + class TestSource4 : TestSourceBase + { + public TestSource4() + { + LastAttributeIndex = -1; + + var prop1 = new TestPropertyType("prop1", true); + var prop2 = new TestPropertyType("prop2"); + var type = new TestRootContentType(this); + var type1 = type.CreateType("type1", prop1, prop2); + + Content[1] = new TestContent(type1, 1, -1).WithValues("", "dang"); + Content[2] = new TestContent(type1, 2, -1).WithValues(null, ""); + Content[3] = new TestContent(type1, 3, -1).WithValues(null, null); + + Root = new TestRootContent(type).WithChildren(1, 2, 3); + } + } + + class TestSource5 : TestSourceBase + { + public TestSource5() + { + LastAttributeIndex = -1; + + var prop1 = new TestPropertyType("prop1"); + var prop2 = new TestPropertyType("prop2"); + var type = new TestRootContentType(this); + var type1 = type.CreateType("type1", prop1, prop2); + + Content[1] = new TestContent(type1, 1, -1).WithValues("p11", "p12").WithChildren(3, 5); + Content[2] = new TestContent(type1, 2, -1).WithValues("p21", "p22").WithChildren(4, 6); + Content[3] = new TestContent(type1, 3, 1).WithValues("p31", "p32"); + Content[4] = new TestContent(type1, 4, 2).WithValues("p41", "p42"); + Content[5] = new TestContent(type1, 5, 1).WithValues("p51", "p52"); + Content[6] = new TestContent(type1, 6, 2).WithValues("p61", "p62"); + + Root = new TestRootContent(type).WithChildren(1, 2); + } + } + + class TestSource6 : TestSourceBase + { + // + // + // + // + // + // + // + // + // blah + // + // bam + // + // + // + // + // + + public TestSource6() + { + LastAttributeIndex = -1; + + var type = new TestRootContentType(this); + var type1 = type.CreateType("wrap", + new TestPropertyType("item1"), + new TestPropertyType("item2"), + new TestPropertyType("item2a"), + new TestPropertyType("item2b"), + new TestPropertyType("item2c"), + new TestPropertyType("item3"), + new TestPropertyType("item3a"), + new TestPropertyType("item4", true), + new TestPropertyType("item5", true) + ); + + Content[1] = new TestContent(type1, 1, -1) + .WithValues( + null, + null, + null, + null, + "\r\n ", + "blah", + "\r\n blah\r\n ", + "bam", + "\r\n " + ); + + Root = new TestRootContent(type).WithChildren(1); + } + } + + class TestSource7 : TestSourceBase + { + public TestSource7() + { + LastAttributeIndex = 0; + + var prop1 = new TestPropertyType("isDoc"); + var prop2 = new TestPropertyType("title"); + var type = new TestRootContentType(this, prop1); + var type1 = type.CreateType("node", prop1, prop2); + + Content[1] = new TestContent(type1, 1, -1).WithValues(1, "title-1").WithChildren(3, 5); + Content[2] = new TestContent(type1, 2, -1).WithValues(1, "title-2").WithChildren(4, 6); + Content[3] = new TestContent(type1, 3, 1).WithValues(1, "title-3").WithChildren(7, 8); + Content[4] = new TestContent(type1, 4, 2).WithValues(1, "title-4"); + Content[5] = new TestContent(type1, 5, 1).WithValues(1, "title-5"); + Content[6] = new TestContent(type1, 6, 2).WithValues(1, "title-6"); + + Content[7] = new TestContent(type1, 7, 3).WithValues(1, "title-7"); + Content[8] = new TestContent(type1, 8, 3).WithValues(1, "title-8"); + + Root = new TestRootContent(type).WithValues(null).WithChildren(1, 2); + } + } + + class TestSource8 : TestSourceBase + { + public TestSource8() + { + LastAttributeIndex = 0; + + var attr = new TestPropertyType("attr"); + var prop = new TestPropertyType("prop"); + var type = new TestRootContentType(this, attr); + var type1 = type.CreateType("item", attr, prop); + Content[1] = new TestContent(type1, 1, -1).WithValues(null, null); + Content[2] = new TestContent(type1, 2, -1).WithValues("", ""); + Content[3] = new TestContent(type1, 3, -1).WithValues(" ", " "); + Content[4] = new TestContent(type1, 4, -1).WithValues("", "\r\n"); + Content[5] = new TestContent(type1, 5, -1).WithValues(" ooo ", " ooo "); + Root = new TestRootContent(type).WithValues(null).WithChildren(1, 2, 3, 4, 5); + } + } + + #endregion +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index e8547ec121..e6c3e0f03e 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -206,6 +206,7 @@ +