Web.Routing - RoutesCache goes with PublishedCache

This commit is contained in:
Stephan
2013-03-20 16:01:49 -01:00
parent 6e3c4854b9
commit fcb8f0d417
45 changed files with 499 additions and 691 deletions

View File

@@ -12,10 +12,12 @@ namespace Umbraco.Web.PublishedCache
internal abstract class ContextualPublishedCache
{
protected readonly UmbracoContext UmbracoContext;
private readonly IPublishedCache _cache;
protected ContextualPublishedCache(UmbracoContext umbracoContext)
protected ContextualPublishedCache(UmbracoContext umbracoContext, IPublishedCache cache)
{
UmbracoContext = umbracoContext;
_cache = cache;
}
/// <summary>
@@ -23,13 +25,19 @@ namespace Umbraco.Web.PublishedCache
/// </summary>
/// <param name="contentId">The content unique identifier.</param>
/// <returns>The content, or null.</returns>
public abstract IPublishedContent GetById(int contentId);
public virtual IPublishedContent GetById(int contentId)
{
return _cache.GetById(UmbracoContext, contentId);
}
/// <summary>
/// Gets contents at root.
/// </summary>
/// <returns>The contents.</returns>
public abstract IEnumerable<IPublishedContent> GetAtRoot();
public virtual IEnumerable<IPublishedContent> GetAtRoot()
{
return _cache.GetAtRoot(UmbracoContext);
}
/// <summary>
/// Gets a content resulting from an XPath query.
@@ -37,7 +45,10 @@ namespace Umbraco.Web.PublishedCache
/// <param name="xpath">The XPath query.</param>
/// <param name="vars">Optional XPath variables.</param>
/// <returns>The content, or null.</returns>
public abstract IPublishedContent GetSingleByXPath(string xpath, Core.Xml.XPathVariable[] vars);
public virtual IPublishedContent GetSingleByXPath(string xpath, Core.Xml.XPathVariable[] vars)
{
return _cache.GetSingleByXPath(UmbracoContext, xpath, vars);
}
/// <summary>
/// Gets contents resulting from an XPath query.
@@ -45,6 +56,18 @@ namespace Umbraco.Web.PublishedCache
/// <param name="xpath">The XPath query.</param>
/// <param name="vars">Optional XPath variables.</param>
/// <returns>The contents.</returns>
public abstract IEnumerable<IPublishedContent> GetByXPath(string xpath, Core.Xml.XPathVariable[] vars);
public virtual IEnumerable<IPublishedContent> GetByXPath(string xpath, Core.Xml.XPathVariable[] vars)
{
return _cache.GetByXPath(UmbracoContext, xpath, vars);
}
/// <summary>
/// Gets a value indicating whether the underlying non-contextual cache contains published content.
/// </summary>
/// <returns>A value indicating whether the underlying non-contextual cache contains published content.</returns>
public virtual bool HasContent()
{
return _cache.HasContent();
}
}
}

View File

