U4-5301 - fix AppendDocumentXml + nodes sorting

This commit is contained in:
Stephan
2014-09-05 12:16:04 +02:00
parent 549f0555ab
commit 322c3e0664
5 changed files with 281 additions and 117 deletions

View File

@@ -128,59 +128,115 @@ namespace Umbraco.Core
}
/// <summary>
/// Sorts the children of the parentNode that match the xpath selector
/// </summary>
/// <param name="parentNode"></param>
/// <param name="childXPathSelector">An xpath expression used to select child nodes of the XmlElement</param>
/// <param name="childSelector">An expression that returns true if the XElement passed in is a valid child node to be sorted</param>
/// <param name="orderByValue">The value to order the results by</param>
internal static void SortNodes(
/// Sorts the children of a parentNode.
/// </summary>
/// <param name="parentNode">The parent node.</param>
/// <param name="childNodesXPath">An XPath expression to select children of <paramref name="parentNode"/> to sort.</param>
/// <param name="orderBy">A function returning the value to order the nodes by.</param>
internal static void SortNodes(
XmlNode parentNode,
string childXPathSelector,
Func<XElement, bool> childSelector,
Func<XElement, object> orderByValue)
string childNodesXPath,
Func<XmlNode, int> orderBy)
{
var sortedChildNodes = parentNode.SelectNodes(childNodesXPath).Cast<XmlNode>()
.OrderBy(orderBy)
.ToArray();
// append child nodes to last position, in sort-order
// so all child nodes will go after the property nodes
foreach (var node in sortedChildNodes)
parentNode.AppendChild(node); // moves the node to the last position
}
/// <summary>
/// Sorts the children of a parentNode if needed.
/// </summary>
/// <param name="parentNode">The parent node.</param>
/// <param name="childNodesXPath">An XPath expression to select children of <paramref name="parentNode"/> to sort.</param>
/// <param name="orderBy">A function returning the value to order the nodes by.</param>
/// <returns>A value indicating whether sorting was needed.</returns>
/// <remarks>same as SortNodes but will do nothing if nodes are already sorted - should improve performances.</remarks>
internal static bool SortNodesIfNeeded(
XmlNode parentNode,
string childNodesXPath,
Func<XmlNode, int> orderBy)
{
// ensure orderBy runs only once per node
// checks whether nodes are already ordered
// and actually sorts only if needed
var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath).Cast<XmlNode>()
.Select(x => Tuple.Create(x, orderBy(x))).ToArray();
var a = 0;
foreach (var x in childNodesAndOrder)
{
if (a > x.Item2)
{
a = -1;
break;
}
a = x.Item2;
}
if (a >= 0)
return false;
// append child nodes to last position, in sort-order
// so all child nodes will go after the property nodes
foreach (var x in childNodesAndOrder.OrderBy(x => x.Item2))
parentNode.AppendChild(x.Item1); // moves the node to the last position
return true;
}
/// <summary>
/// Sorts a single child node of a parentNode.
/// </summary>
/// <param name="parentNode">The parent node.</param>
/// <param name="childNodesXPath">An XPath expression to select children of <paramref name="parentNode"/> to sort.</param>
/// <param name="node">The child node to sort.</param>
/// <param name="orderBy">A function returning the value to order the nodes by.</param>
/// <returns>A value indicating whether sorting was needed.</returns>
/// <remarks>Assuming all nodes but <paramref name="node"/> are sorted, this will move the node to
/// the right position without moving all the nodes (as SortNodes would do) - should improve perfs.</remarks>
internal static bool SortNode(
XmlNode parentNode,
string childNodesXPath,
XmlNode node,
Func<XmlNode, int> orderBy)
{
var nodeSortOrder = orderBy(node);
var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath).Cast<XmlNode>()
.Select(x => Tuple.Create(x, orderBy(x))).ToArray();
var xElement = parentNode.ToXElement();
var children = xElement.Elements().Where(x => childSelector(x)).ToArray(); //(DONT conver to method group, the build server doesn't like it)
var data = children
.OrderByDescending(orderByValue) //order by the sort order desc
.Select(x => children.IndexOf(x)) //store the current item's index (DONT conver to method group, the build server doesn't like it)
.ToList();
// find the first node with a sortOrder > node.sortOrder
var i = 0;
while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder)
i++;
//get the minimum index that a content node exists in the parent
var minElementIndex = xElement.Elements()
.TakeWhile(x => childSelector(x) == false)
.Count();
//if the minimum index is zero, then it is the very first node inside the parent,
// if it is not, we need to store the child property node that exists just before the
// first content node found so we can insert elements after it when we're sorting.
var insertAfter = minElementIndex == 0 ? null : parentNode.ChildNodes[minElementIndex - 1];
var selectedChildren = parentNode.SelectNodes(childXPathSelector);
if (selectedChildren == null)
// if one was found
if (i < childNodesAndOrder.Length)
{
throw new InvalidOperationException(string.Format("The childXPathSelector value did not return any results {0}", childXPathSelector));
}
var childElements = selectedChildren.Cast<XmlElement>().ToArray();
//iterate over the ndoes starting with the node with the highest sort order.
//then we insert this node at the begginning of the parent so that by the end
//of the iteration the node with the least sort order will be at the top.
foreach (var node in data.Select(index => childElements[index]))
{
if (insertAfter == null)
// and node is just before, we're done already
// else we need to move it right before the node that was found
if (i > 0 && childNodesAndOrder[i - 1].Item1 != node)
{
parentNode.PrependChild(node);
}
else
{
parentNode.InsertAfter(node, insertAfter);
parentNode.InsertBefore(node, childNodesAndOrder[i].Item1);
return true;
}
}
else
{
// and node is the last one, we're done already
// else we need to append it as the last one
if (i > 0 && childNodesAndOrder[i - 1].Item1 != node)
{
parentNode.AppendChild(node);
return true;
}
}
return false;
}
// used by DynamicNode only, see note in TryCreateXPathDocumentFromPropertyValue

