Files
Umbraco-CMS/tests/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedContent.cs
Paul Johnson 00133e880d Move test projects from src/ to tests/ (#11357)
* Update gitignore

* Move csproj

* Update project references

* Update solutions

* Update build scripts

* Tests used to share editorconfig with projects in src

* Fix broken tests.

* Stop copying around .editorconfig

merged root one with linting

* csharp_style_expression_bodied -> suggestion

* Move StyleCop rulesets to matching directories and update shared build properties

* Remove legacy build files, update NuGet.cofig and solution files

* Restore myget source

* Clean up .gitignore

* Update .gitignore

* Move new test classes to tests after merge

* Gitignore + nuget config

* Move new test

Co-authored-by: Ronald Barendse <ronald@barend.se>
2021-10-18 08:14:04 +01:00

442 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
using Umbraco.Web.Composing;
namespace Umbraco.Tests.LegacyXmlPublishedCache
{
/// <summary>
/// Represents an IPublishedContent which is created based on an Xml structure.
/// </summary>
[Serializable]
[XmlType(Namespace = "http://umbraco.org/webservices/")]
internal class XmlPublishedContent : PublishedContentBase
{
private XmlPublishedContent(
XmlNode xmlNode,
bool isPreviewing,
IAppCache appCache,
PublishedContentTypeCache contentTypeCache,
IVariationContextAccessor variationContextAccessor): base(variationContextAccessor)
{
_xmlNode = xmlNode;
_isPreviewing = isPreviewing;
_appCache = appCache;
_contentTypeCache = contentTypeCache;
_variationContextAccessor = variationContextAccessor;
}
private readonly XmlNode _xmlNode;
private readonly bool _isPreviewing;
private readonly IAppCache _appCache; // at snapshot/request level (see PublishedContentCache)
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly object _initializeLock = new object();
private bool _nodeInitialized;
private bool _parentInitialized;
private bool _childrenInitialized;
private IEnumerable<IPublishedContent> _children = Enumerable.Empty<IPublishedContent>();
private IPublishedContent _parent;
private IPublishedContentType _contentType;
private Dictionary<string, IPublishedProperty> _properties;
private int _id;
private Guid _key;
private int _template;
private string _name;
private string _docTypeAlias;
private int _docTypeId;
private int _writerId;
private int _creatorId;
private string _urlName;
private string _path;
private DateTime _createDate;
private DateTime _updateDate;
private int _sortOrder;
private int _level;
private bool _isDraft;
public override IEnumerable<IPublishedContent> Children
{
get
{
EnsureNodeInitialized(andChildren: true);
return _children;
}
}
public override IEnumerable<IPublishedContent> ChildrenForAllCultures => Children;
public override IPublishedProperty GetProperty(string alias)
{
EnsureNodeInitialized();
IPublishedProperty property;
return _properties.TryGetValue(alias, out property) ? property : null;
}
public override PublishedItemType ItemType => PublishedItemType.Content;
public override IPublishedContent Parent
{
get
{
EnsureNodeInitialized(andParent: true);
return _parent;
}
}
public override int Id
{
get
{
EnsureNodeInitialized();
return _id;
}
}
public override Guid Key
{
get
{
EnsureNodeInitialized();
return _key;
}
}
public override int? TemplateId
{
get
{
EnsureNodeInitialized();
return _template;
}
}
public override int SortOrder
{
get
{
EnsureNodeInitialized();
return _sortOrder;
}
}
public override string Name
{
get
{
EnsureNodeInitialized();
return _name;
}
}
private Dictionary<string, PublishedCultureInfo> _cultures;
private Dictionary<string, PublishedCultureInfo> GetCultures()
{
EnsureNodeInitialized();
return new Dictionary<string, PublishedCultureInfo> { { "", new PublishedCultureInfo("", _name, _urlName, _updateDate) } };
}
public override IReadOnlyDictionary<string, PublishedCultureInfo> Cultures => _cultures ?? (_cultures = GetCultures());
public override int WriterId
{
get
{
EnsureNodeInitialized();
return _writerId;
}
}
public override int CreatorId
{
get
{
EnsureNodeInitialized();
return _creatorId;
}
}
public override string Path
{
get
{
EnsureNodeInitialized();
return _path;
}
}
public override DateTime CreateDate
{
get
{
EnsureNodeInitialized();
return _createDate;
}
}
public override DateTime UpdateDate
{
get
{
EnsureNodeInitialized();
return _updateDate;
}
}
public override string UrlSegment
{
get
{
EnsureNodeInitialized();
return _urlName;
}
}
public override int Level
{
get
{
EnsureNodeInitialized();
return _level;
}
}
public override bool IsDraft(string culture = null)
{
EnsureNodeInitialized();
return _isDraft; // bah
}
public override bool IsPublished(string culture = null)
{
EnsureNodeInitialized();
return true; // Intentionally not implemented, because the XmlPublishedContent should not support this.
}
public override IEnumerable<IPublishedProperty> Properties
{
get
{
EnsureNodeInitialized();
return _properties.Values;
}
}
public override IPublishedContentType ContentType
{
get
{
EnsureNodeInitialized();
return _contentType;
}
}
private void InitializeParent()
{
var parent = _xmlNode?.ParentNode;
if (parent == null) return;
if (parent.Attributes?.GetNamedItem("isDoc") != null)
_parent = Get(parent, _isPreviewing, _appCache, _contentTypeCache, _variationContextAccessor);
_parentInitialized = true;
}
private void EnsureNodeInitialized(bool andChildren = false, bool andParent = false)
{
// In *theory* XmlPublishedContent are a per-request thing, and so should not
// end up being involved into multi-threaded situations - however, it's been
// reported that some users ended up seeing 100% CPU due to infinite loops in
// the properties dictionary in InitializeNode, which would indicate that the
// dictionary *is* indeed involved in some multi-threaded operation. No idea
// what users are doing that cause this, but let's be friendly and use a true
// lock around initialization.
lock (_initializeLock)
{
if (_nodeInitialized == false) InitializeNode();
if (andChildren && _childrenInitialized == false) InitializeChildren();
if (andParent && _parentInitialized == false) InitializeParent();
}
}
private void InitializeNode()
{
InitializeNode(this, _xmlNode, _isPreviewing,
out _id, out _key, out _template, out _sortOrder, out _name,
out _urlName, out _creatorId, out _writerId, out _docTypeAlias, out _docTypeId, out _path,
out _createDate, out _updateDate, out _level, out _isDraft, out _contentType, out _properties,
_contentTypeCache.Get);
_nodeInitialized = true;
}
// internal for some benchmarks
internal static void InitializeNode(XmlPublishedContent node, XmlNode xmlNode, bool isPreviewing,
out int id, out Guid key, out int template, out int sortOrder, out string name, out string urlName,
out int creatorId, out int writerId, out string docTypeAlias, out int docTypeId, out string path,
out DateTime createDate, out DateTime updateDate, out int level, out bool isDraft,
out IPublishedContentType contentType, out Dictionary<string, IPublishedProperty> properties,
Func<PublishedItemType, string, IPublishedContentType> getPublishedContentType)
{
//initialize the out params with defaults:
docTypeAlias = null;
id = template = sortOrder = template = creatorId = writerId = docTypeId = level = default(int);
key = default(Guid);
name = docTypeAlias = urlName = path = null;
createDate = updateDate = default(DateTime);
isDraft = false;
contentType = null;
properties = null;
if (xmlNode == null) return;
if (xmlNode.Attributes != null)
{
id = int.Parse(xmlNode.Attributes.GetNamedItem("id").Value);
if (xmlNode.Attributes.GetNamedItem("key") != null) // because, migration
key = Guid.Parse(xmlNode.Attributes.GetNamedItem("key").Value);
if (xmlNode.Attributes.GetNamedItem("template") != null)
template = int.Parse(xmlNode.Attributes.GetNamedItem("template").Value);
if (xmlNode.Attributes.GetNamedItem("sortOrder") != null)
sortOrder = int.Parse(xmlNode.Attributes.GetNamedItem("sortOrder").Value);
if (xmlNode.Attributes.GetNamedItem("nodeName") != null)
name = xmlNode.Attributes.GetNamedItem("nodeName").Value;
if (xmlNode.Attributes.GetNamedItem("urlName") != null)
urlName = xmlNode.Attributes.GetNamedItem("urlName").Value;
//Added the actual userID, as a user cannot be looked up via full name only...
if (xmlNode.Attributes.GetNamedItem("creatorID") != null)
creatorId = int.Parse(xmlNode.Attributes.GetNamedItem("creatorID").Value);
if (xmlNode.Attributes.GetNamedItem("writerID") != null)
writerId = int.Parse(xmlNode.Attributes.GetNamedItem("writerID").Value);
docTypeAlias = xmlNode.Name;
if (xmlNode.Attributes.GetNamedItem("nodeType") != null)
docTypeId = int.Parse(xmlNode.Attributes.GetNamedItem("nodeType").Value);
if (xmlNode.Attributes.GetNamedItem("path") != null)
path = xmlNode.Attributes.GetNamedItem("path").Value;
if (xmlNode.Attributes.GetNamedItem("createDate") != null)
createDate = DateTime.Parse(xmlNode.Attributes.GetNamedItem("createDate").Value);
if (xmlNode.Attributes.GetNamedItem("updateDate") != null)
updateDate = DateTime.Parse(xmlNode.Attributes.GetNamedItem("updateDate").Value);
if (xmlNode.Attributes.GetNamedItem("level") != null)
level = int.Parse(xmlNode.Attributes.GetNamedItem("level").Value);
isDraft = xmlNode.Attributes.GetNamedItem("isDraft") != null;
}
//dictionary to store the property node data
var propertyNodes = new Dictionary<string, XmlNode>();
foreach (XmlNode n in xmlNode.ChildNodes)
{
var e = n as XmlElement;
if (e == null) continue;
if (e.HasAttribute("isDoc") == false)
{
PopulatePropertyNodes(propertyNodes, e, false);
}
else break; //we are not longer on property elements
}
//lookup the content type and create the properties collection
try
{
contentType = getPublishedContentType(PublishedItemType.Content, docTypeAlias);
}
catch (InvalidOperationException e)
{
// TODO: enable!
//content.Instance.RefreshContentFromDatabase();
throw new InvalidOperationException($"{e.Message}. This usually indicates that the content cache is corrupt; the content cache has been rebuilt in an attempt to self-fix the issue.");
}
//fill in the property collection
properties = new Dictionary<string, IPublishedProperty>(StringComparer.OrdinalIgnoreCase);
foreach (var propertyType in contentType.PropertyTypes)
{
var val = propertyNodes.TryGetValue(propertyType.Alias.ToLowerInvariant(), out XmlNode n)
? new XmlPublishedProperty(propertyType, node, isPreviewing, n)
: new XmlPublishedProperty(propertyType, node, isPreviewing);
properties[propertyType.Alias] = val;
}
}
private static void PopulatePropertyNodes(IDictionary<string, XmlNode> propertyNodes, XmlNode n, bool legacy)
{
var attrs = n.Attributes;
if (attrs == null) return;
var alias = legacy
? attrs.GetNamedItem("alias").Value
: n.Name;
propertyNodes[alias.ToLowerInvariant()] = n;
}
private void InitializeChildren()
{
if (_xmlNode == null) return;
// load children
const string childXPath = "* [@isDoc]";
var nav = _xmlNode.CreateNavigator();
var expr = nav.Compile(childXPath);
//expr.AddSort("@sortOrder", XmlSortOrder.Ascending, XmlCaseOrder.None, "", XmlDataType.Number);
var iterator = nav.Select(expr);
_children = iterator.Cast<XPathNavigator>()
.Select(n => Get(((IHasXmlNode) n).GetNode(), _isPreviewing, _appCache, _contentTypeCache, _variationContextAccessor))
.OrderBy(x => x.SortOrder)
.ToList();
_childrenInitialized = true;
}
/// <summary>
/// Gets an IPublishedContent corresponding to an Xml cache node.
/// </summary>
/// <param name="node">The Xml node.</param>
/// <param name="isPreviewing">A value indicating whether we are previewing or not.</param>
/// <param name="appCache">A cache.</param>
/// <param name="contentTypeCache">A content type cache.</param>
/// <param name="umbracoContextAccessor">A umbraco context accessor</param>
/// <param name="variationContextAccessor"></param>
/// <returns>The IPublishedContent corresponding to the Xml cache node.</returns>
/// <remarks>Maintains a per-request cache of IPublishedContent items in order to make
/// sure that we create only one instance of each for the duration of a request. The
/// returned IPublishedContent is a model, if models are enabled.</remarks>
public static IPublishedContent Get(XmlNode node, bool isPreviewing, IAppCache appCache,
PublishedContentTypeCache contentTypeCache, IVariationContextAccessor variationContextAccessor)
{
// only 1 per request
var attrs = node.Attributes;
var id = attrs?.GetNamedItem("id").Value;
if (id.IsNullOrWhiteSpace()) throw new InvalidOperationException("Node has no ID attribute.");
var key = CacheKeyPrefix + id; // dont bother with preview, wont change during request in Xml cache
return (IPublishedContent) appCache.Get(key, () => (new XmlPublishedContent(node, isPreviewing, appCache, contentTypeCache, variationContextAccessor)).CreateModel(Current.PublishedModelFactory));
}
private const string CacheKeyPrefix = "CONTENTCACHE_XMLPUBLISHEDCONTENT_";
}
}