Moves the Xml published cache to the tests project since that is the only place it is used, removes the xml data integrity check since it's not needed, removes the xml published cache benchmark since it's not needed. Now we don't include the old xml published cache at all in the shipped product.

This commit is contained in:
Shannon
2019-01-30 17:50:13 +11:00
parent 3371dbaa48
commit 134cc176ff
35 changed files with 66 additions and 535 deletions

View File

@@ -265,10 +265,6 @@ namespace Umbraco.Web.Editors
"examineMgmtBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ExamineManagementController>(
controller => controller.GetIndexerDetails())
},
{
"xmlDataIntegrityBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<XmlDataIntegrityController>(
controller => controller.CheckContentXmlTable())
},
{
"healthCheckBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<HealthCheckController>(
controller => controller.GetAllHealthChecks())

View File

@@ -17,9 +17,6 @@ namespace Umbraco.Web.Editors
[HttpGet]
public string GetPublishedStatusUrl()
{
if (_publishedSnapshotService is PublishedCache.XmlPublishedCache.PublishedSnapshotService)
return "views/dashboard/settings/xmldataintegrityreport.html";
//if (service is PublishedCache.PublishedNoCache.PublishedSnapshotService)
// return "views/dashboard/developer/nocache.html";

View File

@@ -1,61 +0,0 @@
using System;
using System.Web.Http;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.PublishedCache.XmlPublishedCache;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Editors
{
[ValidateAngularAntiForgeryToken]
public class XmlDataIntegrityController : UmbracoAuthorizedApiController
{
private readonly PublishedSnapshotService _publishedSnapshotService;
public XmlDataIntegrityController(IPublishedSnapshotService publishedSnapshotService)
{
if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService));
_publishedSnapshotService = publishedSnapshotService as PublishedSnapshotService;
if (_publishedSnapshotService == null) throw new NotSupportedException("Unsupported IPublishedSnapshotService, only the Xml one is supported.");
}
[HttpPost]
public bool FixContentXmlTable()
{
_publishedSnapshotService.RebuildContentAndPreviewXml();
return _publishedSnapshotService.VerifyContentAndPreviewXml();
}
[HttpPost]
public bool FixMediaXmlTable()
{
_publishedSnapshotService.RebuildMediaXml();
return _publishedSnapshotService.VerifyMediaXml();
}
[HttpPost]
public bool FixMembersXmlTable()
{
_publishedSnapshotService.RebuildMemberXml();
return _publishedSnapshotService.VerifyMemberXml();
}
[HttpGet]
public bool CheckContentXmlTable()
{
return _publishedSnapshotService.VerifyContentAndPreviewXml();
}
[HttpGet]
public bool CheckMediaXmlTable()
{
return _publishedSnapshotService.VerifyMediaXml();
}
[HttpGet]
public bool CheckMembersXmlTable()
{
return _publishedSnapshotService.VerifyMemberXml();
}
}
}

View File

@@ -1,230 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.XPath;
using Examine.LuceneEngine.Providers;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web.Composing;
using Umbraco.Web.Models;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <summary>
/// An IPublishedContent that is represented all by a dictionary.
/// </summary>
/// <remarks>
/// This is a helper class and definitely not intended for public use, it expects that all of the values required
/// to create an IPublishedContent exist in the dictionary by specific aliases.
/// </remarks>
internal class DictionaryPublishedContent : PublishedContentBase
{
// note: I'm not sure this class fully complies with IPublishedContent rules especially
// I'm not sure that _properties contains all properties including those without a value,
// neither that GetProperty will return a property without a value vs. null... @zpqrtbnk
// List of properties that will appear in the XML and do not match
// anything in the ContentType, so they must be ignored.
private static readonly string[] IgnoredKeys = { "version", "isDoc" };
public DictionaryPublishedContent(
IReadOnlyDictionary<string, string> valueDictionary,
Func<int, IPublishedContent> getParent,
Func<int, XPathNavigator, IEnumerable<IPublishedContent>> getChildren,
Func<DictionaryPublishedContent, string, IPublishedProperty> getProperty,
IAppCache appCache,
PublishedContentTypeCache contentTypeCache,
XPathNavigator nav,
bool fromExamine,
IUmbracoContextAccessor umbracoContextAccessor)
:base(umbracoContextAccessor)
{
if (valueDictionary == null) throw new ArgumentNullException(nameof(valueDictionary));
if (getParent == null) throw new ArgumentNullException(nameof(getParent));
if (getProperty == null) throw new ArgumentNullException(nameof(getProperty));
_getParent = new Lazy<IPublishedContent>(() => getParent(ParentId));
_getChildren = new Lazy<IEnumerable<IPublishedContent>>(() => getChildren(Id, nav));
_getProperty = getProperty;
_appCache = appCache;
LoadedFromExamine = fromExamine;
ValidateAndSetProperty(valueDictionary, val => _id = Int32.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int!
ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key", "__key", "__Key");
//ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId");
ValidateAndSetProperty(valueDictionary, val => _sortOrder = Int32.Parse(val), "sortOrder");
ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName");
ValidateAndSetProperty(valueDictionary, val => _urlName = val, "urlName");
ValidateAndSetProperty(valueDictionary, val => _documentTypeAlias = val, "nodeTypeAlias", LuceneIndex.ItemTypeFieldName);
ValidateAndSetProperty(valueDictionary, val => _documentTypeId = Int32.Parse(val), "nodeType");
//ValidateAndSetProperty(valueDictionary, val => _writerName = val, "writerName");
ValidateAndSetProperty(valueDictionary, val => _creatorName = val, "creatorName", "writerName"); //this is a bit of a hack fix for: U4-1132
//ValidateAndSetProperty(valueDictionary, val => _writerId = int.Parse(val), "writerID");
ValidateAndSetProperty(valueDictionary, val => _creatorId = Int32.Parse(val), "creatorID", "writerID"); //this is a bit of a hack fix for: U4-1132
ValidateAndSetProperty(valueDictionary, val => _path = val, "path", "__Path");
ValidateAndSetProperty(valueDictionary, val => _createDate = ParseDateTimeValue(val), "createDate");
ValidateAndSetProperty(valueDictionary, val => _updateDate = ParseDateTimeValue(val), "updateDate");
ValidateAndSetProperty(valueDictionary, val => _level = Int32.Parse(val), "level");
ValidateAndSetProperty(valueDictionary, val =>
{
int pId;
ParentId = -1;
if (Int32.TryParse(val, out pId))
{
ParentId = pId;
}
}, "parentID");
_contentType = contentTypeCache.Get(PublishedItemType.Media, _documentTypeAlias);
_properties = new Collection<IPublishedProperty>();
//handle content type properties
//make sure we create them even if there's no value
foreach (var propertyType in _contentType.PropertyTypes)
{
var alias = propertyType.Alias;
_keysAdded.Add(alias);
string value;
const bool isPreviewing = false; // false :: never preview a media
var property = valueDictionary.TryGetValue(alias, out value) == false || value == null
? new XmlPublishedProperty(propertyType, this, isPreviewing)
: new XmlPublishedProperty(propertyType, this, isPreviewing, value);
_properties.Add(property);
}
//loop through remaining values that haven't been applied
foreach (var i in valueDictionary.Where(x =>
_keysAdded.Contains(x.Key) == false // not already processed
&& IgnoredKeys.Contains(x.Key) == false)) // not ignorable
{
if (i.Key.InvariantStartsWith("__"))
{
// no type for that one, dunno how to convert, drop it
//IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty);
//_properties.Add(property);
}
else
{
// this is a property that does not correspond to anything, ignore and log
Current.Logger.Warn<PublishedMediaCache>("Dropping property '{PropertyKey}' because it does not belong to the content type.", i.Key);
}
}
}
private DateTime ParseDateTimeValue(string val)
{
if (LoadedFromExamine == false)
return DateTime.Parse(val);
//we need to parse the date time using Lucene converters
var ticks = Int64.Parse(val);
return new DateTime(ticks);
}
/// <summary>
/// Flag to get/set if this was loaded from examine cache
/// </summary>
internal bool LoadedFromExamine { get; }
//private readonly Func<DictionaryPublishedContent, IPublishedContent> _getParent;
private readonly Lazy<IPublishedContent> _getParent;
//private readonly Func<DictionaryPublishedContent, IEnumerable<IPublishedContent>> _getChildren;
private readonly Lazy<IEnumerable<IPublishedContent>> _getChildren;
private readonly Func<DictionaryPublishedContent, string, IPublishedProperty> _getProperty;
private readonly IAppCache _appCache;
/// <summary>
/// Returns 'Media' as the item type
/// </summary>
public override PublishedItemType ItemType => PublishedItemType.Media;
public override IPublishedContent Parent => _getParent.Value;
public int ParentId { get; private set; }
public override int Id => _id;
public override Guid Key => _key;
public override int? TemplateId => null;
public override int SortOrder => _sortOrder;
public override string Name => _name;
public override PublishedCultureInfo GetCulture(string culture = null) => null;
private static readonly Lazy<Dictionary<string, PublishedCultureInfo>> NoCultures = new Lazy<Dictionary<string, PublishedCultureInfo>>(() => new Dictionary<string, PublishedCultureInfo>());
public override IReadOnlyDictionary<string, PublishedCultureInfo> Cultures => NoCultures.Value;
public override string UrlSegment => _urlName;
public override string WriterName => _creatorName;
public override string CreatorName => _creatorName;
public override int WriterId => _creatorId;
public override int CreatorId => _creatorId;
public override string Path => _path;
public override DateTime CreateDate => _createDate;
public override DateTime UpdateDate => _updateDate;
public override int Level => _level;
public override bool IsDraft(string culture = null) => false;
public override bool IsPublished(string culture = null) => true;
public override IEnumerable<IPublishedProperty> Properties => _properties;
public override IEnumerable<IPublishedContent> Children => _getChildren.Value;
public override IPublishedProperty GetProperty(string alias)
{
return _getProperty(this, alias);
}
public override PublishedContentType ContentType => _contentType;
private readonly List<string> _keysAdded = new List<string>();
private int _id;
private Guid _key;
//private int _templateId;
private int _sortOrder;
private string _name;
private string _urlName;
private string _documentTypeAlias;
private int _documentTypeId;
//private string _writerName;
private string _creatorName;
//private int _writerId;
private int _creatorId;
private string _path;
private DateTime _createDate;
private DateTime _updateDate;
//private Guid _version;
private int _level;
private readonly ICollection<IPublishedProperty> _properties;
private readonly PublishedContentType _contentType;
private void ValidateAndSetProperty(IReadOnlyDictionary<string, string> valueDictionary, Action<string> setProperty, params string[] potentialKeys)
{
var key = potentialKeys.FirstOrDefault(x => valueDictionary.ContainsKey(x) && valueDictionary[x] != null);
if (key == null)
{
throw new FormatException("The valueDictionary is not formatted correctly and is missing any of the '" + String.Join(",", potentialKeys) + "' elements");
}
setProperty(valueDictionary[key]);
_keysAdded.Add(key);
}
}
}