View File

@@ -86,8 +86,7 @@ namespace Umbraco.Tests
XmlHelper.SortNodes(
parentNode,
"./* [@id]",
element => element.Attribute("id") != null,
element => element.AttributeValue<int>("sortOrder"));
x => x.AttributeValue<int>("sortOrder"));
watch.Stop();
totalTime += watch.ElapsedMilliseconds;
watch.Reset();
@@ -125,8 +124,7 @@ namespace Umbraco.Tests
XmlHelper.SortNodes(
parentNode,
"./* [@id]",
element => element.Attribute("id") != null,
element => element.AttributeValue<int>("sortOrder"));
x => x.AttributeValue<int>("sortOrder"));
//do assertions just to make sure it is working properly.
var currSort = 0;

View File

@@ -1,5 +1,6 @@
using System;
using System.Web.Script.Serialization;
using umbraco;
using Umbraco.Core.Cache;
using Umbraco.Core.Models;
using System.Linq;
@@ -81,6 +82,7 @@ namespace Umbraco.Web.Cache
public override void Refresh(int id)
{
RuntimeCacheProvider.Current.Delete(typeof(IContent), id);
content.Instance.UpdateSortOrder(id);
base.Refresh(id);
}
@@ -94,6 +96,7 @@ namespace Umbraco.Web.Cache
public override void Refresh(IContent instance)
{
RuntimeCacheProvider.Current.Delete(typeof(IContent), instance.Id);
content.Instance.UpdateSortOrder(instance);
base.Refresh(instance);
}
@@ -112,6 +115,7 @@ namespace Umbraco.Web.Cache
foreach (var payload in DeserializeFromJsonPayload(jsonPayload))
{
RuntimeCacheProvider.Current.Delete(typeof(IContent), payload.Id);
content.Instance.UpdateSortOrder(payload.Id);
}
OnCacheUpdated(Instance, new CacheRefresherEventArgs(jsonPayload, MessageType.RefreshByJson));

View File