@@ -19,7 +19,7 @@ namespace Umbraco.Web.PublishedCache
/// <param name="cache">A published content cache.</param>
/// <param name="umbracoContext">A context.</param>
public ContextualPublishedContentCache(IPublishedContentCache cache, UmbracoContext umbracoContext)
: base(umbracoContext)
: base(umbracoContext, cache)
{
_cache = cache;
}
@@ -31,65 +31,31 @@ namespace Umbraco.Web.PublishedCache
internal IPublishedContentCache InnerCache { get { return _cache; } }
/// <summary>
/// Gets a content identified by its unique identifier.
/// Gets content identified by a route.
/// </summary>
/// <param name="contentId">The content unique identifier.</param>
/// <param name="route">The route</param>
/// <param name="hideTopLevelNode">FIXME</param>
/// <returns>The content, or null.</returns>
public override IPublishedContent GetById(int contentId)
{
return _cache.GetById(UmbracoContext, contentId);
}
/// <summary>
/// Gets contents at root.
/// </summary>
/// <returns>The contents.</returns>
public override IEnumerable<IPublishedContent> GetAtRoot()
{
return _cache.GetAtRoot(UmbracoContext);
}
/// <summary>
/// Gets a content resulting from an XPath query.
/// </summary>
/// <param name="xpath">The XPath query.</param>
/// <param name="vars">Optional XPath variables.</param>
/// <returns>The content, or null.</returns>
public override IPublishedContent GetSingleByXPath(string xpath, Core.Xml.XPathVariable[] vars)
{
return _cache.GetSingleByXPath(UmbracoContext, xpath, vars);
}
/// <summary>
/// Gets contents resulting from an XPath query.
/// </summary>
/// <param name="xpath">The XPath query.</param>
/// <param name="vars">Optional XPath variables.</param>
/// <returns>The contents.</returns>
public override IEnumerable<IPublishedContent> GetByXPath(string xpath, Core.Xml.XPathVariable[] vars)
{
return _cache.GetByXPath(UmbracoContext, xpath, vars);
}
// FIXME do we want that one here?
/// <remarks>A valid route is either a simple path eg <c>/foo/bar/nil</c> or a root node id and a path, eg <c>123/foo/bar/nil</c>.</remarks>
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null)
{
return _cache.GetByRoute(UmbracoContext, route, hideTopLevelNode);
}
/// <summary>
/// Gets the route for a content identified by its unique identifier.
/// </summary>
/// <param name="contentId">The content unique identifier.</param>
/// <returns>The route.</returns>
public string GetRouteById(int contentId)
{
return _cache.GetRouteById(UmbracoContext, contentId);
}
// FIXME do we want that one here?
public IPublishedContent GetByUrlAlias(int rootNodeId, string alias)
{
return _cache.GetByUrlAlias(UmbracoContext, rootNodeId, alias);
}
/// <summary>
/// Gets a value indicating whether the underlying non-contextual cache contains published content.
/// </summary>
/// <returns>A value indicating whether the underlying non-contextual cache contains published content.</returns>
public bool HasContent()
{
return _cache.HasContent();
}
}
}

View File

@@ -19,50 +19,9 @@ namespace Umbraco.Web.PublishedCache
/// <param name="cache">A published media cache.</param>
/// <param name="umbracoContext">A context.</param>
public ContextualPublishedMediaCache(IPublishedMediaCache cache, UmbracoContext umbracoContext)
: base(umbracoContext)
: base(umbracoContext, cache)
{
_cache = cache;
}
/// <summary>
/// Gets a content identified by its unique identifier.
/// </summary>
/// <param name="contentId">The content unique identifier.</param>
/// <returns>The content, or null.</returns>
public override IPublishedContent GetById(int contentId)
{
return _cache.GetById(UmbracoContext, contentId);
}
/// <summary>
/// Gets contents at root.
/// </summary>
/// <returns>The contents.</returns>
public override IEnumerable<IPublishedContent> GetAtRoot()
{
return _cache.GetAtRoot(UmbracoContext);
}
/// <summary>
/// Gets a content resulting from an XPath query.
/// </summary>
/// <param name="xpath">The XPath query.</param>
/// <param name="vars">Optional XPath variables.</param>
/// <returns>The content, or null.</returns>
public override IPublishedContent GetSingleByXPath(string xpath, Core.Xml.XPathVariable[] vars)
{
return _cache.GetSingleByXPath(UmbracoContext, xpath, vars);
}
/// <summary>
/// Gets contents resulting from an XPath query.
/// </summary>
/// <param name="xpath">The XPath query.</param>
/// <param name="vars">Optional XPath variables.</param>
/// <returns>The contents.</returns>
public override IEnumerable<IPublishedContent> GetByXPath(string xpath, Core.Xml.XPathVariable[] vars)
{
return _cache.GetByXPath(UmbracoContext, xpath, vars);
}
}
}

View File