View File

@@ -1,38 +0,0 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Umbraco.Web.Routing;
using Umbraco.Core;
using Umbraco.Core.Services;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
internal class DomainCache : IDomainCache
{
private readonly IDomainService _domainService;
public DomainCache(IDomainService domainService, IDefaultCultureAccessor defaultCultureAccessor)
{
_domainService = domainService;
DefaultCulture = defaultCultureAccessor.DefaultCulture;
}
/// <inheritdoc />
public IEnumerable<Domain> GetAll(bool includeWildcards)
{
return _domainService.GetAll(includeWildcards)
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard));
}
/// <inheritdoc />
public IEnumerable<Domain> GetAssigned(int contentId, bool includeWildcards)
{
return _domainService.GetAssignedDomains(contentId, includeWildcards)
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard));
}
public string DefaultCulture { get; }
}
}

View File

@@ -1,163 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Xml;
using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Web.Composing;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
class PreviewContent
{
private readonly int _userId;
private readonly Guid _previewSet;
private string _previewSetPath;
private XmlDocument _previewXml;
private readonly XmlStore _xmlStore;
/// <summary>
/// Gets the XML document.
/// </summary>
/// <remarks>May return <c>null</c> if the preview content set is invalid.</remarks>
public XmlDocument XmlContent
{
get
{
// null if invalid preview content
if (_previewSetPath == null) return null;
// load if not loaded yet
if (_previewXml != null)
return _previewXml;
_previewXml = new XmlDocument();
try
{
_previewXml.Load(_previewSetPath);
}
catch (Exception ex)
{
Current.Logger.Error<PreviewContent>(ex, "Could not load preview set {PreviewSet} for user {UserId}.", _previewSet, _userId);
ClearPreviewSet();
_previewXml = null;
_previewSetPath = null; // do not try again
}
return _previewXml;
}
}
/// <summary>
/// Gets the preview token.
/// </summary>
/// <remarks>To be stored in a cookie or wherever appropriate.</remarks>
public string Token => _userId + ":" + _previewSet;
/// <summary>
/// Initializes a new instance of the <see cref="PreviewContent"/> class for a user.
/// </summary>
/// <param name="xmlStore">The underlying Xml store.</param>
/// <param name="userId">The user identifier.</param>
public PreviewContent(XmlStore xmlStore, int userId)
{
if (xmlStore == null)
throw new ArgumentNullException(nameof(xmlStore));
_xmlStore = xmlStore;
_userId = userId;
_previewSet = Guid.NewGuid();
_previewSetPath = GetPreviewSetPath(_userId, _previewSet);
}
/// <summary>
/// Initializes a new instance of the <see cref="PreviewContent"/> with a preview token.
/// </summary>
/// <param name="xmlStore">The underlying Xml store.</param>
/// <param name="token">The preview token.</param>
public PreviewContent(XmlStore xmlStore, string token)
{
if (xmlStore == null)
throw new ArgumentNullException(nameof(xmlStore));
_xmlStore = xmlStore;
if (token.IsNullOrWhiteSpace())
throw new ArgumentException("Null or empty token.", nameof(token));
var parts = token.Split(':');
if (parts.Length != 2)
throw new ArgumentException("Invalid token.", nameof(token));
if (int.TryParse(parts[0], out _userId) == false)
throw new ArgumentException("Invalid token.", nameof(token));
if (Guid.TryParse(parts[1], out _previewSet) == false)
throw new ArgumentException("Invalid token.", nameof(token));
_previewSetPath = GetPreviewSetPath(_userId, _previewSet);
}
// creates and saves a new preview set
// used in 2 places and each time includeSubs is true
// have to use the Document class at the moment because IContent does not do ToXml...
public void CreatePreviewSet(int contentId, bool includeSubs)
{
// note: always include subs
_previewXml = _xmlStore.GetPreviewXml(contentId, includeSubs);
// make sure the preview folder exists
var dir = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Preview));
if (dir.Exists == false)
dir.Create();
// clean old preview sets
ClearPreviewDirectory(_userId, dir);
// save
_previewXml.Save(_previewSetPath);
}
// get the full path to the preview set
private static string GetPreviewSetPath(int userId, Guid previewSet)
{
return IOHelper.MapPath(Path.Combine(SystemDirectories.Preview, userId + "_" + previewSet + ".config"));
}
// deletes files for the user, and files accessed more than one hour ago
private static void ClearPreviewDirectory(int userId, DirectoryInfo dir)
{
var now = DateTime.Now;
var prefix = userId + "_";
foreach (var file in dir.GetFiles("*.config")
.Where(x => x.Name.StartsWith(prefix) || (now - x.LastAccessTime).TotalMinutes > 1))
{
DeletePreviewSetFile(userId, file);
}
}
// delete one preview set file in a safe way
private static void DeletePreviewSetFile(int userId, FileSystemInfo file)
{
try
{
file.Delete();
}
catch (Exception ex)
{
Current.Logger.Error<PreviewContent>(ex, "Couldn't delete preview set {FileName} for user {UserId}", file.Name, userId);
}
}
/// <summary>
/// Deletes the preview set in a safe way.
/// </summary>
public void ClearPreviewSet()
{
if (_previewSetPath == null) return;
var previewSetFile = new FileInfo(_previewSetPath);
DeletePreviewSetFile(_userId, previewSetFile);
}
}
}

View File