@@ -2,12 +2,14 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web;
using System.Xml;
using System.Xml.XPath;
using umbraco.cms.presentation;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
@@ -19,12 +21,14 @@ using umbraco.BusinessLogic.Utils;
using umbraco.cms.businesslogic;
using umbraco.cms.businesslogic.cache;
using umbraco.cms.businesslogic.web;
using Umbraco.Core.Models;
using umbraco.DataLayer;
using umbraco.presentation.nodeFactory;
using Umbraco.Web;
using Action = umbraco.BusinessLogic.Actions.Action;
using Node = umbraco.NodeFactory.Node;
using Umbraco.Core;
using File = System.IO.File;
namespace umbraco
{
@@ -322,7 +326,7 @@ namespace umbraco
}
}
public static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode DocumentNode, XmlNode PublishedNode)
private static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode DocumentNode, XmlNode PublishedNode)
{
// Remove all attributes and data nodes from the published node
PublishedNode.Attributes.RemoveAll();
@@ -353,8 +357,12 @@ namespace umbraco
if (d.Published)
{
var parentId = d.Level == 1 ? -1 : d.Parent.Id;
xmlContentCopy = AppendDocumentXml(d.Id, d.Level, parentId,
GetPreviewOrPublishedNode(d, xmlContentCopy, false), xmlContentCopy);
// fix sortOrder - see note in UpdateSortOrder
var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false);
var attr = ((XmlElement)node).GetAttributeNode("sortOrder");
attr.Value = d.sortOrder.ToString();
xmlContentCopy = AppendDocumentXml(d.Id, d.Level, parentId, node, xmlContentCopy);
// update sitemapprovider
if (updateSitemapProvider && SiteMap.Provider is UmbracoSiteMapProvider)
@@ -382,79 +390,113 @@ namespace umbraco
return xmlContentCopy;
}
public static XmlDocument AppendDocumentXml(int id, int level, int parentId, XmlNode docNode, XmlDocument xmlContentCopy)
// appends a node (docNode) into a cache (xmlContentCopy)
// and returns a cache (not necessarily the original one)
//
internal static XmlDocument AppendDocumentXml(int id, int level, int parentId, XmlNode docNode, XmlDocument xmlContentCopy)
{
// Find the document in the xml cache
// sanity checks
if (id != docNode.AttributeValue<int>("id"))
throw new ArgumentException("Values of id and docNode/@id are different.");
if (parentId != docNode.AttributeValue<int>("parentID"))
throw new ArgumentException("Values of parentId and docNode/@parentID are different.");
// find the document in the cache
XmlNode currentNode = xmlContentCopy.GetElementById(id.ToString());
// if the document is not there already then it's a new document
// we must make sure that its document type exists in the schema
var xmlContentCopy2 = xmlContentCopy;
if (currentNode == null && UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema == false)
{
xmlContentCopy = ValidateSchema(docNode.Name, xmlContentCopy);
// ValidateSchema looks for the doctype in the schema and if not found
// creates a new XML document with a schema containing the doctype. If
// a new cache copy is returned, must import the new node into the new
// copy.
var xmlContentCopy2 = xmlContentCopy;
xmlContentCopy = ValidateSchema(docNode.Name, xmlContentCopy);
if (xmlContentCopy != xmlContentCopy2)
docNode = xmlContentCopy.ImportNode(docNode, true);
}
// Find the parent (used for sortering and maybe creation of new node)
// find the parent
XmlNode parentNode = level == 1
? xmlContentCopy.DocumentElement
: xmlContentCopy.GetElementById(parentId.ToString());
? xmlContentCopy.DocumentElement
: xmlContentCopy.GetElementById(parentId.ToString());
if (parentNode != null)
// no parent = cannot do anything
if (parentNode == null)
return xmlContentCopy;
// define xpath for getting the children nodes (not properties) of a node
var childNodesXPath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema
? "./node"
: "./* [@id]";
// insert/move the node under the parent
if (currentNode == null)
{
if (currentNode == null)
// document not there, new node, append
currentNode = docNode;
parentNode.AppendChild(currentNode);
}
else
{
// document found... we could just copy the currentNode children nodes over under
// docNode, then remove currentNode and insert docNode... the code below tries to
// be clever and faster, though only benchmarking could tell whether it's worth the
// pain...
// first copy current parent ID - so we can compare with target parent
var moving = currentNode.AttributeValue<int>("parentID") != parentId;
if (docNode.Name == currentNode.Name)
{
currentNode = docNode;
parentNode.AppendChild(currentNode);
// name has not changed, safe to just update the current node
// by transfering values eg copying the attributes, and importing the data elements
TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode);
// if moving, move the node to the new parent
// else it's already under the right parent
// (but maybe the sort order has been updated)
if (moving)
parentNode.AppendChild(currentNode); // remove then append to parentNode
}
else
{
//check the current parent id
var currParentId = currentNode.AttributeValue<int>("parentID");
// name has changed, must use docNode (with new name)
// move children nodes from currentNode to docNode (already has properties)
foreach (XmlNode child in currentNode.SelectNodes(childNodesXPath))
docNode.AppendChild(child); // remove then append to docNode
//update the node with it's new values
TransferValuesFromDocumentXmlToPublishedXml(docNode, currentNode);
//If the node is being moved we also need to ensure that it exists under the new parent!
// http://issues.umbraco.org/issue/U4-2312
// we were never checking this before and instead simply changing the parentId value but not
// changing the actual parent.
//check the new parent
if (currParentId != currentNode.AttributeValue<int>("parentID"))
// and put docNode in the right place - if parent has not changed, then
// just replace, else remove currentNode and insert docNode under the right parent
// (but maybe not at the right position due to sort order)
if (moving)
{
//ok, we've actually got to move the node
parentNode.AppendChild(currentNode);
currentNode.ParentNode.RemoveChild(currentNode);
parentNode.AppendChild(docNode);
}
}
// TODO: Update with new schema!
var xpath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema
? "./node"
: "./* [@id]";
var childNodes = parentNode.SelectNodes(xpath);
// Sort the nodes if the added node has a lower sortorder than the last
if (childNodes != null && childNodes.Count > 0)
{
//get the biggest sort order for all children including the one added
var largestSortOrder = childNodes.Cast<XmlNode>().Max(x => x.AttributeValue<int>("sortOrder"));
var currentSortOrder = currentNode.AttributeValue<int>("sortOrder");
//if the current item's sort order is less than the largest sort order in the list then
//we need to resort the xml structure since this item belongs somewhere in the middle.
//http://issues.umbraco.org/issue/U4-509
if (childNodes.Count > 1 && currentSortOrder < largestSortOrder)
else
{
SortNodes(ref parentNode);
// replacing might screw the sort order
parentNode.ReplaceChild(docNode, currentNode);
}
currentNode = docNode;
}
}
return xmlContentCopy;
// if the nodes are not ordered, must sort
// (see U4-509 + has to work with ReplaceChild too)
//XmlHelper.SortNodesIfNeeded(parentNode, childNodesXPath, x => x.AttributeValue<int>("sortOrder"));
// but...
// if we assume that nodes are always correctly sorted
// then we just need to ensure that currentNode is at the right position.
// should be faster that moving all the nodes around.
XmlHelper.SortNode(parentNode, childNodesXPath, currentNode, x => x.AttributeValue<int>("sortOrder"));
return xmlContentCopy;
}
private static XmlNode GetPreviewOrPublishedNode(Document d, XmlDocument xmlContentCopy, bool isPreview)
@@ -472,18 +514,36 @@ namespace umbraco
/// <summary>
/// Sorts the documents.
/// </summary>
/// <param name="parentNode">The parent node.</param>
public static void SortNodes(ref XmlNode parentNode)
/// <param name="parentId">The parent node identifier.</param>
public void SortNodes(int parentId)
{
var xpath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema
? "./node"
: "./* [@id]";
var childNodesXPath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema
? "./node"
: "./* [@id]";
XmlHelper.SortNodes(
parentNode,
xpath,
element => element.Attribute("id") != null,
element => element.AttributeValue<int>("sortOrder"));
lock (XmlContentInternalSyncLock)
{
// modify a clone of the cache because even though we're into the write-lock
// we may have threads reading at the same time. why is this an option?
var wip = UmbracoConfig.For.UmbracoSettings().Content.CloneXmlContent
? CloneXmlDoc(XmlContentInternal)
: XmlContentInternal;
var parentNode = parentId == -1
? XmlContent.DocumentElement
: XmlContent.GetElementById(parentId.ToString(CultureInfo.InvariantCulture));
if (parentNode == null) return;
var sorted = XmlHelper.SortNodesIfNeeded(
parentNode,
childNodesXPath,
x => x.AttributeValue<int>("sortOrder"));
if (sorted == false) return;
XmlContentInternal = wip;
}
}
@@ -531,6 +591,50 @@ namespace umbraco
}
}
internal virtual void UpdateSortOrder(int contentId)
{
var content = ApplicationContext.Current.Services.ContentService.GetById(contentId);
if (content != null) return;
UpdateSortOrder(content);
}
internal virtual void UpdateSortOrder(IContent c)
{
// the XML in database is updated only when content is published, and then
// it contains the sortOrder value at the time the XML was generated. when
// a document with unpublished changes is sorted, then it is simply saved
// (see ContentService) and so the sortOrder has changed but the XML has
// not been updated accordingly.
// this updates the published cache to take care of the situation
// without ContentService having to ... what exactly?
// no need to do it if the content is published without unpublished changes,
// though, because in that case the XML will get re-generated with the
// correct sort order.
if (c.Published)
return;
lock (XmlContentInternalSyncLock)
{
var wip = UmbracoConfig.For.UmbracoSettings().Content.CloneXmlContent
? CloneXmlDoc(XmlContentInternal)
: XmlContentInternal;
var node = wip.GetElementById(c.Id.ToString());
if (node == null) return;
var attr = node.GetAttributeNode("sortOrder");
var sortOrder = c.SortOrder.ToString();
if (attr.Value == sortOrder) return;
// only if node was actually modified
attr.Value = sortOrder;
XmlContentInternal = wip;
// no need to clear any cache
}
}
/// <summary>
/// Updates the document cache for multiple documents
/// </summary>
@@ -1010,6 +1114,13 @@ order by umbracoNode.level, umbracoNode.sortOrder";
int parentId = dr.GetInt("parentId");
string xml = dr.GetString("xml");
// fix sortOrder - see notes in UpdateSortOrder
var tmp = new XmlDocument();
tmp.LoadXml(xml);
var attr = tmp.DocumentElement.GetAttributeNode("sortOrder");
attr.Value = dr.GetInt("sortOrder").ToString();
xml = tmp.InnerXml;
// Call the eventhandler to allow modification of the string
var e1 = new ContentCacheLoadNodeEventArgs();
FireAfterContentCacheDatabaseLoadXmlString(ref xml, e1);

View File

@@ -155,14 +155,9 @@ namespace umbraco.presentation.webservices
// Save content with new sort order and update db+cache accordingly
var sorted = contentService.Sort(sortedContent);
// Refresh sort order on cached xml
XmlNode parentNode = parentId == -1
? content.Instance.XmlContent.DocumentElement
: content.Instance.XmlContent.GetElementById(parentId.ToString(CultureInfo.InvariantCulture));
//only try to do the content sort if the the parent node is available...
if (parentNode != null)
content.SortNodes(ref parentNode);
// refresh sort order on cached xml
// but no... this is not distributed - solely relying on content service & events should be enough
//content.Instance.SortNodes(parentId);
//send notifications! TODO: This should be put somewhere centralized instead of hard coded directly here
ApplicationContext.Services.NotificationService.SendNotification(contentService.GetById(parentId), ActionSort.Instance, UmbracoContext, ApplicationContext);