@@ -49,8 +49,11 @@ namespace Umbraco.Web.PublishedCache
/// <returns>The contents.</returns>
IEnumerable<IPublishedContent> GetByXPath(UmbracoContext umbracoContext, string xpath, XPathVariable[] vars);
// ... GetXPath single or multi
// ... pass the helper and NOT the store, so we're consistent?!
/// <summary>
/// Gets a value indicating whether the cache contains published content.
/// </summary>
/// <returns>A value indicating whether the cache contains published content.</returns>
bool HasContent();
//TODO: SD: We should make this happen! This will allow us to natively do a GetByDocumentType query
// on the UmbracoHelper (or an internal DataContext that it uses, etc...)

View File

@@ -9,16 +9,28 @@ namespace Umbraco.Web.PublishedCache
{
internal interface IPublishedContentCache : IPublishedCache
{
// FIXME do we want that one?
/// <summary>
/// Gets content identified by a route.
/// </summary>
/// <param name="umbracoContext">The context.</param>
/// <param name="route">The route</param>
/// <param name="hideTopLevelNode">A value forcing the HideTopLevelNode setting.</param>
/// <returns>The content, or null.</returns>
/// <remarks>
/// <para>A valid route is either a simple path eg <c>/foo/bar/nil</c> or a root node id and a path, eg <c>123/foo/bar/nil</c>.</para>
/// <para>If <param name="hideTopLevelNode" /> is <c>null</c> then the settings value is used.</para>
/// </remarks>
IPublishedContent GetByRoute(UmbracoContext umbracoContext, string route, bool? hideTopLevelNode = null);
/// <summary>
/// Gets the route for a content identified by its unique identifier.
/// </summary>
/// <param name="umbracoContext">The context.</param>
/// <param name="contentId">The content unique identifier.</param>
/// <returns>The route.</returns>
string GetRouteById(UmbracoContext umbracoContext, int contentId);
// FIXME do we want that one?
IPublishedContent GetByUrlAlias(UmbracoContext umbracoContext, int rootNodeId, string alias);
/// <summary>
/// Gets a value indicating whether the cache contains published content.
/// </summary>
/// <returns>A value indicating whether the cache contains published content.</returns>
bool HasContent();
}
}

View File