@@ -1,554 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Xml;
using System.Xml.XPath;
using Umbraco.Core.Configuration;
using Umbraco.Core;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Xml;
using Umbraco.Web.Routing;
using System.Linq;
using Umbraco.Core.Cache;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache
{
private readonly IAppCache _appCache;
private readonly IGlobalSettings _globalSettings;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly RoutesCache _routesCache;
private readonly IDomainCache _domainCache;
private readonly DomainHelper _domainHelper;
private readonly PublishedContentTypeCache _contentTypeCache;
// initialize a PublishedContentCache instance with
// an XmlStore containing the master xml
// an IAppCache that should be at request-level
// a RoutesCache - need to cleanup that one
// a preview token string (or null if not previewing)
public PublishedContentCache(
XmlStore xmlStore, // an XmlStore containing the master xml
IDomainCache domainCache, // an IDomainCache implementation
IAppCache appCache, // an IAppCache that should be at request-level
IGlobalSettings globalSettings,
ISiteDomainHelper siteDomainHelper,
IUmbracoContextAccessor umbracoContextAccessor,
PublishedContentTypeCache contentTypeCache, // a PublishedContentType cache
RoutesCache routesCache, // a RoutesCache
string previewToken) // a preview token string (or null if not previewing)
: base(previewToken.IsNullOrWhiteSpace() == false)
{
_appCache = appCache;
_globalSettings = globalSettings;
_umbracoContextAccessor = umbracoContextAccessor;
_routesCache = routesCache; // may be null for unit-testing
_contentTypeCache = contentTypeCache;
_domainCache = domainCache;
_domainHelper = new DomainHelper(_domainCache, siteDomainHelper);
_xmlStore = xmlStore;
_xml = _xmlStore.Xml; // capture - because the cache has to remain consistent
if (previewToken.IsNullOrWhiteSpace() == false)
_previewContent = new PreviewContent(_xmlStore, previewToken);
}
#region Unit Tests
// for INTERNAL, UNIT TESTS use ONLY
internal RoutesCache RoutesCache => _routesCache;
// for INTERNAL, UNIT TESTS use ONLY
internal XmlStore XmlStore => _xmlStore;
#endregion
#region Routes
public virtual IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string culture = null)
{
if (route == null) throw new ArgumentNullException(nameof(route));
// try to get from cache if not previewing
var contentId = preview || _routesCache == null ? 0 : _routesCache.GetNodeId(route);
// if found id in cache then get corresponding content
// and clear cache if not found - for whatever reason
IPublishedContent content = null;
if (contentId > 0)
{
content = GetById(preview, contentId);
if (content == null)
_routesCache?.ClearNode(contentId);
}
// still have nothing? actually determine the id
hideTopLevelNode = hideTopLevelNode ?? _globalSettings.HideTopLevelNodeFromPath; // default = settings
content = content ?? DetermineIdByRoute(preview, route, hideTopLevelNode.Value);
// cache if we have a content and not previewing
if (content != null && preview == false && _routesCache != null)
AddToCacheIfDeepestRoute(content, route);
return content;
}
private void AddToCacheIfDeepestRoute(IPublishedContent content, string route)
{
var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/')));
// so we have a route that maps to a content... say "1234/path/to/content" - however, there could be a
// domain set on "to" and route "4567/content" would also map to the same content - and due to how
// urls computing work (by walking the tree up to the first domain we find) it is that second route
// that would be returned - the "deepest" route - and that is the route we want to cache, *not* the
// longer one - so make sure we don't cache the wrong route
var deepest = DomainHelper.ExistsDomainInPath(_domainCache.GetAll(false), content.Path, domainRootNodeId) == false;
if (deepest)
_routesCache.Store(content.Id, route, true); // trusted route
}
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string culture = null)
{
return GetByRoute(PreviewDefault, route, hideTopLevelNode);
}
public virtual string GetRouteById(bool preview, int contentId, string culture = null)
{
// try to get from cache if not previewing
var route = preview || _routesCache == null ? null : _routesCache.GetRoute(contentId);
// if found in cache then return
if (route != null)
return route;
// else actually determine the route
route = DetermineRouteById(preview, contentId);
// node not found
if (route == null)
return null;
// cache the route BUT do NOT trust it as it can be a colliding route
// meaning if we GetRouteById again, we'll get it from cache, but it
// won't be used for inbound routing
if (preview == false)
_routesCache.Store(contentId, route, false);
return route;
}
public string GetRouteById(int contentId, string culture = null)
{
return GetRouteById(PreviewDefault, contentId, culture);
}
IPublishedContent DetermineIdByRoute(bool preview, string route, bool hideTopLevelNode)
{
//the route always needs to be lower case because we only store the urlName attribute in lower case
route = route?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(route));
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
//check if we can find the node in our xml cache
var id = NavigateRoute(preview, startNodeId, path, hideTopLevelNode);
return id > 0 ? GetById(preview, id) : null;
}
private static XmlElement GetXmlElementChildWithLowestSortOrder(XmlNode element)
{
XmlElement elt = null;
var min = int.MaxValue;
foreach (var n in element.ChildNodes)
{
var e = n as XmlElement;
if (e == null) continue;
var sortOrder = int.Parse(e.GetAttribute("sortOrder"));
if (sortOrder >= min) continue;
min = sortOrder;
elt = e;
}
return elt;
}
private int NavigateRoute(bool preview, int startNodeId, string path, bool hideTopLevelNode)
{
var xml = GetXml(preview);
XmlElement elt;
// empty path
if (path == string.Empty || path == "/")
{
if (startNodeId > 0)
{
elt = xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture));
return elt == null ? -1 : startNodeId;
}
elt = GetXmlElementChildWithLowestSortOrder(xml.DocumentElement);
return elt == null ? -1 : int.Parse(elt.GetAttribute("id"));
}
// non-empty path
elt = startNodeId <= 0
? xml.DocumentElement
: xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture));
if (elt == null) return -1;
var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries);
if (hideTopLevelNode && startNodeId <= 0)
{
//Don't use OfType<T> or Cast<T>, this is critical code, all ChildNodes are XmlElement so explicitly cast
// https://gist.github.com/Shazwazza/04e2e5642a316f4a87e52dada2901198
foreach (var n in elt.ChildNodes)
{
var e = n as XmlElement;
if (e == null) continue;
var id = NavigateElementRoute(e, urlParts);
if (id > 0) return id;
}
if (urlParts.Length > 1)
return -1;
}
return NavigateElementRoute(elt, urlParts);
}
private static int NavigateElementRoute(XmlElement elt, string[] urlParts)
{
var found = true;
var i = 0;
while (found && i < urlParts.Length)
{
found = false;
//Don't use OfType<T> or Cast<T>, this is critical code, all ChildNodes are XmlElement so explicitly cast
// https://gist.github.com/Shazwazza/04e2e5642a316f4a87e52dada2901198
var sortOrder = -1;
foreach (var o in elt.ChildNodes)
{
var child = o as XmlElement;
if (child == null) continue;
var noNode = child.GetAttributeNode("isDoc") == null;
if (noNode) continue;
if (child.GetAttribute("urlName") != urlParts[i]) continue;
found = true;
var so = int.Parse(child.GetAttribute("sortOrder"));
if (sortOrder >= 0 && so >= sortOrder) continue;
sortOrder = so;
elt = child;
}
i++;
}
return found ? int.Parse(elt.GetAttribute("id")) : -1;
}
string DetermineRouteById(bool preview, int contentId)
{
var node = GetById(preview, contentId);
if (node == null) return null;
// walk up from that node until we hit a node with a domain,
// or we reach the content root, collecting urls in the way
var pathParts = new List<string>();
var n = node;
var hasDomains = _domainHelper.NodeHasDomains(n.Id);
while (hasDomains == false && n != null) // n is null at root
{
// get the url
var urlName = n.UrlSegment;
pathParts.Add(urlName);
// move to parent node
n = n.Parent;
hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id);
}
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (hasDomains == false && _globalSettings.HideTopLevelNodeFromPath)
{
if (node.Parent == null)
{
var rootNode = GetByRoute(preview, "/", true);
if (rootNode == null)
throw new Exception("Failed to get node at /.");
if (rootNode.Id == node.Id) // remove only if we're the default node
pathParts.RemoveAt(pathParts.Count - 1);
}
else
{
pathParts.RemoveAt(pathParts.Count - 1);
}
}
// assemble the route
pathParts.Reverse();
var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
var route = (n?.Id.ToString(CultureInfo.InvariantCulture) ?? "") + path;
return route;
}
#endregion
#region XPath Strings
static class XPathStrings
{
public const string Root = "/root";
public const string RootDocuments = "/root/* [@isDoc]";
}
#endregion
#region Converters
private IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing)
{
return xmlNode == null ? null : XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache,_umbracoContextAccessor);
}
private IEnumerable<IPublishedContent> ConvertToDocuments(XmlNodeList xmlNodes, bool isPreviewing)
{
return xmlNodes.Cast<XmlNode>()
.Select(xmlNode => XmlPublishedContent.Get(xmlNode, isPreviewing, _appCache, _contentTypeCache, _umbracoContextAccessor));
}
#endregion
#region Getters
public override IPublishedContent GetById(bool preview, int nodeId)
{
return ConvertToDocument(GetXml(preview).GetElementById(nodeId.ToString(CultureInfo.InvariantCulture)), preview);
}
public override IPublishedContent GetById(bool preview, Guid nodeId)
{
// implement this, but in a more efficient way
//const string xpath = "//* [@isDoc and @key=$guid]";
//return GetSingleByXPath(preview, xpath, new[] { new XPathVariable("guid", nodeId.ToString()) });
var keyMatch = nodeId.ToString();
var nav = GetXml(preview).CreateNavigator();
if (nav.MoveToFirstChild() == false) return null; // from / to /root
if (nav.MoveToFirstChild() == false) return null; // from /root to /root/*
while (true)
{
var isDoc = false;
string key = null;
if (nav.HasAttributes)
{
nav.MoveToFirstAttribute();
do
{
if (nav.Name == "isDoc") isDoc = true;
if (nav.Name == "key") key = nav.Value;
if (isDoc && key != null) break;
} while (nav.MoveToNextAttribute());
nav.MoveToParent();
}
if (isDoc == false || key != keyMatch)
{
if (isDoc && nav.MoveToFirstChild())
continue;
while (nav.MoveToNext(XPathNodeType.Element) == false)
if (nav.MoveToParent() == false || nav.NodeType == XPathNodeType.Root) return null;
continue;
}
var elt = nav.UnderlyingObject as XmlNode;
return ConvertToDocument(elt, preview);
}
}
public override bool HasById(bool preview, int contentId)
{
return GetXml(preview).CreateNavigator().MoveToId(contentId.ToString(CultureInfo.InvariantCulture));
}
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview)
{
return ConvertToDocuments(GetXml(preview).SelectNodes(XPathStrings.RootDocuments), preview);
}
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
if (string.IsNullOrWhiteSpace(xpath)) return null;
var xml = GetXml(preview);
var node = vars == null
? xml.SelectSingleNode(xpath)
: xml.SelectSingleNode(xpath, vars);
return ConvertToDocument(node, preview);
}
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
var xml = GetXml(preview);
var node = vars == null
? xml.SelectSingleNode(xpath)
: xml.SelectSingleNode(xpath, vars);
return ConvertToDocument(node, preview);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
if (string.IsNullOrWhiteSpace(xpath)) return Enumerable.Empty<IPublishedContent>();
var xml = GetXml(preview);
var nodes = vars == null
? xml.SelectNodes(xpath)
: xml.SelectNodes(xpath, vars);
return ConvertToDocuments(nodes, preview);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
var xml = GetXml(preview);
var nodes = vars == null
? xml.SelectNodes(xpath)
: xml.SelectNodes(xpath, vars);
return ConvertToDocuments(nodes, preview);
}
public override bool HasContent(bool preview)
{
var xml = GetXml(preview);
var node = xml?.SelectSingleNode(XPathStrings.RootDocuments);
return node != null;
}
public override XPathNavigator CreateNavigator(bool preview)
{
var xml = GetXml(preview);
return xml.CreateNavigator();
}
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
{
// hackish - backward compatibility ;-(
XPathNavigator navigator = null;
if (preview)
{
var node = _xmlStore.GetPreviewXmlNode(id);
if (node != null)
{
navigator = node.CreateNavigator();
}
}
else
{
var node = GetXml(false).GetElementById(id.ToInvariantString());
if (node != null)
{
var doc = new XmlDocument();
var clone = doc.ImportNode(node, false);
var props = node.SelectNodes("./* [not(@id)]");
if (props == null) throw new Exception("oops");
foreach (var n in props.Cast<XmlNode>())
clone.AppendChild(doc.ImportNode(n, true));
navigator = node.CreateNavigator();
}
}
return navigator;
}
#endregion
#region Legacy Xml
private readonly XmlStore _xmlStore;
private XmlDocument _xml;
private readonly PreviewContent _previewContent;
internal XmlDocument GetXml(bool preview)
{
// not trying to be thread-safe here, that's not the point
if (preview == false)
{
// if there's a current enlisted reader/writer, use its xml
var tempXml = _xmlStore.TempXml;
if (tempXml != null) return tempXml;
return _xml;
}
// Xml cache does not support retrieving preview content when not previewing
if (_previewContent == null)
throw new InvalidOperationException("Cannot retrieve preview content when not previewing.");
// PreviewContent tries to load the Xml once and if it fails,
// it invalidates itself and always return null for XmlContent.
var previewXml = _previewContent.XmlContent;
return previewXml ?? _xml;
}
internal void Resync(XmlDocument xml)
{
_xml = xml; // re-capture
// note: we're not resyncing "preview" because that would mean re-building the whole
// preview set which is costly, so basically when previewing, there will be no resync.
// clear recursive properties cached by XmlPublishedContent.GetProperty
// assume that nothing else is going to cache IPublishedProperty items (else would need to do ByKeySearch)
// NOTE also clears all the media cache properties, which is OK (see media cache)
_appCache.ClearOfType<IPublishedProperty>();
//_appCache.ClearCacheByKeySearch("XmlPublishedCache.PublishedContentCache:RecursiveProperty-");
}
#endregion
#region XPathQuery
static readonly char[] SlashChar = { '/' };
#endregion
#region Content types
public override PublishedContentType GetContentType(int id)
{
return _contentTypeCache.Get(PublishedItemType.Content, id);
}
public override PublishedContentType GetContentType(string alias)
{
return _contentTypeCache.Get(PublishedItemType.Content, alias);
}
public override IEnumerable<IPublishedContent> GetByContentType(PublishedContentType contentType)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -1,719 +0,0 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.XPath;
using Examine;
using Examine.Providers;
using Examine.Search;
using Lucene.Net.Store;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Xml;
using Umbraco.Examine;
using Umbraco.Core.Cache;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Web.Composing;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <summary>
/// An IPublishedMediaStore that first checks for the media in Examine, and then reverts to the database
/// </summary>
/// <remarks>
/// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly.
/// </remarks>
internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache
{
private readonly IMediaService _mediaService;
private readonly IUserService _userService;
// by default these are null unless specified by the ctor dedicated to tests
// when they are null the cache derives them from the ExamineManager, see
// method GetExamineManagerSafe().
//
private readonly ISearcher _searchProvider;
private readonly XmlStore _xmlStore;
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IEntityXmlSerializer _entitySerializer;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
// must be specified by the ctor
private readonly IAppCache _appCache;
public PublishedMediaCache(XmlStore xmlStore, IMediaService mediaService, IUserService userService,
IAppCache appCache, PublishedContentTypeCache contentTypeCache, IEntityXmlSerializer entitySerializer,
IUmbracoContextAccessor umbracoContextAccessor)
: base(false)
{
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_appCache = appCache;
_xmlStore = xmlStore;
_contentTypeCache = contentTypeCache;
_entitySerializer = entitySerializer;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Generally used for unit testing to use an explicit examine searcher
/// </summary>
/// <param name="mediaService"></param>
/// <param name="userService"></param>
/// <param name="searchProvider"></param>
/// <param name="appCache"></param>
/// <param name="contentTypeCache"></param>
/// <param name="entitySerializer"></param>
internal PublishedMediaCache(IMediaService mediaService, IUserService userService, ISearcher searchProvider, IAppCache appCache, PublishedContentTypeCache contentTypeCache, IEntityXmlSerializer entitySerializer)
: base(false)
{
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_searchProvider = searchProvider ?? throw new ArgumentNullException(nameof(searchProvider));
_appCache = appCache;
_contentTypeCache = contentTypeCache;
_entitySerializer = entitySerializer;
}
static PublishedMediaCache()
{
InitializeCacheConfig();
}
public override IPublishedContent GetById(bool preview, int nodeId)
{
return GetUmbracoMedia(nodeId);
}
public override IPublishedContent GetById(bool preview, Guid nodeId)
{
throw new NotImplementedException();
}
public override bool HasById(bool preview, int contentId)
{
return GetUmbracoMedia(contentId) != null;
}
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview)
{
var searchProvider = GetSearchProviderSafe();
if (searchProvider != null)
{
try
{
// first check in Examine for the cache values
// +(+parentID:-1) +__IndexType:media
var criteria = searchProvider.CreateQuery("media");
var filter = criteria.ParentId(-1).Not().Field(UmbracoExamineIndex.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard());
var result = filter.Execute();
if (result != null)
return result.Select(x => CreateFromCacheValues(ConvertFromSearchResult(x)));
}
catch (Exception ex)
{
if (ex is FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
// TODO: Need to fix examine in LB scenarios!
Current.Logger.Error<PublishedMediaCache>(ex, "Could not load data from Examine index for media");
}
else if (ex is AlreadyClosedException)
{
//If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot
//be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db.
Current.Logger.Error<PublishedMediaCache>(ex, "Could not load data from Examine index for media, the app domain is most likely in a shutdown state");
}
else throw;
}
}
//something went wrong, fetch from the db
var rootMedia = _mediaService.GetRootMedia();
return rootMedia.Select(m => GetUmbracoMedia(m.Id));
}
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetSingleByXPath(iterator);
}
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetSingleByXPath(iterator);
}
private IPublishedContent GetSingleByXPath(XPathNodeIterator iterator)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//if (iterator.MoveNext() == false) return null;
//var idAttr = iterator.Current.GetAttribute("id", "");
//int id;
//return int.TryParse(idAttr, out id) ? GetUmbracoMedia(id) : null;
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetByXPath(iterator);
}
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var navigator = CreateNavigator(preview);
//var iterator = navigator.Select(xpath, vars);
//return GetByXPath(iterator);
}
private IEnumerable<IPublishedContent> GetByXPath(XPathNodeIterator iterator)
{
while (iterator.MoveNext())
{
var idAttr = iterator.Current.GetAttribute("id", "");
int id;
if (int.TryParse(idAttr, out id))
yield return GetUmbracoMedia(id);
}
}
public override XPathNavigator CreateNavigator(bool preview)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
//var doc = _xmlStore.GetMediaXml();
//return doc.CreateNavigator();
}
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
{
// preview is ignored for media cache
// this code is mostly used when replacing old media.ToXml() code, and that code
// stored the XML attached to the media itself - so for some time in memory - so
// unless we implement some sort of cache here, we're probably degrading perfs.
XPathNavigator navigator = null;
var node = _xmlStore.GetMediaXmlNode(id);
if (node != null)
{
navigator = node.CreateNavigator();
}
return navigator;
}
public override bool HasContent(bool preview) { throw new NotImplementedException(); }
private static IExamineManager GetExamineManagerSafe()
{
try
{
return ExamineManager.Instance;
}
catch (TypeInitializationException)
{
return null;
}
}
private ISearcher GetSearchProviderSafe()
{
if (_searchProvider != null)
return _searchProvider;
var eMgr = GetExamineManagerSafe();
if (eMgr == null) return null;
try
{
return eMgr.TryGetIndex(Constants.UmbracoIndexes.InternalIndexName, out var index) ? index.GetSearcher() : null;
}
catch (FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
// TODO: Need to fix examine in LB scenarios!
}
catch (NullReferenceException)
{
//This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore
// the manager will return the singleton without throwing initialization errors, however if examine isn't configured correctly a null
// reference error will occur because the examine settings are null.
}
catch (AlreadyClosedException)
{
//If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot
//be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db.
}
return null;
}
private IPublishedContent GetUmbracoMedia(int id)
{
// this recreates an IPublishedContent and model each time
// it is called, but at least it should NOT hit the database
// nor Lucene each time, relying on the memory cache instead
if (id <= 0) return null; // fail fast
var cacheValues = GetCacheValues(id, GetUmbracoMediaCacheValues);
return cacheValues == null ? null : CreateFromCacheValues(cacheValues);
}
private CacheValues GetUmbracoMediaCacheValues(int id)
{
var searchProvider = GetSearchProviderSafe();
if (searchProvider != null)
{
try
{
// first check in Examine as this is WAY faster
//
// the filter will create a query like this:
// +(+__NodeId:3113 -__Path:-1,-21,*) +__IndexType:media
//
// note that since the use of the wildcard, it automatically escapes it in Lucene.
var criteria = searchProvider.CreateQuery("media");
var filter = criteria.Id(id.ToInvariantString()).Not().Field(UmbracoExamineIndex.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard());
var result = filter.Execute().FirstOrDefault();
if (result != null) return ConvertFromSearchResult(result);
}
catch (Exception ex)
{
if (ex is FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
// TODO: Need to fix examine in LB scenarios!
Current.Logger.Error<PublishedMediaCache>(ex, "Could not load data from Examine index for media");
}
else if (ex is AlreadyClosedException)
{
//If the app domain is shutting down and the site is under heavy load the index reader will be closed and it really cannot
//be re-opened since the app domain is shutting down. In this case we have no option but to try to load the data from the db.
Current.Logger.Error<PublishedMediaCache>(ex, "Could not load data from Examine index for media, the app domain is most likely in a shutdown state");
}
else throw;
}
}
// don't log a warning here, as it can flood the log in case of eg a media picker referencing a media
// that has been deleted, hence is not in the Examine index anymore (for a good reason). try to get
// the media from the service, first
var media = _mediaService.GetById(id);
if (media == null || media.Trashed) return null; // not found, ok
// so, the media was not found in Examine's index *yet* it exists, which probably indicates that
// the index is corrupted. Or not up-to-date. Log a warning, but only once, and only if seeing the
// error more that a number of times.
var miss = Interlocked.CompareExchange(ref _examineIndexMiss, 0, 0); // volatile read
if (miss < ExamineIndexMissMax && Interlocked.Increment(ref _examineIndexMiss) == ExamineIndexMissMax)
Current.Logger.Warn<PublishedMediaCache>("Failed ({ExamineIndexMissMax} times) to retrieve medias from Examine index and had to load"
+ " them from DB. This may indicate that the Examine index is corrupted.", ExamineIndexMissMax);
return ConvertFromIMedia(media);
}
private const int ExamineIndexMissMax = 10;
private int _examineIndexMiss;
internal CacheValues ConvertFromXPathNodeIterator(XPathNodeIterator media, int id)
{
if (media?.Current != null)
{
return media.Current.Name.InvariantEquals("error")
? null
: ConvertFromXPathNavigator(media.Current);
}
Current.Logger.Warn<PublishedMediaCache>("Could not retrieve media {MediaId} from Examine index or from legacy library.GetMedia method", id);
return null;
}
internal CacheValues ConvertFromSearchResult(ISearchResult searchResult)
{
// note: fixing fields in 7.x, removed by Shan for 8.0
return new CacheValues
{
Values = searchResult.Values,
FromExamine = true
};
}
internal CacheValues ConvertFromXPathNavigator(XPathNavigator xpath, bool forceNav = false)
{
if (xpath == null) throw new ArgumentNullException(nameof(xpath));
var values = new Dictionary<string, string> { { "nodeName", xpath.GetAttribute("nodeName", "") } };
values["nodeTypeAlias"] = xpath.Name;
var result = xpath.SelectChildren(XPathNodeType.Element);
//add the attributes e.g. id, parentId etc
if (result.Current != null && result.Current.HasAttributes)
{
if (result.Current.MoveToFirstAttribute())
{
//checking for duplicate keys because of the 'nodeTypeAlias' might already be added above.
if (values.ContainsKey(result.Current.Name) == false)
{
values[result.Current.Name] = result.Current.Value;
}
while (result.Current.MoveToNextAttribute())
{
if (values.ContainsKey(result.Current.Name) == false)
{
values[result.Current.Name] = result.Current.Value;
}
}
result.Current.MoveToParent();
}
}
// because, migration
if (values.ContainsKey("key") == false)
values["key"] = Guid.Empty.ToString();
//add the user props
while (result.MoveNext())
{
if (result.Current != null && result.Current.HasAttributes == false)
{
var value = result.Current.Value;
if (string.IsNullOrEmpty(value))
{
if (result.Current.HasAttributes || result.Current.SelectChildren(XPathNodeType.Element).Count > 0)
{
value = result.Current.OuterXml;
}
}
values[result.Current.Name] = value;
}
}
return new CacheValues
{
Values = values,
XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator!
};
}
internal CacheValues ConvertFromIMedia(IMedia media)
{
var values = new Dictionary<string, string>();
var creator = _userService.GetProfileById(media.CreatorId);
var creatorName = creator == null ? "" : creator.Name;
values["id"] = media.Id.ToString();
values["key"] = media.Key.ToString();
values["parentID"] = media.ParentId.ToString();
values["level"] = media.Level.ToString();
values["creatorID"] = media.CreatorId.ToString();
values["creatorName"] = creatorName;
values["writerID"] = media.CreatorId.ToString();
values["writerName"] = creatorName;
values["template"] = "0";
values["urlName"] = "";
values["sortOrder"] = media.SortOrder.ToString();
values["createDate"] = media.CreateDate.ToString("yyyy-MM-dd HH:mm:ss");
values["updateDate"] = media.UpdateDate.ToString("yyyy-MM-dd HH:mm:ss");
values["nodeName"] = media.Name;
values["path"] = media.Path;
values["nodeType"] = media.ContentType.Id.ToString();
values["nodeTypeAlias"] = media.ContentType.Alias;
// add the user props
foreach (var prop in media.Properties)
values[prop.Alias] = prop.GetValue()?.ToString();
return new CacheValues
{
Values = values
};
}
/// <summary>
/// We will need to first check if the document was loaded by Examine, if so we'll need to check if this property exists
/// in the results, if it does not, then we'll have to revert to looking up in the db.
/// </summary>
/// <param name="dd"> </param>
/// <param name="alias"></param>
/// <returns></returns>
private IPublishedProperty GetProperty(DictionaryPublishedContent dd, string alias)
{
//lets check if the alias does not exist on the document.
//NOTE: Examine will not index empty values and we do not output empty XML Elements to the cache - either of these situations
// would mean that the property is missing from the collection whether we are getting the value from Examine or from the library media cache.
if (dd.Properties.All(x => x.Alias.InvariantEquals(alias) == false))
{
return null;
}
if (dd.LoadedFromExamine)
{
//We are going to check for a special field however, that is because in some cases we store a 'Raw'
//value in the index such as for xml/html.
var rawValue = dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(UmbracoExamineIndex.RawFieldPrefix + alias));
return rawValue
?? dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}
//if its not loaded from examine, then just return the property
return dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
}
/// <summary>
/// A Helper methods to return the children for media whether it is based on examine or xml
/// </summary>
/// <param name="parentId"></param>
/// <param name="xpath"></param>
/// <returns></returns>
private IEnumerable<IPublishedContent> GetChildrenMedia(int parentId, XPathNavigator xpath = null)
{
// if there *is* a navigator, directly look it up
if (xpath != null)
{
return ToIPublishedContent(parentId, xpath);
}
// otherwise, try examine first, then re-look it up
var searchProvider = GetSearchProviderSafe();
if (searchProvider != null)
{
try
{
//first check in Examine as this is WAY faster
var criteria = searchProvider.CreateQuery("media");
var filter = criteria.ParentId(parentId).Not().Field(UmbracoExamineIndex.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard())
.OrderBy(new SortableField("sortOrder", SortType.Int));
//the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene.
//+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media
// sort with the Sort field (updated for 8.0)
var results = filter.Execute();
if (results.Any())
{
// var medias = results.Select(ConvertFromSearchResult);
var medias = results.Select(x =>
{
int nid;
if (int.TryParse(x["__NodeId"], out nid) == false && int.TryParse(x["NodeId"], out nid) == false)
throw new Exception("Failed to extract NodeId from search result.");
var cacheValues = GetCacheValues(nid, id => ConvertFromSearchResult(x));
return CreateFromCacheValues(cacheValues);
});
return medias;
}
//if there's no result then return null. Previously we defaulted back to library.GetMedia below
//but this will always get called for when we are getting descendants since many items won't have
//children and then we are hitting the database again!
//So instead we're going to rely on Examine to have the correct results like it should.
return Enumerable.Empty<IPublishedContent>();
}
catch (FileNotFoundException)
{
//Currently examine is throwing FileNotFound exceptions when we have a load balanced filestore and a node is published in umbraco
//See this thread: http://examine.cdodeplex.com/discussions/264341
//Catch the exception here for the time being, and just fallback to GetMedia
}
}
// falling back to get media
// was library.GetMedia which had its own cache, but MediaService *also* caches
// so, library.GetMedia is gone and now we directly work with MediaService
// (code below copied from what library was doing)
var media = Current.Services.MediaService.GetById(parentId);
if (media == null)
{
return Enumerable.Empty<IPublishedContent>();
}
var serialized = _entitySerializer.Serialize(media, true);
var mediaIterator = serialized.CreateNavigator().Select("/");
return mediaIterator.Current == null
? Enumerable.Empty<IPublishedContent>()
: ToIPublishedContent(parentId, mediaIterator.Current);
}
internal IEnumerable<IPublishedContent> ToIPublishedContent(int parentId, XPathNavigator xpath)
{
var mediaList = new List<IPublishedContent>();
// this is so bad, really
var item = xpath.Select("//*[@id='" + parentId + "']");
if (item.Current == null)
return Enumerable.Empty<IPublishedContent>();
var items = item.Current.SelectChildren(XPathNodeType.Element);
// and this does not work, because... meh
//var q = "//* [@id='" + parentId + "']/* [@id]";
//var items = xpath.Select(q);
foreach (XPathNavigator itemm in items)
{
int id;
if (int.TryParse(itemm.GetAttribute("id", ""), out id) == false)
continue; // wtf?
var captured = itemm;
var cacheValues = GetCacheValues(id, idd => ConvertFromXPathNavigator(captured));
mediaList.Add(CreateFromCacheValues(cacheValues));
}
return mediaList;
}
internal void Resync()
{
// clear recursive properties cached by XmlPublishedContent.GetProperty
// assume that nothing else is going to cache IPublishedProperty items (else would need to do ByKeySearch)
// NOTE all properties cleared when clearing the content cache (see content cache)
//_appCache.ClearCacheObjectTypes<IPublishedProperty>();
//_appCache.ClearCacheByKeySearch("XmlPublishedCache.PublishedMediaCache:RecursiveProperty-");
}
#region Content types
public override PublishedContentType GetContentType(int id)
{
return _contentTypeCache.Get(PublishedItemType.Media, id);
}
public override PublishedContentType GetContentType(string alias)
{
return _contentTypeCache.Get(PublishedItemType.Media, alias);
}
public override IEnumerable<IPublishedContent> GetByContentType(PublishedContentType contentType)
{
throw new NotImplementedException();
}
#endregion
// REFACTORING
// caching the basic atomic values - and the parent id
// but NOT caching actual parent nor children and NOT even
// the list of children ids - BUT caching the path
internal class CacheValues
{
public IReadOnlyDictionary<string, string> Values { get; set; }
public XPathNavigator XPath { get; set; }
public bool FromExamine { get; set; }
}
public const string PublishedMediaCacheKey = "MediaCacheMeh.";
private const int PublishedMediaCacheTimespanSeconds = 4 * 60; // 4 mins
private static TimeSpan _publishedMediaCacheTimespan;
private static bool _publishedMediaCacheEnabled;
private static void InitializeCacheConfig()
{
var value = ConfigurationManager.AppSettings["Umbraco.PublishedMediaCache.Seconds"];
int seconds;
if (int.TryParse(value, out seconds) == false)
seconds = PublishedMediaCacheTimespanSeconds;
if (seconds > 0)
{
_publishedMediaCacheEnabled = true;
_publishedMediaCacheTimespan = TimeSpan.FromSeconds(seconds);
}
else
{
_publishedMediaCacheEnabled = false;
}
}
internal IPublishedContent CreateFromCacheValues(CacheValues cacheValues)
{
var content = new DictionaryPublishedContent(
cacheValues.Values,
parentId => parentId < 0 ? null : GetUmbracoMedia(parentId),
GetChildrenMedia,
GetProperty,
_appCache,
_contentTypeCache,
cacheValues.XPath, // though, outside of tests, that should be null
cacheValues.FromExamine,
_umbracoContextAccessor
);
return content.CreateModel();
}
private static CacheValues GetCacheValues(int id, Func<int, CacheValues> func)
{
if (_publishedMediaCacheEnabled == false)
return func(id);
var cache = Current.AppCaches.RuntimeCache;
var key = PublishedMediaCacheKey + id;
return (CacheValues)cache.Get(key, () => func(id), _publishedMediaCacheTimespan);
}
internal static void ClearCache(int id)
{
var cache = Current.AppCaches.RuntimeCache;
var sid = id.ToString();
var key = PublishedMediaCacheKey + sid;
// we do clear a lot of things... but the cache refresher is somewhat
// convoluted and it's hard to tell what to clear exactly ;-(
// clear the parent - NOT (why?)
//var exist = (CacheValues) cache.GetCacheItem(key);
//if (exist != null)
// cache.ClearCacheItem(PublishedMediaCacheKey + GetValuesValue(exist.Values, "parentID"));
// clear the item
cache.Clear(key);
// clear all children - in case we moved and their path has changed
var fid = "/" + sid + "/";
cache.ClearOfType<CacheValues>((k, v) =>
GetValuesValue(v.Values, "path", "__Path").Contains(fid));
}
private static string GetValuesValue(IReadOnlyDictionary<string, string> d, params string[] keys)
{
string value = null;
var ignored = keys.Any(x => d.TryGetValue(x, out value));
return value ?? "";
}
}
}

