1233 lines
47 KiB
C#
1233 lines
47 KiB
C#
// DEBUG
|
|
|
|
// We make sure that diagnostics code will not be compiled nor called into Release configuration.
|
|
// In Debug configuration, diagnostics code can be enabled by defining DEBUGNAVIGATOR below,
|
|
// but by default nothing is writted, unless some lines are un-commented in Debug(...) below.
|
|
//
|
|
// Beware! Diagnostics are extremely verbose and can overflow logging pretty easily.
|
|
|
|
#if DEBUG
|
|
// define to enable diagnostics code
|
|
#undef DEBUGNAVIGATOR
|
|
#endif
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Provides a cursor model for navigating Umbraco data as if it were XML.
|
|
/// </summary>
|
|
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;
|
|
private readonly int _maxDepth;
|
|
|
|
#region Constructor
|
|
|
|
///// <summary>
|
|
///// Initializes a new instance of the <see cref="NavigableNavigator"/> class with a content source.
|
|
///// </summary>
|
|
///// <param name="source">The content source.</param>
|
|
///// <param name="maxDepth">The maximum depth.</param>
|
|
//private NavigableNavigator(INavigableSource source, int maxDepth)
|
|
//{
|
|
// _source = source;
|
|
// _lastAttributeIndex = source.LastAttributeIndex;
|
|
// _maxDepth = maxDepth;
|
|
//}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="NavigableNavigator"/> class with a content source,
|
|
/// and an optional root content.
|
|
/// </summary>
|
|
/// <param name="source">The content source.</param>
|
|
/// <param name="rootId">The root content identifier.</param>
|
|
/// <param name="maxDepth">The maximum depth.</param>
|
|
/// <remarks>When no root content is supplied then the root of the source is used.</remarks>
|
|
public NavigableNavigator(INavigableSource source, int rootId = 0, int maxDepth = int.MaxValue)
|
|
//: this(source, maxDepth)
|
|
{
|
|
_source = source;
|
|
_lastAttributeIndex = source.LastAttributeIndex;
|
|
_maxDepth = maxDepth;
|
|
|
|
_nameTable = new NameTable();
|
|
_lastAttributeIndex = source.LastAttributeIndex;
|
|
var content = rootId <= 0 ? source.Root : source.Get(rootId);
|
|
if (content == null)
|
|
throw new ArgumentException("Not the identifier of a content within the source.", nameof(rootId));
|
|
_state = new State(content, null, null, 0, StatePosition.Root);
|
|
|
|
_contents = new ConcurrentDictionary<int, INavigableContent>();
|
|
}
|
|
|
|
///// <summary>
|
|
///// Initializes a new instance of the <see cref="NavigableNavigator"/> class with a content source, a name table and a state.
|
|
///// </summary>
|
|
///// <param name="source">The content source.</param>
|
|
///// <param name="nameTable">The name table.</param>
|
|
///// <param name="state">The state.</param>
|
|
///// <param name="maxDepth">The maximum depth.</param>
|
|
///// <remarks>Privately used for cloning a navigator.</remarks>
|
|
//private NavigableNavigator(INavigableSource source, XmlNameTable nameTable, State state, int maxDepth)
|
|
// : this(source, rootId: 0, maxDepth: maxDepth)
|
|
//{
|
|
// _nameTable = nameTable;
|
|
// _state = state;
|
|
//}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="NavigableNavigator"/> class as a clone.
|
|
/// </summary>
|
|
/// <param name="orig">The cloned navigator.</param>
|
|
/// <param name="state">The clone state.</param>
|
|
/// <param name="maxDepth">The clone maximum depth.</param>
|
|
/// <remarks>Privately used for cloning a navigator.</remarks>
|
|
private NavigableNavigator(NavigableNavigator orig, State state = null, int maxDepth = -1)
|
|
: this(orig._source, rootId: 0, maxDepth: orig._maxDepth)
|
|
{
|
|
_nameTable = orig._nameTable;
|
|
|
|
_state = state ?? orig._state.Clone();
|
|
if (state != null && maxDepth < 0)
|
|
throw new ArgumentException("Both state and maxDepth are required.");
|
|
_maxDepth = maxDepth < 0 ? orig._maxDepth : maxDepth;
|
|
|
|
_contents = orig._contents;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Diagnostics
|
|
|
|
#if DEBUGNAVIGATOR
|
|
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
|
|
|
|
// About conditional methods: marking a method with the [Conditional] attribute ensures
|
|
// that no calls to the method will be generated by the compiler. However, the method
|
|
// does exist. Wrapping the method body with #if/endif ensures that no IL is generated
|
|
// and so it's only an empty method.
|
|
|
|
[Conditional("DEBUGNAVIGATOR")]
|
|
void DebugEnter(string name)
|
|
{
|
|
#if DEBUGNAVIGATOR
|
|
Debug("");
|
|
DebugState(":");
|
|
Debug(name);
|
|
_tabs = Math.Min(Tabs.Length, _tabs + 2);
|
|
#endif
|
|
}
|
|
|
|
[Conditional("DEBUGNAVIGATOR")]
|
|
void DebugCreate(NavigableNavigator nav)
|
|
{
|
|
#if DEBUGNAVIGATOR
|
|
Debug("Create: [NavigableNavigator::{0}]", nav._uid);
|
|
#endif
|
|
}
|
|
|
|
[Conditional("DEBUGNAVIGATOR")]
|
|
private void DebugReturn()
|
|
{
|
|
#if DEBUGNAVIGATOR
|
|
// ReSharper disable IntroduceOptionalParameters.Local
|
|
DebugReturn("(void)");
|
|
// ReSharper restore IntroduceOptionalParameters.Local
|
|
#endif
|
|
}
|
|
|
|
[Conditional("DEBUGNAVIGATOR")]
|
|
private void DebugReturn(bool value)
|
|
{
|
|
#if DEBUGNAVIGATOR
|
|
DebugReturn(value ? "true" : "false");
|
|
#endif
|
|
}
|
|
|
|
[Conditional("DEBUGNAVIGATOR")]
|
|
void DebugReturn(string format, params object[] args)
|
|
{
|
|
#if DEBUGNAVIGATOR
|
|
Debug("=> " + format, args);
|
|
if (_tabs > 0) _tabs -= 2;
|
|
#endif
|
|
}
|
|
|
|
[Conditional("DEBUGNAVIGATOR")]
|
|
void DebugState(string s = " =>")
|
|
{
|
|
#if DEBUGNAVIGATOR
|
|
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}' (depth={1}).",
|
|
_state.Content.Type.Name, _state.Depth);
|
|
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 = string.Format("At root (depth={0}).",
|
|
_state.Depth);
|
|
break;
|
|
default:
|
|
throw new InvalidOperationException("Invalid position.");
|
|
}
|
|
|
|
Debug("State{0} {1}", s, position);
|
|
#endif
|
|
}
|
|
|
|
#if DEBUGNAVIGATOR
|
|
void Debug(string format, params object[] args)
|
|
{
|
|
// remove comments to write
|
|
|
|
//format = "[" + _uid.ToString("00000") + "] " + Tabs.Substring(0, _tabs) + format;
|
|
//var msg = string.Format(format, args);
|
|
}
|
|
#endif
|
|
|
|
#endregion
|
|
|
|
#region Source management
|
|
|
|
private readonly ConcurrentDictionary<int, INavigableContent> _contents;
|
|
|
|
private INavigableContent SourceGet(int id)
|
|
{
|
|
// original version, would keep creating INavigableContent objects
|
|
//return _source.Get(id);
|
|
|
|
// improved version, uses a cache, shared with clones
|
|
return _contents.GetOrAdd(id, x => _source.Get(x));
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Gets the underlying content object.
|
|
/// </summary>
|
|
public override object UnderlyingObject => _state.Content;
|
|
|
|
/// <summary>
|
|
/// Creates a new XPathNavigator positioned at the same node as this XPathNavigator.
|
|
/// </summary>
|
|
/// <returns>A new XPathNavigator positioned at the same node as this XPathNavigator.</returns>
|
|
public override XPathNavigator Clone()
|
|
{
|
|
DebugEnter("Clone");
|
|
var nav = new NavigableNavigator(this);
|
|
DebugCreate(nav);
|
|
DebugReturn("[XPathNavigator]");
|
|
return nav;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new XPathNavigator using the same source but positioned at a new root.
|
|
/// </summary>
|
|
/// <returns>A new XPathNavigator using the same source and positioned at a new root.</returns>
|
|
/// <remarks>The new root can be above this navigator's root.</remarks>
|
|
public XPathNavigator CloneWithNewRoot(string id, int maxDepth = int.MaxValue)
|
|
{
|
|
int i;
|
|
if (int.TryParse(id, out i) == false)
|
|
throw new ArgumentException("Not a valid identifier.", nameof(id));
|
|
return CloneWithNewRoot(id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new XPathNavigator using the same source but positioned at a new root.
|
|
/// </summary>
|
|
/// <returns>A new XPathNavigator using the same source and positioned at a new root.</returns>
|
|
/// <remarks>The new root can be above this navigator's root.</remarks>
|
|
public XPathNavigator CloneWithNewRoot(int id, int maxDepth = int.MaxValue)
|
|
{
|
|
DebugEnter("CloneWithNewRoot");
|
|
|
|
State state = null;
|
|
|
|
if (id <= 0)
|
|
{
|
|
state = new State(_source.Root, null, null, 0, StatePosition.Root);
|
|
}
|
|
else
|
|
{
|
|
var content = SourceGet(id);
|
|
if (content != null)
|
|
{
|
|
state = new State(content, null, null, 0, StatePosition.Root);
|
|
}
|
|
}
|
|
|
|
NavigableNavigator clone = null;
|
|
|
|
if (state != null)
|
|
{
|
|
clone = new NavigableNavigator(this, state, maxDepth);
|
|
DebugCreate(clone);
|
|
DebugReturn("[XPathNavigator]");
|
|
}
|
|
else
|
|
{
|
|
DebugReturn("[null]");
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the current node is an empty element without an end element tag.
|
|
/// </summary>
|
|
public override bool IsEmptyElement
|
|
{
|
|
get
|
|
{
|
|
DebugEnter("IsEmptyElement");
|
|
bool isEmpty;
|
|
|
|
switch (_state.Position)
|
|
{
|
|
case StatePosition.Element:
|
|
// must go through source because of preview/published ie there may be
|
|
// ids but corresponding to preview elements that we don't see here
|
|
var hasContentChild = _state.GetContentChildIds(_maxDepth).Any(x => SourceGet(x) != null);
|
|
isEmpty = (hasContentChild == false) // 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the current XPathNavigator is at the same position as the specified XPathNavigator.
|
|
/// </summary>
|
|
/// <param name="nav">The XPathNavigator to compare to this XPathNavigator.</param>
|
|
/// <returns>true if the two XPathNavigator objects have the same position; otherwise, false.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the qualified name of the current node.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the Name of the current node without any namespace prefix.
|
|
/// </summary>
|
|
public override string LocalName
|
|
{
|
|
get
|
|
{
|
|
DebugEnter("LocalName");
|
|
var name = Name;
|
|
DebugReturn("\"{0}\"", name);
|
|
return name;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the same position as the specified XPathNavigator.
|
|
/// </summary>
|
|
/// <param name="nav">The XPathNavigator positioned on the node that you want to move to. </param>
|
|
/// <returns>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.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the first attribute of the current node.
|
|
/// </summary>
|
|
/// <returns>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.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the first child node of the current node.
|
|
/// </summary>
|
|
/// <returns>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.</returns>
|
|
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.GetContentChildIds(_maxDepth);
|
|
|
|
if (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 => SourceGet(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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the first namespace node that matches the XPathNamespaceScope specified.
|
|
/// </summary>
|
|
/// <param name="namespaceScope">An XPathNamespaceScope value describing the namespace scope. </param>
|
|
/// <returns>Returns true if the XPathNavigator is successful moving to the first namespace node;
|
|
/// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
|
|
public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope)
|
|
{
|
|
DebugEnter("MoveToFirstNamespace");
|
|
DebugReturn(false);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the next namespace node matching the XPathNamespaceScope specified.
|
|
/// </summary>
|
|
/// <param name="namespaceScope">An XPathNamespaceScope value describing the namespace scope. </param>
|
|
/// <returns>Returns true if the XPathNavigator is successful moving to the next namespace node;
|
|
/// otherwise, false. If false, the position of the XPathNavigator is unchanged.</returns>
|
|
public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope)
|
|
{
|
|
DebugEnter("MoveToNextNamespace");
|
|
DebugReturn(false);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves to the node that has an attribute of type ID whose value matches the specified String.
|
|
/// </summary>
|
|
/// <param name="id">A String representing the ID value of the node to which you want to move.</param>
|
|
/// <returns>true if the XPathNavigator is successful moving; otherwise, false.
|
|
/// If false, the position of the navigator is unchanged.</returns>
|
|
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.
|
|
|
|
// navigator may be rooted below source root
|
|
// find the navigator root id
|
|
var state = _state;
|
|
while (state.Parent != null) // root state has no parent
|
|
state = state.Parent;
|
|
var navRootId = state.Content.Id;
|
|
|
|
int contentId;
|
|
if (int.TryParse(id, out contentId))
|
|
{
|
|
if (contentId == navRootId)
|
|
{
|
|
_state = new State(state.Content, null, null, 0, StatePosition.Element);
|
|
succ = true;
|
|
}
|
|
else
|
|
{
|
|
var content = SourceGet(contentId);
|
|
if (content != null)
|
|
{
|
|
// walk up to the navigator's root - or the source's root
|
|
var s = new Stack<INavigableContent>();
|
|
while (content != null && content.ParentId != navRootId)
|
|
{
|
|
s.Push(content);
|
|
content = SourceGet(content.ParentId);
|
|
}
|
|
|
|
if (content != null && s.Count < _maxDepth)
|
|
{
|
|
_state = new State(state.Content, null, null, 0, StatePosition.Element);
|
|
while (content != null)
|
|
{
|
|
_state = new State(content, _state, _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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the next sibling node of the current node.
|
|
/// </summary>
|
|
/// <returns>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.</returns>
|
|
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 = SourceGet(_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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the previous sibling node of the current node.
|
|
/// </summary>
|
|
/// <returns>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.</returns>
|
|
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 = SourceGet(_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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the next attribute.
|
|
/// </summary>
|
|
/// <returns>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.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the parent node of the current node.
|
|
/// </summary>
|
|
/// <returns>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.</returns>
|
|
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 == false)
|
|
{
|
|
_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() == false)
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the XPathNavigator to the root node that the current node belongs to.
|
|
/// </summary>
|
|
public override void MoveToRoot()
|
|
{
|
|
DebugEnter("MoveToRoot");
|
|
|
|
while (_state.Parent != null)
|
|
_state = _state.Parent;
|
|
DebugState();
|
|
|
|
DebugReturn();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the base URI for the current node.
|
|
/// </summary>
|
|
public override string BaseURI => string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets the XmlNameTable of the XPathNavigator.
|
|
/// </summary>
|
|
public override XmlNameTable NameTable => _nameTable;
|
|
|
|
/// <summary>
|
|
/// Gets the namespace URI of the current node.
|
|
/// </summary>
|
|
public override string NamespaceURI => string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets the XPathNodeType of the current node.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the namespace prefix associated with the current node.
|
|
/// </summary>
|
|
public override string Prefix => string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets the string value of the item.
|
|
/// </summary>
|
|
/// <remarks>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.</remarks>
|
|
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 => _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<int> siblings, int siblingIndex, StatePosition position)
|
|
: this(position)
|
|
{
|
|
Content = content;
|
|
Parent = parent;
|
|
Depth = parent?.Depth + 1 ?? 0;
|
|
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;
|
|
Depth = other.Depth;
|
|
|
|
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 depth
|
|
public int Depth { get; }
|
|
|
|
// the current content
|
|
private INavigableContent _content;
|
|
|
|
// the current content
|
|
public INavigableContent Content
|
|
{
|
|
get
|
|
{
|
|
return _content;
|
|
}
|
|
set
|
|
{
|
|
FieldsCount = value?.Type.FieldTypes.Length ?? 0;
|
|
_content = value;
|
|
}
|
|
}
|
|
|
|
private static readonly int[] NoChildIds = new int[0];
|
|
|
|
// the current content child ids
|
|
public IList<int> GetContentChildIds(int maxDepth)
|
|
{
|
|
return Depth < maxDepth && _content.ChildIds != null ? _content.ChildIds : NoChildIds;
|
|
}
|
|
|
|
// 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<int> Siblings { get; }
|
|
|
|
// 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 => 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
|
|
}
|
|
}
|