@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Xml;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Xml;
using Umbraco.Web.Routing;
using umbraco;
using System.Linq;
using umbraco.BusinessLogic;
@@ -13,9 +16,180 @@ namespace Umbraco.Web.PublishedCache.LegacyXmlCache
{
internal class PublishedContentCache : IPublishedContentCache
{
#region XPath Strings
#region Routes cache
class XPathStringsDefinition
private readonly RoutesCache _routesCache = new RoutesCache(!UnitTesting);
// for INTERNAL, UNIT TESTS use ONLY
internal RoutesCache RoutesCache { get { return _routesCache; } }
// for INTERNAL, UNIT TESTS use ONLY
internal static bool UnitTesting = false;
/// <summary>
/// Gets content identified by a route.
/// </summary>
/// <param name="umbracoContext">The context.</param>
/// <param name="route">The route</param>
/// <param name="hideTopLevelNode">A value forcing the HideTopLevelNode setting.</param>
/// <returns>The content, or null.</returns>
/// <remarks>
/// <para>A valid route is either a simple path eg <c>/foo/bar/nil</c> or a root node id and a path, eg <c>123/foo/bar/nil</c>.</para>
/// <para>If <param name="hideTopLevelNode" /> is <c>null</c> then the settings value is used.</para>
/// </remarks>
public IPublishedContent GetByRoute(UmbracoContext umbracoContext, string route, bool? hideTopLevelNode = null)
{
if (route == null) throw new ArgumentNullException("route");
// try to get from cache if not previewing
var contentId = umbracoContext.InPreviewMode
? 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(umbracoContext, contentId);
if (content == null)
_routesCache.ClearNode(contentId);
}
// still have nothing? actually determine the id
hideTopLevelNode = hideTopLevelNode ?? GlobalSettings.HideTopLevelNodeFromPath; // default = settings
content = content ?? DetermineIdByRoute(umbracoContext, route, hideTopLevelNode.Value);
// cache if we have a content and not previewing
if (content != null && !umbracoContext.InPreviewMode)
{
var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/')));
var iscanon = !UnitTesting && !DomainHelper.ExistsDomainInPath(DomainHelper.GetAllDomains(false), content.Path, domainRootNodeId);
// and only if this is the canonical url (the one GetUrl would return)
if (iscanon)
_routesCache.Store(contentId, route);
}
return content;
}
/// <summary>
/// Gets the route for a content identified by its unique identifier.
/// </summary>
/// <param name="umbracoContext">The context.</param>
/// <param name="contentId">The content unique identifier.</param>
/// <returns>The route.</returns>
public string GetRouteById(UmbracoContext umbracoContext, int contentId)
{
// try to get from cache if not previewing
var route = umbracoContext.InPreviewMode
? null
: _routesCache.GetRoute(contentId);
// if found in cache then return
if (route != null)
return route;
// else actually determine the route
route = DetermineRouteById(umbracoContext, contentId);
// cache if we have a route and not previewing
if (route != null && !umbracoContext.InPreviewMode)
_routesCache.Store(contentId, route);
return route;
}
IPublishedContent DetermineIdByRoute(UmbracoContext umbracoContext, string route, bool hideTopLevelNode)
{
if (route == null) throw new ArgumentNullException("route");
//the route always needs to be lower case because we only store the urlName attribute in lower case
route = route.ToLowerInvariant();
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
IEnumerable<XPathVariable> vars;
var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode, out vars);
//check if we can find the node in our xml cache
var content = GetSingleByXPath(umbracoContext, xpath, vars == null ? null : vars.ToArray());
// if hideTopLevelNodePath is true then for url /foo we looked for /*/foo
// but maybe that was the url of a non-default top-level node, so we also
// have to look for /foo (see note in ApplyHideTopLevelNodeFromPath).
if (content == null && hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0)
{
xpath = CreateXpathQuery(startNodeId, path, false, out vars);
content = GetSingleByXPath(umbracoContext, xpath, vars == null ? null : vars.ToArray());
}
return content;
}
string DetermineRouteById(UmbracoContext umbracoContext, int contentId)
{
var node = GetById(umbracoContext, 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 && n != null) // n is null at root
{
// get the url
var urlName = n.UrlName;
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 && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath)
ApplyHideTopLevelNodeFromPath(umbracoContext, node, pathParts);
// assemble the route
pathParts.Reverse();
var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
var route = (n == null ? "" : n.Id.ToString(CultureInfo.InvariantCulture)) + path;
return route;
}
static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublishedContent node, IList<string> pathParts)
{
// in theory if hideTopLevelNodeFromPath is true, then there should be only once
// top-level node, or else domains should be assigned. but for backward compatibility
// we add this check - we look for the document matching "/" and if it's not us, then
// we do not hide the top level path
// it has to be taken care of in GetByRoute too so if
// "/foo" fails (looking for "/*/foo") we try also "/foo".
// this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but
// that's the way it works pre-4.10 and we try to be backward compat for the time being
if (node.Parent == null)
{
var rootNode = umbracoContext.ContentCache.GetByRoute("/", true);
if (rootNode.Id == node.Id) // remove only if we're the default node
pathParts.RemoveAt(pathParts.Count - 1);
}
else
{
pathParts.RemoveAt(pathParts.Count - 1);
}
}
#endregion
#region XPath Strings
class XPathStringsDefinition
{
public int Version { get; private set; }
@@ -134,39 +308,6 @@ namespace Umbraco.Web.PublishedCache.LegacyXmlCache
return ConvertToDocuments(nodes);
}
//FIXME keep here or remove?
public IPublishedContent GetByRoute(UmbracoContext umbracoContext, string route, bool? hideTopLevelNode = null)
{
if (route == null) throw new ArgumentNullException("route");
//set the default to be what is in the settings
hideTopLevelNode = hideTopLevelNode ?? GlobalSettings.HideTopLevelNodeFromPath;
//the route always needs to be lower case because we only store the urlName attribute in lower case
route = route.ToLowerInvariant();
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
IEnumerable<XPathVariable> vars;
var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode.Value, out vars);
//check if we can find the node in our xml cache
var content = GetSingleByXPath(umbracoContext, xpath, vars == null ? null : vars.ToArray());
// if hideTopLevelNodePath is true then for url /foo we looked for /*/foo
// but maybe that was the url of a non-default top-level node, so we also
// have to look for /foo (see note in NiceUrlProvider).
if (content == null && hideTopLevelNode.Value && path.Length > 1 && path.IndexOf('/', 1) < 0)
{
xpath = CreateXpathQuery(startNodeId, path, false, out vars);
content = GetSingleByXPath(umbracoContext, xpath, vars == null ? null : vars.ToArray());
}
return content;
}
// FIXME MOVE THAT ONE OUT OF HERE?
public IPublishedContent GetByUrlAlias(UmbracoContext umbracoContext, int rootNodeId, string alias)
{

View File

@@ -77,6 +77,8 @@ namespace Umbraco.Web.PublishedCache.LegacyXmlCache
throw new NotImplementedException("PublishedMediaCache does not support XPath queries.");
}
public bool HasContent() { throw new NotImplementedException(); }
private ExamineManager GetExamineManagerSafe()
{
try

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Umbraco.Core.ObjectResolution;
namespace Umbraco.Web.PublishedCache.LegacyXmlCache
{
class RoutesCache
{
private ConcurrentDictionary<int, string> _routes;
private ConcurrentDictionary<string, int> _nodeIds;
/// <summary>
/// Initializes a new instance of the <see cref="RoutesCache"/> class.
/// </summary>
public RoutesCache()
: this(true)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="RoutesCache"/> class.
/// </summary>
internal RoutesCache(bool bindToEvents)
{
Clear();
if (bindToEvents)
{
Resolution.Frozen += ResolutionFrozen;
}
}
/// <summary>
/// Once resolution is frozen, then we can bind to the events that we require
/// </summary>
/// <param name="s"></param>
/// <param name="args"></param>
private void ResolutionFrozen(object s, EventArgs args)
{
// content - whenever the entire XML cache is rebuilt (from disk cache or from database)
// we must clear the cache entirely
global::umbraco.content.AfterRefreshContent += (sender, e) => Clear();
// document - whenever a document is updated in, or removed from, the XML cache
// we must clear the cache - at the moment, we clear the entire cache
// TODO could we do partial updates instead of clearing the whole cache?
global::umbraco.content.AfterUpdateDocumentCache += (sender, e) => Clear();
global::umbraco.content.AfterClearDocumentCache += (sender, e) => Clear();
// domains - whenever a domain change we must clear the cache
// because routes contain the id of root nodes of domains
// TODO could we do partial updates instead of clearing the whole cache?
global::umbraco.cms.businesslogic.web.Domain.AfterDelete += (sender, e) => Clear();
global::umbraco.cms.businesslogic.web.Domain.AfterSave += (sender, e) => Clear();
global::umbraco.cms.businesslogic.web.Domain.New += (sender, e) => Clear();
// FIXME
// the content class needs to be refactored - at the moment
// content.XmlContentInternal setter does not trigger any event
// content.UpdateDocumentCache(List<Document> Documents) does not trigger any event
// content.RefreshContentFromDatabaseAsync triggers AfterRefresh _while_ refreshing
// etc...
// in addition some events do not make sense... we trigger Publish when moving
// a node, which we should not (the node is moved, not published...) etc.
}
/// <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>
public void Store(int nodeId, string route)
{
_routes.AddOrUpdate(nodeId, i => route, (i, s) => route);
_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)
{
if (!_routes.ContainsKey(nodeId)) return;
string key;
if (!_routes.TryGetValue(nodeId, out key)) return;
int val;
_nodeIds.TryRemove(key, out val);
string val2;
_routes.TryRemove(nodeId, out val2);
}
/// <summary>
/// Clears all routes.
/// </summary>
public void Clear()
{
_routes = new ConcurrentDictionary<int, string>();
_nodeIds = new ConcurrentDictionary<string, int>();
}
#endregion
}
}