View File

@@ -1,151 +0,0 @@
using System;
using System.Text;
using System.Xml.XPath;
using Umbraco.Core.Cache;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Security;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
class PublishedMemberCache : IPublishedMemberCache
{
private readonly IMemberService _memberService;
private readonly IAppCache _requestCache;
private readonly XmlStore _xmlStore;
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
public PublishedMemberCache(XmlStore xmlStore, IAppCache requestCache, IMemberService memberService,
PublishedContentTypeCache contentTypeCache, IUmbracoContextAccessor umbracoContextAccessor)
{
_requestCache = requestCache;
_memberService = memberService;
_xmlStore = xmlStore;
_contentTypeCache = contentTypeCache;
_umbracoContextAccessor = umbracoContextAccessor;
}
public IPublishedContent GetByProviderKey(object key)
{
return _requestCache.GetCacheItem<IPublishedContent>(
GetCacheKey("GetByProviderKey", key), () =>
{
var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
if (provider.IsUmbracoMembershipProvider() == false)
{
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _memberService.GetByProviderKey(key);
if (result == null) return null;
var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId);
return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel();
});
}
public IPublishedContent GetById(int memberId)
{
return _requestCache.GetCacheItem<IPublishedContent>(
GetCacheKey("GetById", memberId), () =>
{
var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
if (provider.IsUmbracoMembershipProvider() == false)
{
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _memberService.GetById(memberId);
if (result == null) return null;
var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId);
return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel();
});
}
public IPublishedContent GetByUsername(string username)
{
return _requestCache.GetCacheItem<IPublishedContent>(
GetCacheKey("GetByUsername", username), () =>
{
var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
if (provider.IsUmbracoMembershipProvider() == false)
{
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _memberService.GetByUsername(username);
if (result == null) return null;
var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId);
return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel();
});
}
public IPublishedContent GetByEmail(string email)
{
return _requestCache.GetCacheItem<IPublishedContent>(
GetCacheKey("GetByEmail", email), () =>
{
var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
if (provider.IsUmbracoMembershipProvider() == false)
{
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
}
var result = _memberService.GetByEmail(email);
if (result == null) return null;
var type = _contentTypeCache.Get(PublishedItemType.Member, result.ContentTypeId);
return new PublishedMember(result, type, _umbracoContextAccessor).CreateModel();
});
}
public IPublishedContent GetByMember(IMember member)
{
var type = _contentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId);
return new PublishedMember(member, type, _umbracoContextAccessor).CreateModel();
}
public XPathNavigator CreateNavigator()
{
var doc = _xmlStore.GetMemberXml();
return doc.CreateNavigator();
}
public XPathNavigator CreateNavigator(bool preview)
{
return CreateNavigator();
}
public XPathNavigator CreateNodeNavigator(int id, bool preview)
{
var n = _xmlStore.GetMemberXmlNode(id);
return n?.CreateNavigator();
}
private static string GetCacheKey(string key, params object[] additional)
{
var sb = new StringBuilder($"{typeof (MembershipHelper).Name}-{key}");
foreach (var s in additional)
{
sb.Append("-");
sb.Append(s);
}
return sb.ToString();
}
#region Content types
public PublishedContentType GetContentType(int id)
{
return _contentTypeCache.Get(PublishedItemType.Member, id);
}
public PublishedContentType GetContentType(string alias)
{
return _contentTypeCache.Get(PublishedItemType.Member, alias);
}
#endregion
}
}

