Files
Umbraco-CMS/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs
2017-10-31 12:48:24 +01:00

476 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Composing;
using Umbraco.Web.Models;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <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, ICacheProvider cacheProvider, PublishedContentTypeCache contentTypeCache)
{
_xmlNode = xmlNode;
_isPreviewing = isPreviewing;
_cacheProvider = cacheProvider;
_contentTypeCache = contentTypeCache;
}
private readonly XmlNode _xmlNode;
private readonly bool _isPreviewing;
private readonly ICacheProvider _cacheProvider; // at snapshot/request level (see PublishedContentCache)
private readonly PublishedContentTypeCache _contentTypeCache;
private bool _nodeInitialized;
private bool _parentInitialized;
private bool _childrenInitialized;
private IEnumerable<IPublishedContent> _children = Enumerable.Empty<IPublishedContent>();
private IPublishedContent _parent;
private PublishedContentType _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 string _writerName;
private string _creatorName;
private int _writerId;
private int _creatorId;
private string _urlName;
private string _path;
private DateTime _createDate;
private DateTime _updateDate;
private Guid _version;
private int _sortOrder;
private int _level;
private bool _isDraft;
public override IEnumerable<IPublishedContent> Children
{
get
{
if (_nodeInitialized == false) InitializeNode();
if (_childrenInitialized == false) InitializeChildren();
return _children;
}
}
public override IPublishedProperty GetProperty(string alias)
{
if (_nodeInitialized == false) InitializeNode();
IPublishedProperty property;
return _properties.TryGetValue(alias, out property) ? property : null;
}
// override to implement cache
// cache at context level, ie once for the whole request
// but cache is not shared by requests because we wouldn't know how to clear it
public override IPublishedProperty GetProperty(string alias, bool recurse)
{
if (recurse == false) return GetProperty(alias);
var key = $"XmlPublishedCache.PublishedContentCache:RecursiveProperty-{Id}-{alias.ToLowerInvariant()}";
var cacheProvider = _cacheProvider;
return cacheProvider.GetCacheItem<IPublishedProperty>(key, () => base.GetProperty(alias, true));
// note: cleared by PublishedContentCache.Resync - any change here must be applied there
}
public override PublishedItemType ItemType => PublishedItemType.Content;
public override IPublishedContent Parent
{
get
{
if (_nodeInitialized == false) InitializeNode();
if (_parentInitialized == false) InitializeParent();
return _parent;
}
}
public override int Id
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _id;
}
}
public override Guid Key
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _key;
}
}
public override int TemplateId
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _template;
}
}
public override int SortOrder
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _sortOrder;
}
}
public override string Name
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _name;
}
}
public override string DocumentTypeAlias
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _docTypeAlias;
}
}
public override int DocumentTypeId
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _docTypeId;
}
}
public override string WriterName
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _writerName;
}
}
public override string CreatorName
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _creatorName;
}
}
public override int WriterId
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _writerId;
}
}
public override int CreatorId
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _creatorId;
}
}
public override string Path
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _path;
}
}
public override DateTime CreateDate
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _createDate;
}
}
public override DateTime UpdateDate
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _updateDate;
}
}
public override Guid Version
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _version;
}
}
public override string UrlName
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _urlName;
}
}
public override int Level
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _level;
}
}
public override bool IsDraft
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _isDraft;
}
}
public override IEnumerable<IPublishedProperty> Properties
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _properties.Values;
}
}
public override PublishedContentType ContentType
{
get
{
if (_nodeInitialized == false) InitializeNode();
return _contentType;
}
}
private void InitializeParent()
{
var parent = _xmlNode?.ParentNode;
if (parent == null) return;
if (parent.Attributes?.GetNamedItem("isDoc") != null)
_parent = Get(parent, _isPreviewing, _cacheProvider, _contentTypeCache);
// warn: this is not thread-safe...
_parentInitialized = true;
}
private void InitializeNode()
{
InitializeNode(this, _xmlNode, _isPreviewing,
out _id, out _key, out _template, out _sortOrder, out _name, out _writerName,
out _urlName, out _creatorName, out _creatorId, out _writerId, out _docTypeAlias, out _docTypeId, out _path,
out _version, out _createDate, out _updateDate, out _level, out _isDraft, out _contentType, out _properties,
_contentTypeCache.Get);
// warn: this is not thread-safe...
_nodeInitialized = true;
}
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 writerName, out string urlName,
out string creatorName, out int creatorId, out int writerId, out string docTypeAlias, out int docTypeId, out string path,
out Guid version, out DateTime createDate, out DateTime updateDate, out int level, out bool isDraft,
out PublishedContentType contentType, out Dictionary<string, IPublishedProperty> properties,
Func<PublishedItemType, string, PublishedContentType> getPublishedContentType)
{
//initialize the out params with defaults:
writerName = null;
docTypeAlias = null;
id = template = sortOrder = template = creatorId = writerId = docTypeId = level = default(int);
key = version = default(Guid);
name = writerName = urlName = creatorName = docTypeAlias = 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("writerName") != null)
writerName = xmlNode.Attributes.GetNamedItem("writerName").Value;
if (xmlNode.Attributes.GetNamedItem("urlName") != null)
urlName = xmlNode.Attributes.GetNamedItem("urlName").Value;
if (xmlNode.Attributes.GetNamedItem("creatorName") != null)
creatorName = xmlNode.Attributes.GetNamedItem("creatorName").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("version") != null)
version = new Guid(xmlNode.Attributes.GetNamedItem("version").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)
{
// fixme - 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.PropertyTypeAlias.ToLowerInvariant(), out XmlNode n)
? new XmlPublishedProperty(propertyType, node, isPreviewing, n)
: new XmlPublishedProperty(propertyType, node, isPreviewing);
properties[propertyType.PropertyTypeAlias] = 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, _cacheProvider, _contentTypeCache))
.OrderBy(x => x.SortOrder)
.ToList();
// warn: this is not thread-safe
_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="cacheProvider">A cache provider.</param>
/// <param name="contentTypeCache">A content type cache.</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, ICacheProvider cacheProvider, PublishedContentTypeCache contentTypeCache)
{
// 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) cacheProvider.GetCacheItem(key, () => (new XmlPublishedContent(node, isPreviewing, cacheProvider, contentTypeCache)).CreateModel());
}
public static void ClearRequest()
{
Current.ApplicationCache.RequestCache.ClearCacheByKeySearch(CacheKeyPrefix);
}
private const string CacheKeyPrefix = "CONTENTCACHE_XMLPUBLISHEDCONTENT_";
}
}