2018-06-29 19:52:40 +02:00
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.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 ;
2018-04-24 01:31:01 +10:00
}
2018-06-29 19:52:40 +02:00
private readonly XmlNode _xmlNode ;
private readonly bool _isPreviewing ;
private readonly ICacheProvider _cacheProvider ; // at snapshot/request level (see PublishedContentCache)
private readonly PublishedContentTypeCache _contentTypeCache ;
2018-10-22 16:03:01 +11:00
private readonly object _initializeLock = new object ();
2018-06-29 19:52:40 +02:00
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 int _sortOrder ;
private int _level ;
private bool _isDraft ;
public override IEnumerable < IPublishedContent > Children
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ( andChildren : true );
2018-06-29 19:52:40 +02:00
return _children ;
}
}
public override IPublishedProperty GetProperty ( string alias )
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
IPublishedProperty property ;
return _properties . TryGetValue ( alias , out property ) ? property : null ;
}
public override PublishedItemType ItemType => PublishedItemType . Content ;
public override IPublishedContent Parent
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ( andParent : true );
2018-06-29 19:52:40 +02:00
return _parent ;
}
}
public override int Id
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _id ;
}
}
public override Guid Key
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _key ;
}
}
public override int TemplateId
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _template ;
}
}
public override int SortOrder
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _sortOrder ;
}
}
public override string Name
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _name ;
}
}
public override PublishedCultureInfo GetCulture ( string culture = null ) => throw new NotSupportedException ();
public override IReadOnlyDictionary < string , PublishedCultureInfo > Cultures => throw new NotSupportedException ();
public override string WriterName
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _writerName ;
}
}
public override string CreatorName
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _creatorName ;
}
}
public override int WriterId
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _writerId ;
}
}
public override int CreatorId
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _creatorId ;
}
}
public override string Path
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _path ;
}
}
public override DateTime CreateDate
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _createDate ;
}
}
public override DateTime UpdateDate
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _updateDate ;
}
}
public override string UrlSegment
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _urlName ;
}
}
public override int Level
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _level ;
}
}
public override bool IsDraft
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _isDraft ;
}
}
public override IEnumerable < IPublishedProperty > Properties
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
return _properties . Values ;
}
}
public override PublishedContentType ContentType
{
get
{
2018-10-22 16:03:01 +11:00
EnsureNodeInitialized ();
2018-06-29 19:52:40 +02:00
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 );
_parentInitialized = true ;
}
2018-10-22 16:03:01 +11:00
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 ();
}
}
2018-06-29 19:52:40 +02:00
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 _createDate , out _updateDate , out _level , out _isDraft , out _contentType , out _properties ,
_contentTypeCache . Get );
_nodeInitialized = true ;
}
2018-10-22 16:03:01 +11:00
// internal for some benchmarks
2018-06-29 19:52:40 +02:00
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 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 = 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 ( "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 . 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 , _cacheProvider , _contentTypeCache ))
. 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="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_" ;
}
}