View File

@@ -1,59 +0,0 @@
using System;
using Umbraco.Core;
using Umbraco.Core.Cache;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <summary>
/// Implements a published snapshot.
/// </summary>
class PublishedSnapshot : IPublishedSnapshot
{
/// <summary>
/// Initializes a new instance of the <see cref="PublishedSnapshot"/> class with a content cache
/// and a media cache.
/// </summary>
public PublishedSnapshot(
PublishedContentCache contentCache,
PublishedMediaCache mediaCache,
PublishedMemberCache memberCache,
DomainCache domainCache)
{
Content = contentCache;
Media = mediaCache;
Members = memberCache;
Domains = domainCache;
}
/// <inheritdoc />
public IPublishedContentCache Content { get; }
/// <inheritdoc />
public IPublishedMediaCache Media { get; }
/// <inheritdoc />
public IPublishedMemberCache Members { get; }
/// <inheritdoc />
public IDomainCache Domains { get; }
/// <inheritdoc />
public IAppCache SnapshotCache => null;
/// <inheritdoc />
public IAppCache ElementsCache => null;
/// <inheritdoc />
public IDisposable ForcedPreview(bool preview, Action<bool> callback = null)
{
// the XML cache does not support forcing preview, really, so, just pretend...
return new ForcedPreviewObject();
}
private class ForcedPreviewObject : DisposableObject
{
protected override void DisposeResources()
{ }
}
}
}

View File

@@ -1,260 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Cache;
using Umbraco.Web.Routing;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <summary>
/// Implements a published snapshot service.
/// </summary>
internal class PublishedSnapshotService : PublishedSnapshotServiceBase
{
private readonly XmlStore _xmlStore;
private readonly RoutesCache _routesCache;
private readonly IPublishedContentTypeFactory _publishedContentTypeFactory;
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IDomainService _domainService;
private readonly IMemberService _memberService;
private readonly IMediaService _mediaService;
private readonly IUserService _userService;
private readonly IAppCache _requestCache;
private readonly IGlobalSettings _globalSettings;
private readonly IDefaultCultureAccessor _defaultCultureAccessor;
private readonly ISiteDomainHelper _siteDomainHelper;
private readonly IEntityXmlSerializer _entitySerializer;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
#region Constructors
// used in WebBootManager + tests
public PublishedSnapshotService(ServiceContext serviceContext,
IPublishedContentTypeFactory publishedContentTypeFactory,
IScopeProvider scopeProvider,
IAppCache requestCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor,
IUmbracoContextAccessor umbracoContextAccessor,
IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository,
IDefaultCultureAccessor defaultCultureAccessor,
ILogger logger,
IGlobalSettings globalSettings,
ISiteDomainHelper siteDomainHelper,
IEntityXmlSerializer entitySerializer,
MainDom mainDom,
bool testing = false, bool enableRepositoryEvents = true)
: this(serviceContext, publishedContentTypeFactory, scopeProvider, requestCache,
publishedSnapshotAccessor, variationContextAccessor, umbracoContextAccessor,
documentRepository, mediaRepository, memberRepository,
defaultCultureAccessor,
logger, globalSettings, siteDomainHelper, entitySerializer, null, mainDom, testing, enableRepositoryEvents)
{
_umbracoContextAccessor = umbracoContextAccessor;
}
// used in some tests
internal PublishedSnapshotService(ServiceContext serviceContext,
IPublishedContentTypeFactory publishedContentTypeFactory,
IScopeProvider scopeProvider,
IAppCache requestCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor,
IUmbracoContextAccessor umbracoContextAccessor,
IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository,
IDefaultCultureAccessor defaultCultureAccessor,
ILogger logger,
IGlobalSettings globalSettings,
ISiteDomainHelper siteDomainHelper,
IEntityXmlSerializer entitySerializer,
PublishedContentTypeCache contentTypeCache,
MainDom mainDom,
bool testing, bool enableRepositoryEvents)
: base(publishedSnapshotAccessor, variationContextAccessor)
{
_routesCache = new RoutesCache();
_publishedContentTypeFactory = publishedContentTypeFactory;
_contentTypeCache = contentTypeCache
?? new PublishedContentTypeCache(serviceContext.ContentTypeService, serviceContext.MediaTypeService, serviceContext.MemberTypeService, publishedContentTypeFactory, logger);
_xmlStore = new XmlStore(serviceContext.ContentTypeService, serviceContext.ContentService, scopeProvider, _routesCache,
_contentTypeCache, publishedSnapshotAccessor, mainDom, testing, enableRepositoryEvents,
documentRepository, mediaRepository, memberRepository, globalSettings, entitySerializer);
_domainService = serviceContext.DomainService;
_memberService = serviceContext.MemberService;
_mediaService = serviceContext.MediaService;
_userService = serviceContext.UserService;
_defaultCultureAccessor = defaultCultureAccessor;
_requestCache = requestCache;
_umbracoContextAccessor = umbracoContextAccessor;
_globalSettings = globalSettings;
_siteDomainHelper = siteDomainHelper;
_entitySerializer = entitySerializer;
}
public override void Dispose()
{
_xmlStore.Dispose();
}
#endregion
#region Environment
public override bool EnsureEnvironment(out IEnumerable<string> errors)
{
// Test creating/saving/deleting a file in the same location as the content xml file
// NOTE: We cannot modify the xml file directly because a background thread is responsible for
// that and we might get lock issues.
try
{
XmlStore.EnsureFilePermission();
errors = Enumerable.Empty<string>();
return true;
}
catch
{
errors = new[] { SystemFiles.GetContentCacheXml(_globalSettings) };
return false;
}
}
#endregion
#region Caches
public override IPublishedSnapshot CreatePublishedSnapshot(string previewToken)
{
// use _requestCache to store recursive properties lookup, etc. both in content
// and media cache. Life span should be the current request. Or, ideally
// the current caches, but that would mean creating an extra cache (StaticCache
// probably) so better use RequestCache.
var domainCache = new DomainCache(_domainService, _defaultCultureAccessor);
return new PublishedSnapshot(
new PublishedContentCache(_xmlStore, domainCache, _requestCache, _globalSettings, _siteDomainHelper,_umbracoContextAccessor, _contentTypeCache, _routesCache, previewToken),
new PublishedMediaCache(_xmlStore, _mediaService, _userService, _requestCache, _contentTypeCache, _entitySerializer, _umbracoContextAccessor),
new PublishedMemberCache(_xmlStore, _requestCache, _memberService, _contentTypeCache, _umbracoContextAccessor),
domainCache);
}
#endregion
#region Preview
public override string EnterPreview(IUser user, int contentId)
{
var previewContent = new PreviewContent(_xmlStore, user.Id);
previewContent.CreatePreviewSet(contentId, true); // preview branch below that content
return previewContent.Token;
//previewContent.ActivatePreviewCookie();
}
public override void RefreshPreview(string previewToken, int contentId)
{
if (previewToken.IsNullOrWhiteSpace()) return;
var previewContent = new PreviewContent(_xmlStore, previewToken);
previewContent.CreatePreviewSet(contentId, true); // preview branch below that content
}
public override void ExitPreview(string previewToken)
{
if (previewToken.IsNullOrWhiteSpace()) return;
var previewContent = new PreviewContent(_xmlStore, previewToken);
previewContent.ClearPreviewSet();
}
#endregion
#region Xml specific
/// <summary>
/// Gets the underlying XML store.
/// </summary>
public XmlStore XmlStore => _xmlStore;
/// <summary>
/// Gets the underlying RoutesCache.
/// </summary>
public RoutesCache RoutesCache => _routesCache;
public bool VerifyContentAndPreviewXml()
{
return XmlStore.VerifyContentAndPreviewXml();
}
public void RebuildContentAndPreviewXml()
{
XmlStore.RebuildContentAndPreviewXml();
}
public bool VerifyMediaXml()
{
return XmlStore.VerifyMediaXml();
}
public void RebuildMediaXml()
{
XmlStore.RebuildMediaXml();
}
public bool VerifyMemberXml()
{
return XmlStore.VerifyMemberXml();
}
public void RebuildMemberXml()
{
XmlStore.RebuildMemberXml();
}
#endregion
#region Change management
public override void Notify(ContentCacheRefresher.JsonPayload[] payloads, out bool draftChanged, out bool publishedChanged)
{
_xmlStore.Notify(payloads, out draftChanged, out publishedChanged);
}
public override void Notify(MediaCacheRefresher.JsonPayload[] payloads, out bool anythingChanged)
{
foreach (var payload in payloads)
PublishedMediaCache.ClearCache(payload.Id);
anythingChanged = true;
}
public override void Notify(ContentTypeCacheRefresher.JsonPayload[] payloads)
{
_xmlStore.Notify(payloads);
if (payloads.Any(x => x.ItemType == typeof(IContentType).Name))
_routesCache.Clear();
}
public override void Notify(DataTypeCacheRefresher.JsonPayload[] payloads)
{
_publishedContentTypeFactory.NotifyDataTypeChanges(payloads.Select(x => x.Id).ToArray());
_xmlStore.Notify(payloads);
}
public override void Notify(DomainCacheRefresher.JsonPayload[] payloads)
{
_routesCache.Clear();
}
#endregion
}
}

View File

@@ -1,115 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
// Note: RoutesCache closely follows the caching strategy dating from v4, which
// is obviously broken in many ways (eg it's a global cache but relying to some
// extend to the content cache, which itself is local to each request...).
// Not going to fix it anyway.
class RoutesCache
{
private ConcurrentDictionary<int, string> _routes;
private ConcurrentDictionary<string, int> _nodeIds;
// NOTE
// RoutesCache is cleared by
// - ContentTypeCacheRefresher, whenever anything happens to any content type
// - DomainCacheRefresher, whenever anything happens to any domain
// - XmlStore, whenever anything happens to the XML cache
/// <summary>
/// Initializes a new instance of the <see cref="RoutesCache"/> class.
/// </summary>
public RoutesCache()
{
Clear();
}
/// <summary>
/// Used ONLY for unit tests
/// </summary>
/// <returns></returns>
internal IDictionary<int, string> GetCachedRoutes()
{
return _routes;
}
/// <summary>
/// Used ONLY for unit tests
/// </summary>
/// <returns></returns>
internal IDictionary<string, int> GetCachedIds()
{
return _nodeIds;
}
#region Public
/// <summary>
/// Stores a route for a node.
/// </summary>
/// <param name="nodeId">The node identified.</param>
/// <param name="route">The route.</param>
/// <param name="trust">A value indicating whether the value can be trusted for inbound routing.</param>
public void Store(int nodeId, string route, bool trust)
{
_routes.AddOrUpdate(nodeId, i => route, (i, s) => route);
if (trust)
_nodeIds.AddOrUpdate(route, i => nodeId, (i, s) => nodeId);
}
/// <summary>
/// Gets a route for a node.
/// </summary>
/// <param name="nodeId">The node identifier.</param>
/// <returns>The route for the node, else null.</returns>
public string GetRoute(int nodeId)
{
string val;
_routes.TryGetValue(nodeId, out val);
return val;
}
/// <summary>
/// Gets a node for a route.
/// </summary>
/// <param name="route">The route.</param>
/// <returns>The node identified for the route, else zero.</returns>
public int GetNodeId(string route)
{
int val;
_nodeIds.TryGetValue(route, out val);
return val;
}
/// <summary>
/// Clears the route for a node.
/// </summary>
/// <param name="nodeId">The node identifier.</param>
public void ClearNode(int nodeId)
{
string route;
if (_routes.TryRemove(nodeId, out route))
{
int id;
_nodeIds.TryRemove(route, out id);
}
}
/// <summary>
/// Clears all routes.
/// </summary>
public void Clear()
{
_routes = new ConcurrentDictionary<int, string>();
_nodeIds = new ConcurrentDictionary<string, int>();
}
#endregion
}
}

View File

@@ -1,154 +0,0 @@
using System;
using System.Xml;
using Umbraco.Core;
using Umbraco.Core.Scoping;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
// TODO: should be a ScopeContextualBase
internal class SafeXmlReaderWriter : IDisposable
{
private readonly bool _scoped;
private readonly Action<XmlDocument> _refresh;
private readonly Action<XmlDocument, bool> _apply;
private IDisposable _releaser;
private bool _applyChanges;
private XmlDocument _xml, _origXml;
private bool _using;
private bool _registerXmlChange;
// the default enlist priority is 100
// enlist with a lower priority to ensure that anything "default" has a clean xml
private const int EnlistPriority = 60;
private const string EnlistKey = "safeXmlReaderWriter";
private SafeXmlReaderWriter(IDisposable releaser, XmlDocument xml, Action<XmlDocument> refresh, Action<XmlDocument, bool> apply, bool isWriter, bool scoped)
{
_releaser = releaser;
_refresh = refresh;
_apply = apply;
_scoped = scoped;
IsWriter = isWriter;
_xml = IsWriter ? Clone(xml) : xml;
}
public static SafeXmlReaderWriter Get(IScopeProvider scopeProvider)
{
return scopeProvider?.Context?.GetEnlisted<SafeXmlReaderWriter>(EnlistKey);
}
public static SafeXmlReaderWriter Get(IScopeProvider scopeProvider, AsyncLock xmlLock, XmlDocument xml, Action<XmlDocument> refresh, Action<XmlDocument, bool> apply, bool writer)
{
var scopeContext = scopeProvider.Context;
// no real scope = just create a reader/writer instance
if (scopeContext == null)
{
// obtain exclusive access to xml and create reader/writer
var releaser = xmlLock.Lock();
return new SafeXmlReaderWriter(releaser, xml, refresh, apply, writer, false);
}
// get or create an enlisted reader/writer
var rw = scopeContext.Enlist(EnlistKey,
() => // creator
{
// obtain exclusive access to xml and create reader/writer
var releaser = xmlLock.Lock();
return new SafeXmlReaderWriter(releaser, xml, refresh, apply, writer, true);
},
(completed, item) => // action
{
item.DisposeForReal(completed);
}, EnlistPriority);
// ensure it's not already in-use - should never happen, just being super safe
if (rw._using)
throw new InvalidOperationException("panic: used.");
rw._using = true;
return rw;
}
public bool IsWriter { get; private set; }
public void UpgradeToWriter(bool auto)
{
if (IsWriter)
throw new InvalidOperationException("Already a writer.");
IsWriter = true;
_xml = Clone(_xml);
}
// for tests
internal static Action Cloning { get; set; }
private XmlDocument Clone(XmlDocument xml)
{
Cloning?.Invoke();
if (_origXml != null)
throw new Exception("panic.");
_origXml = xml;
return (XmlDocument) xml?.CloneNode(true);
}
public XmlDocument Xml
{
get => _xml;
set
{
if (IsWriter == false)
throw new InvalidOperationException("Not a writer.");
_xml = value;
}
}
// registerXmlChange indicates whether to do what should be done when Xml changes,
// that is, to request that the file be written to disk - something we don't want
// to do if we're committing Xml precisely after we've read from disk!
public void AcceptChanges(bool registerXmlChange = true)
{
if (IsWriter == false)
throw new InvalidOperationException("Not a writer.");
_applyChanges = true;
_registerXmlChange |= registerXmlChange;
}
private void DisposeForReal(bool completed)
{
if (IsWriter)
{
// apply changes, or restore the original xml for the current request
if (_applyChanges && completed)
_apply(_xml, _registerXmlChange);
else
_refresh(_origXml);
}
// release the lock
_releaser.Dispose();
_releaser = null;
}
public void Dispose()
{
_using = false;
if (_scoped == false)
{
// really dispose
DisposeForReal(true);
}
else
{
// don't really dispose,
// just apply the changes for the current request
_refresh(_xml);
}
}
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
static class UmbracoContextCache
{
static readonly ConditionalWeakTable<UmbracoContext, ConcurrentDictionary<string, object>> Caches
= new ConditionalWeakTable<UmbracoContext, ConcurrentDictionary<string, object>>();
public static ConcurrentDictionary<string, object> Current
{
get
{
var umbracoContext = UmbracoContext.Current;
// will get or create a value
// a ConditionalWeakTable is thread-safe
// does not prevent the context from being disposed, and then the dictionary will be disposed too
return umbracoContext == null ? null : Caches.GetOrCreateValue(umbracoContext);
}
}
}
}

View File

@@ -1,465 +0,0 @@
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,
IAppCache appCache,
PublishedContentTypeCache contentTypeCache,
IUmbracoContextAccessor umbracoContextAccessor)
:base(umbracoContextAccessor)
{
_xmlNode = xmlNode;
_isPreviewing = isPreviewing;
_appCache = appCache;
_contentTypeCache = contentTypeCache;
_umbracoContextAccessor = umbracoContextAccessor;
}
private readonly XmlNode _xmlNode;
private readonly bool _isPreviewing;
private readonly IAppCache _appCache; // at snapshot/request level (see PublishedContentCache)
private readonly PublishedContentTypeCache _contentTypeCache;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
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 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
{
EnsureNodeInitialized(andChildren: true);
return _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;
}
}
public override PublishedCultureInfo GetCulture(string culture = null) => null;
private static readonly Lazy<Dictionary<string, PublishedCultureInfo>> NoCultures = new Lazy<Dictionary<string, PublishedCultureInfo>>(() => new Dictionary<string, PublishedCultureInfo>());
public override IReadOnlyDictionary<string, PublishedCultureInfo> Cultures => NoCultures.Value;
public override string WriterName
{
get
{
EnsureNodeInitialized();
return _writerName;
}
}
public override string CreatorName
{
get
{
EnsureNodeInitialized();
return _creatorName;
}
}
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 PublishedContentType 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, _umbracoContextAccessor);
_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 _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;
}
// 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 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)
{
// 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, _umbracoContextAccessor))
.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>
/// <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, IUmbracoContextAccessor umbracoContextAccessor)
{
// 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, umbracoContextAccessor)).CreateModel());
}
public static void ClearRequest()
{
Current.AppCaches.RequestCache.ClearByKey(CacheKeyPrefix);
}
private const string CacheKeyPrefix = "CONTENTCACHE_XMLPUBLISHEDCONTENT_";
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.Xml;
using System.Xml.Serialization;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Xml;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <summary>
/// Represents an IDocumentProperty which is created based on an Xml structure.
/// </summary>
[Serializable]
[XmlType(Namespace = "http://umbraco.org/webservices/")]
internal class XmlPublishedProperty : PublishedPropertyBase
{
private readonly string _sourceValue; // the raw, xml node value
// Xml cache not using XPath value... and as for the rest...
// we're single threaded here, keep it simple
private object _objectValue;
private bool _objectValueComputed;
private readonly bool _isPreviewing;
private readonly IPublishedContent _content;
/// <summary>
/// Gets the raw value of the property.
/// </summary>
public override object GetSourceValue(string culture = null, string segment = null) => _sourceValue;
// in the Xml cache, everything is a string, and to have a value
// you want to have a non-null, non-empty string.
public override bool HasValue(string culture = null, string segment = null) => _sourceValue.Trim().Length > 0;
public override object GetValue(string culture = null, string segment = null)
{
// NOT caching the source (intermediate) value since we'll never need it
// everything in Xml cache is per-request anyways
// also, properties should not be shared between requests and therefore
// are single threaded, so the following code should be safe & fast
if (_objectValueComputed) return _objectValue;
var inter = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing);
// initial reference cache level always is .Content
_objectValue = PropertyType.ConvertInterToObject(_content, PropertyCacheLevel.Element, inter, _isPreviewing);
_objectValueComputed = true;
return _objectValue;
}
public override object GetXPathValue(string culture = null, string segment = null) { throw new NotImplementedException(); }
public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, XmlNode propertyXmlData)
: this(propertyType, content, isPreviewing)
{
if (propertyXmlData == null)
throw new ArgumentNullException(nameof(propertyXmlData), "Property xml source is null");
_sourceValue = XmlHelper.GetNodeValue(propertyXmlData);
}
public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing, string propertyData)
: this(propertyType, content, isPreviewing)
{
if (propertyData == null)
throw new ArgumentNullException(nameof(propertyData));
_sourceValue = propertyData;
}
public XmlPublishedProperty(PublishedPropertyType propertyType, IPublishedContent content, bool isPreviewing)
: base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored
{
_sourceValue = string.Empty;
_content = content;
_isPreviewing = isPreviewing;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,182 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Web.Scheduling;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
/// <summary>
/// This is the background task runner that persists the xml file to the file system
/// </summary>
/// <remarks>
/// This is used so that all file saving is done on a web aware worker background thread and all logic is performed async so this
/// process will not interfere with any web requests threads. This is also done as to not require any global locks and to ensure that
/// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting
/// xml structure since the file writes are queued.
/// </remarks>
internal class XmlStoreFilePersister : LatchedBackgroundTaskBase
{
private readonly IBackgroundTaskRunner<XmlStoreFilePersister> _runner;
private readonly ILogger _logger;
private readonly XmlStore _store;
private readonly object _locko = new object();
private bool _released;
private Timer _timer;
private DateTime _initialTouch;
private readonly AsyncLock _runLock = new AsyncLock(); // ensure we run once at a time
// note:
// as long as the runner controls the runs, we know that we run once at a time, but
// when the AppDomain goes down and the runner has completed and yet the persister is
// asked to save, then we need to run immediately - but the runner may be running, so
// we need to make sure there's no collision - hence _runLock
private const int WaitMilliseconds = 4000; // save the cache 4s after the last change (ie every 4s min)
private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes)
// save the cache when the app goes down
public override bool RunsOnShutdown => _timer != null;
// initialize the first instance, which is inactive (not touched yet)
public XmlStoreFilePersister(IBackgroundTaskRunner<XmlStoreFilePersister> runner, XmlStore store, ILogger logger)
: this(runner, store, logger, false)
{ }
// initialize further instances, which are active (touched)
private XmlStoreFilePersister(IBackgroundTaskRunner<XmlStoreFilePersister> runner, XmlStore store, ILogger logger, bool touched)
{
_runner = runner;
_store = store;
_logger = logger;
if (_runner.TryAdd(this) == false)
{
_runner = null; // runner's down
_released = true; // don't mess with timer
return;
}
// runner could decide to run it anytime now
if (touched == false) return;
_logger.Debug<XmlStoreFilePersister>("Created, save in {WaitMilliseconds}ms.", WaitMilliseconds);
_initialTouch = DateTime.Now;
_timer = new Timer(_ => TimerRelease());
_timer.Change(WaitMilliseconds, 0);
}
public XmlStoreFilePersister Touch()
{
// if _released is false then we're going to setup a timer
// then the runner wants to shutdown & run immediately
// this sets _released to true & the timer will trigger eventually & who cares?
// if _released is true, either it's a normal release, or
// a runner shutdown, in which case we won't be able to
// add a new task, and so we'll run immediately
var ret = this;
var runNow = false;
lock (_locko)
{
if (_released) // our timer has triggered OR the runner is shutting down
{
_logger.Debug<XmlStoreFilePersister>("Touched, was released...");
// release: has run or is running, too late, return a new task (adds itself to runner)
if (_runner == null)
{
_logger.Debug<XmlStoreFilePersister>("Runner is down, run now.");
runNow = true;
}
else
{
_logger.Debug<XmlStoreFilePersister>("Create new...");
ret = new XmlStoreFilePersister(_runner, _store, _logger, true);
if (ret._runner == null)
{
// could not enlist with the runner, runner is completed, must run now
_logger.Debug<XmlStoreFilePersister>("Runner is down, run now.");
runNow = true;
}
}
}
else if (_timer == null) // we don't have a timer yet
{
_logger.Debug<XmlStoreFilePersister>("Touched, was idle, start and save in {WaitMilliseconds}ms.", WaitMilliseconds);
_initialTouch = DateTime.Now;
_timer = new Timer(_ => TimerRelease());
_timer.Change(WaitMilliseconds, 0);
}
else // we have a timer
{
// change the timer to trigger in WaitMilliseconds unless we've been touched first more
// than MaxWaitMilliseconds ago and then leave the time unchanged
if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds))
{
_logger.Debug<XmlStoreFilePersister>("Touched, was waiting, can delay, save in {WaitMilliseconds}ms.", WaitMilliseconds);
_timer.Change(WaitMilliseconds, 0);
}
else
{
_logger.Debug<XmlStoreFilePersister>("Touched, was waiting, cannot delay.");
}
}
}
// note: this comes from 7.x where it was not possible to lock the entire content service
// in our case, the XmlStore configures everything so that it is not possible to access content
// when going down, so this should never happen.
if (runNow)
//Run();
_logger.Warn<XmlStoreFilePersister>("Cannot write now because we are going down, changes may be lost.");
return ret; // this, by default, unless we created a new one
}
private void TimerRelease()
{
lock (_locko)
{
_logger.Debug<XmlStoreFilePersister>("Timer: release.");
_released = true;
Release();
}
}
public override bool IsAsync => false;
public override void Run()
{
lock (_locko)
{
_logger.Debug<XmlStoreFilePersister>("Run now (sync).");
// not really needed but safer (it's only us invoking Run, but the method is public...)
_released = true;
}
using (_runLock.Lock())
{
_store.SaveXmlToFile();
}
}
protected override void DisposeResources()
{
base.DisposeResources();
// stop the timer
if (_timer == null) return;
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose();
}
}
}

View File

@@ -536,15 +536,6 @@
<Compile Include="PublishedCache\PublishedContentTypeCache.cs" />
<Compile Include="PublishedCache\DefaultCultureAccessor.cs" />
<Compile Include="PublishedCache\UmbracoContextPublishedSnapshotAccessor.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\DictionaryPublishedContent.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\DomainCache.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\PublishedSnapshot.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\PublishedSnapshotService.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\PreviewContent.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\PublishedMemberCache.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\SafeXmlReaderWriter.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\XmlStore.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\XmlStoreFilePersister.cs" />
<Compile Include="PublishedElementExtensions.cs" />
<Compile Include="PublishedModels\DummyClassSoThatPublishedModelsNamespaceExists.cs" />
<Compile Include="Routing\ContentFinderByUrl.cs" />
@@ -1023,7 +1014,6 @@
<Compile Include="Models\PublishedContentBase.cs" />
<Compile Include="Mvc\AreaRegistrationExtensions.cs" />
<Compile Include="Mvc\QueryStringFilterAttribute.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\PublishedMediaCache.cs" />
<Compile Include="Dictionary\UmbracoCultureDictionary.cs" />
<Compile Include="Dictionary\UmbracoCultureDictionaryFactory.cs" />
<Compile Include="Mvc\MemberAuthorizeAttribute.cs" />
@@ -1031,8 +1021,6 @@
<Compile Include="Mvc\ControllerFactoryExtensions.cs" />
<Compile Include="Mvc\IRenderMvcController.cs" />
<Compile Include="Mvc\SurfaceRouteHandler.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\RoutesCache.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\UmbracoContextCache.cs" />
<Compile Include="Routing\UrlProviderMode.cs" />
<Compile Include="Search\ExamineIndexModel.cs" />
<Compile Include="Security\ValidateRequestAttempt.cs" />
@@ -1139,10 +1127,7 @@
<Compile Include="UmbracoHelper.cs" />
<Compile Include="Mvc\ViewContextExtensions.cs" />
<Compile Include="Mvc\ViewDataContainerExtensions.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\PublishedContentCache.cs" />
<Compile Include="Routing\PublishedContentNotFoundHandler.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\XmlPublishedContent.cs" />
<Compile Include="PublishedCache\XmlPublishedCache\XmlPublishedProperty.cs" />
<Compile Include="Mvc\Constants.cs" />
<Compile Include="Mvc\IFilteredControllerFactory.cs" />
<Compile Include="Mvc\MasterControllerFactory.cs" />
@@ -1194,7 +1179,6 @@
<Compile Include="UmbracoWebService.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Editors\XmlDataIntegrityController.cs" />
<Compile Include="WebViewPageExtensions.cs" />
</ItemGroup>
<ItemGroup>