From 27bcea78d2b05ad62ff87e98f22d2ac1d8772edc Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Sat, 8 Sep 2012 13:22:45 +0700 Subject: [PATCH] Started implementing new IPublishedMediaStore which will replace the notion of the legacy ExamineBackedMedia and moving forward will open up many nice opportunities. This will be used for the media accessor in the new DynamicDocument. Renamed XmlPublishedContentStore to just DefaultPublishedContentStore. Made GetProperty an extension method for IDocument and removed from the interface as it is not needed there. --- .../Dynamics/DynamicBackingItem.cs | 5 +- src/Umbraco.Core/Models/DocumentExtensions.cs | 21 ++ src/Umbraco.Core/Models/IDocument.cs | 3 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../DynamicDocument/DynamicDocumentTests.cs | 2 +- .../DynamicDocument/DynamicNodeTests.cs | 2 +- src/Umbraco.Tests/PublishContentStoreTests.cs | 4 +- src/Umbraco.Tests/Routing/BaseRoutingTest.cs | 2 +- ...ore.cs => DefaultPublishedContentStore.cs} | 39 +-- src/Umbraco.Web/DefaultPublishedMediaStore.cs | 230 ++++++++++++++++++ src/Umbraco.Web/IPublishedContentStore.cs | 9 +- src/Umbraco.Web/IPublishedMediaStore.cs | 13 + src/Umbraco.Web/Models/XmlDocument.cs | 12 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 +- src/Umbraco.Web/WebBootManager.cs | 2 +- 15 files changed, 286 insertions(+), 63 deletions(-) create mode 100644 src/Umbraco.Core/Models/DocumentExtensions.cs rename src/Umbraco.Web/{XmlPublishedContentStore.cs => DefaultPublishedContentStore.cs} (73%) create mode 100644 src/Umbraco.Web/DefaultPublishedMediaStore.cs create mode 100644 src/Umbraco.Web/IPublishedMediaStore.cs diff --git a/src/Umbraco.Core/Dynamics/DynamicBackingItem.cs b/src/Umbraco.Core/Dynamics/DynamicBackingItem.cs index f80b204bd4..de7a024858 100644 --- a/src/Umbraco.Core/Dynamics/DynamicBackingItem.cs +++ b/src/Umbraco.Core/Dynamics/DynamicBackingItem.cs @@ -106,10 +106,7 @@ namespace Umbraco.Core.Dynamics //if we're looking for a user defined property if (checkUserProperty) { - var prop = content.GetProperty(alias) - ?? (alias[0].IsUpperCase() //if it's null, try to get it with a different casing format (pascal vs camel) - ? content.GetProperty(alias.ConvertCase(StringAliasCaseType.CamelCase)) - : content.GetProperty(alias.ConvertCase(StringAliasCaseType.PascalCase))); + var prop = content.GetProperty(alias); return prop == null ? null diff --git a/src/Umbraco.Core/Models/DocumentExtensions.cs b/src/Umbraco.Core/Models/DocumentExtensions.cs new file mode 100644 index 0000000000..84be18b9ae --- /dev/null +++ b/src/Umbraco.Core/Models/DocumentExtensions.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// Extension methods for IDocument + /// + public static class DocumentExtensions + { + /// + /// Returns the property based on the case insensitive match of the alias + /// + /// + /// + /// + public static IDocumentProperty GetProperty(this IDocument d, string alias) + { + return d.Properties.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IDocument.cs b/src/Umbraco.Core/Models/IDocument.cs index c162157eab..fae2866ff2 100644 --- a/src/Umbraco.Core/Models/IDocument.cs +++ b/src/Umbraco.Core/Models/IDocument.cs @@ -31,7 +31,6 @@ namespace Umbraco.Core.Models Guid Version { get; } int Level { get; } Collection Properties { get; } - IEnumerable Children { get; } - IDocumentProperty GetProperty(string alias); + IEnumerable Children { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 221f573c1f..5fdeed9fec 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -90,6 +90,7 @@ + diff --git a/src/Umbraco.Tests/DynamicDocument/DynamicDocumentTests.cs b/src/Umbraco.Tests/DynamicDocument/DynamicDocumentTests.cs index 40aec8da96..b25a87ff65 100644 --- a/src/Umbraco.Tests/DynamicDocument/DynamicDocumentTests.cs +++ b/src/Umbraco.Tests/DynamicDocument/DynamicDocumentTests.cs @@ -54,7 +54,7 @@ namespace Umbraco.Tests.DynamicDocument { var template = Template.MakeNew("test", new User(0)); var ctx = GetUmbracoContext("/test", template.Id); - var contentStore = new XmlPublishedContentStore(); + var contentStore = new DefaultPublishedContentStore(); var doc = contentStore.GetDocumentById(ctx, id); Assert.IsNotNull(doc); var dynamicNode = new Core.Dynamics.DynamicDocument(doc); diff --git a/src/Umbraco.Tests/DynamicDocument/DynamicNodeTests.cs b/src/Umbraco.Tests/DynamicDocument/DynamicNodeTests.cs index 020451936a..61a510001a 100644 --- a/src/Umbraco.Tests/DynamicDocument/DynamicNodeTests.cs +++ b/src/Umbraco.Tests/DynamicDocument/DynamicNodeTests.cs @@ -35,7 +35,7 @@ namespace Umbraco.Tests.DynamicDocument { var template = Template.MakeNew("test", new User(0)); var ctx = GetUmbracoContext("/test", template.Id); - var contentStore = new XmlPublishedContentStore(); + var contentStore = new DefaultPublishedContentStore(); var node = new DynamicNode( new DynamicBackingItem( new Node(ctx.GetXml().SelectSingleNode("//*[@id='" + id + "' and @isDoc]")))); diff --git a/src/Umbraco.Tests/PublishContentStoreTests.cs b/src/Umbraco.Tests/PublishContentStoreTests.cs index d4decac6e5..e686582e9a 100644 --- a/src/Umbraco.Tests/PublishContentStoreTests.cs +++ b/src/Umbraco.Tests/PublishContentStoreTests.cs @@ -13,7 +13,7 @@ namespace Umbraco.Tests { private FakeHttpContextFactory _httpContextFactory; private UmbracoContext _umbracoContext; - private XmlPublishedContentStore _publishedContentStore; + private DefaultPublishedContentStore _publishedContentStore; [SetUp] public void SetUp() @@ -56,7 +56,7 @@ namespace Umbraco.Tests return xDoc; }; - _publishedContentStore = new XmlPublishedContentStore(); + _publishedContentStore = new DefaultPublishedContentStore(); } diff --git a/src/Umbraco.Tests/Routing/BaseRoutingTest.cs b/src/Umbraco.Tests/Routing/BaseRoutingTest.cs index eb489b01d0..2dc58d9937 100644 --- a/src/Umbraco.Tests/Routing/BaseRoutingTest.cs +++ b/src/Umbraco.Tests/Routing/BaseRoutingTest.cs @@ -38,7 +38,7 @@ namespace Umbraco.Tests.Routing protected RoutingContext GetRoutingContext(string url, int templateId, RouteData routeData = null) { var umbracoContext = GetUmbracoContext(url, templateId, routeData); - var contentStore = new XmlPublishedContentStore(); + var contentStore = new DefaultPublishedContentStore(); var niceUrls = new NiceUrlProvider(contentStore, umbracoContext); var routingRequest = new RoutingContext( umbracoContext, diff --git a/src/Umbraco.Web/XmlPublishedContentStore.cs b/src/Umbraco.Web/DefaultPublishedContentStore.cs similarity index 73% rename from src/Umbraco.Web/XmlPublishedContentStore.cs rename to src/Umbraco.Web/DefaultPublishedContentStore.cs index 8eec803f05..2e41db2819 100644 --- a/src/Umbraco.Web/XmlPublishedContentStore.cs +++ b/src/Umbraco.Web/DefaultPublishedContentStore.cs @@ -1,7 +1,7 @@ using System; using System.Text; using System.Xml; -using Umbraco.Core; +using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Web.Routing; using umbraco; @@ -11,9 +11,9 @@ using umbraco.interfaces; namespace Umbraco.Web { /// - /// An IContentStore which uses the Xml cache system to return data + /// An IPublishedContentStore which uses the Xml cache system to return data /// - internal class XmlPublishedContentStore : IPublishedContentStore + internal class DefaultPublishedContentStore : DefaultPublishedMediaStore, IPublishedContentStore { private IDocument ConvertToDocument(XmlNode xmlNode) @@ -24,7 +24,7 @@ namespace Umbraco.Web return new Models.XmlDocument(xmlNode); } - public IDocument GetDocumentById(UmbracoContext umbracoContext, int nodeId) + public override IDocument GetDocumentById(UmbracoContext umbracoContext, int nodeId) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); @@ -84,37 +84,6 @@ namespace Umbraco.Web return ConvertToDocument(GetXml(umbracoContext).SelectSingleNode(xpath)); } - //public IDocument GetNodeParent(IDocument node) - //{ - // return node.Parent; - //} - - public string GetDocumentProperty(UmbracoContext umbracoContext, IDocument node, string propertyAlias) - { - if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); - if (node == null) throw new ArgumentNullException("node"); - if (propertyAlias == null) throw new ArgumentNullException("propertyAlias"); - - if (propertyAlias.StartsWith("@")) - { - //if it starts with an @ then its a property of the object, not a user defined property - var propName = propertyAlias.TrimStart('@'); - var prop = TypeHelper.GetProperty(typeof(IDocument), propName, true, false, false, false); - if (prop == null) - throw new ArgumentException("The property name " + propertyAlias + " was not found on type " + typeof(IDocument)); - var val = prop.GetValue(node, null); - var valAsString = val == null ? "" : val.ToString(); - return valAsString; - } - else - { - var prop = node.GetProperty(propertyAlias); - return prop == null ? null : Convert.ToString(prop.Value); - //var propertyNode = node.SelectSingleNode("./" + propertyAlias); - //return propertyNode == null ? null : propertyNode.InnerText; - } - } - XmlDocument GetXml(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); diff --git a/src/Umbraco.Web/DefaultPublishedMediaStore.cs b/src/Umbraco.Web/DefaultPublishedMediaStore.cs new file mode 100644 index 0000000000..ec8ad95dc5 --- /dev/null +++ b/src/Umbraco.Web/DefaultPublishedMediaStore.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Xml.XPath; +using Examine; +using Umbraco.Core; +using Umbraco.Core.Dynamics; +using Umbraco.Core.Models; + +namespace Umbraco.Web +{ + /// + /// An IPublishedMediaStore that first checks for the media in Examine, and then reverts to the database + /// + internal class DefaultPublishedMediaStore : IPublishedMediaStore + { + public virtual IDocument GetDocumentById(UmbracoContext umbracoContext, int nodeId) + { + return GetUmbracoMedia(nodeId); + } + + public virtual string GetDocumentProperty(UmbracoContext umbracoContext, IDocument node, string propertyAlias) + { + if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); + if (node == null) throw new ArgumentNullException("node"); + if (propertyAlias == null) throw new ArgumentNullException("propertyAlias"); + + if (propertyAlias.StartsWith("@")) + { + //if it starts with an @ then its a property of the object, not a user defined property + var propName = propertyAlias.TrimStart('@'); + var prop = TypeHelper.GetProperty(typeof(IDocument), propName, true, false, false, false); + if (prop == null) + throw new ArgumentException("The property name " + propertyAlias + " was not found on type " + typeof(IDocument)); + var val = prop.GetValue(node, null); + var valAsString = val == null ? "" : val.ToString(); + return valAsString; + } + else + { + var prop = node.GetProperty(propertyAlias); + return prop == null ? null : Convert.ToString(prop.Value); + //var propertyNode = node.SelectSingleNode("./" + propertyAlias); + //return propertyNode == null ? null : propertyNode.InnerText; + } + } + + private IDocument GetUmbracoMedia(int id) + { + + try + { + //first check in Examine as this is WAY faster + var criteria = ExamineManager.Instance + .SearchProviderCollection["InternalSearcher"] + .CreateSearchCriteria("media"); + var filter = criteria.Id(id); + var results = ExamineManager + .Instance.SearchProviderCollection["InternalSearcher"] + .Search(filter.Compile()); + if (results.Any()) + { + return ConvertFromSearchResult(results.First()); + } + } + catch (FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced 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! + } + + var media = global::umbraco.library.GetMedia(id, true); + if (media != null && media.Current != null) + { + media.MoveNext(); + return ConvertFromXPathNavigator(media.Current); + } + + return null; + } + + internal IDocument ConvertFromSearchResult(SearchResult searchResult) + { + //TODO: Unit test this + throw new NotImplementedException(); + } + + internal IDocument ConvertFromXPathNavigator(XPathNavigator xpath) + { + //TODO: Unit test this + + if (xpath == null) throw new ArgumentNullException("xpath"); + + var values = new Dictionary {{"nodeName", xpath.GetAttribute("nodeName", "")}}; + + 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()) + { + values.Add(result.Current.Name, result.Current.Value); + while (result.Current.MoveToNextAttribute()) + { + values.Add(result.Current.Name, result.Current.Value); + } + result.Current.MoveToParent(); + } + } + //add the user props + while (result.MoveNext()) + { + if (result.Current != null && !result.Current.HasAttributes) + { + string value = result.Current.Value; + if (string.IsNullOrEmpty(value)) + { + if (result.Current.HasAttributes || result.Current.SelectChildren(XPathNodeType.Element).Count > 0) + { + value = result.Current.OuterXml; + } + } + values.Add(result.Current.Name, value); + } + } + + return new DictionaryDocument(values, d => d.ParentId.HasValue ? GetUmbracoMedia(d.ParentId.Value) : null); + } + + /// + /// An IDocument that is represented all by a dictionary. + /// + /// + /// This is a helper class and definitely not intended for public use, it expects that all of the values required + /// to create an IDocument exist in the dictionary by specific aliases. + /// + internal class DictionaryDocument : IDocument + { + + //TODO: Unit test this! + + public DictionaryDocument(IDictionary valueDictionary, Func getParent) + { + if (valueDictionary == null) throw new ArgumentNullException("valueDictionary"); + if (getParent == null) throw new ArgumentNullException("getParent"); + + _getParent = getParent; + + ValidateAndSetProperty(valueDictionary, val => Id = int.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int! + ValidateAndSetProperty(valueDictionary, val => TemplateId = int.Parse(val), "template", "templateId"); + ValidateAndSetProperty(valueDictionary, val => SortOrder = int.Parse(val), "sortOrder"); + ValidateAndSetProperty(valueDictionary, val => Name = val, "nodeName", "__nodeName"); + ValidateAndSetProperty(valueDictionary, val => UrlName = val, "urlName"); + ValidateAndSetProperty(valueDictionary, val => DocumentTypeAlias = val, "nodeTypeAlias", "__NodeTypeAlias"); + ValidateAndSetProperty(valueDictionary, val => DocumentTypeId = int.Parse(val), "nodeType"); + ValidateAndSetProperty(valueDictionary, val => WriterName = val, "writerName"); + ValidateAndSetProperty(valueDictionary, val => CreatorName = val, "creatorName"); + ValidateAndSetProperty(valueDictionary, val => WriterId = int.Parse(val), "writerID"); + ValidateAndSetProperty(valueDictionary, val => CreatorId = int.Parse(val), "creatorID"); + ValidateAndSetProperty(valueDictionary, val => Path = val, "path", "__Path"); + ValidateAndSetProperty(valueDictionary, val => CreateDate = DateTime.Parse(val), "createDate"); + ValidateAndSetProperty(valueDictionary, val => Level = int.Parse(val), "level"); + ValidateAndSetProperty(valueDictionary, val => + { + int pId; + ParentId = null; + if (int.TryParse(val, out pId)) + { + ParentId = pId; + } + }, "parentID"); + + Properties = new Collection(); + + //loop through remaining values that haven't been applied + foreach (var i in valueDictionary.Where(x => !_keysAdded.Contains(x.Key))) + { + //this is taken from examine + Properties.Add(i.Key.InvariantStartsWith("__") + ? new PropertyResult(i.Key, i.Value, Guid.Empty, PropertyResultType.CustomProperty) + : new PropertyResult(i.Key, i.Value, Guid.Empty, PropertyResultType.UserProperty)); + } + } + + private readonly Func _getParent; + public IDocument Parent + { + get { return _getParent(this); } + } + + public int? ParentId { get; private set; } + public int Id { get; private set; } + public int TemplateId { get; private set; } + public int SortOrder { get; private set; } + public string Name { get; private set; } + public string UrlName { get; private set; } + public string DocumentTypeAlias { get; private set; } + public int DocumentTypeId { get; private set; } + public string WriterName { get; private set; } + public string CreatorName { get; private set; } + public int WriterId { get; private set; } + public int CreatorId { get; private set; } + public string Path { get; private set; } + public DateTime CreateDate { get; private set; } + public DateTime UpdateDate { get; private set; } + public Guid Version { get; private set; } + public int Level { get; private set; } + public Collection Properties { get; private set; } + public IEnumerable Children { get; private set; } + + private readonly List _keysAdded = new List(); + private void ValidateAndSetProperty(IDictionary valueDictionary, Action setProperty, params string[] potentialKeys) + { + foreach (var s in potentialKeys) + { + if (valueDictionary[s] == null) + throw new FormatException("The valueDictionary is not formatted correctly and is missing the '" + s + "' element"); + setProperty(valueDictionary[s]); + _keysAdded.Add(s); + break; + } + + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/IPublishedContentStore.cs b/src/Umbraco.Web/IPublishedContentStore.cs index 3b89537340..22ee23ba7a 100644 --- a/src/Umbraco.Web/IPublishedContentStore.cs +++ b/src/Umbraco.Web/IPublishedContentStore.cs @@ -2,11 +2,12 @@ using Umbraco.Core.Models; namespace Umbraco.Web { - internal interface IPublishedContentStore - { - IDocument GetDocumentById(UmbracoContext umbracoContext, int nodeId); + /// + /// Defines the methods to access published content + /// + internal interface IPublishedContentStore : IPublishedMediaStore + { IDocument GetDocumentByRoute(UmbracoContext umbracoContext, string route, bool? hideTopLevelNode = null); IDocument GetDocumentByUrlAlias(UmbracoContext umbracoContext, int rootNodeId, string alias); - string GetDocumentProperty(UmbracoContext umbracoContext, IDocument node, string propertyAlias); } } \ No newline at end of file diff --git a/src/Umbraco.Web/IPublishedMediaStore.cs b/src/Umbraco.Web/IPublishedMediaStore.cs new file mode 100644 index 0000000000..78efe3488f --- /dev/null +++ b/src/Umbraco.Web/IPublishedMediaStore.cs @@ -0,0 +1,13 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Web +{ + /// + /// Defines the methods to access published media + /// + internal interface IPublishedMediaStore + { + IDocument GetDocumentById(UmbracoContext umbracoContext, int nodeId); + string GetDocumentProperty(UmbracoContext umbracoContext, IDocument node, string propertyAlias); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/XmlDocument.cs b/src/Umbraco.Web/Models/XmlDocument.cs index cf782d642a..f51e8dc973 100644 --- a/src/Umbraco.Web/Models/XmlDocument.cs +++ b/src/Umbraco.Web/Models/XmlDocument.cs @@ -236,11 +236,6 @@ namespace Umbraco.Web.Models } } - //public string NiceUrl - //{ - // get { return _niceUrlProvider.GetNiceUrl(Id); } - //} - public int Level { get @@ -260,12 +255,7 @@ namespace Umbraco.Web.Models return _properties; } } - - - public IDocumentProperty GetProperty(string alias) - { - return Properties.FirstOrDefault(p => p.Alias.InvariantEquals(alias)); - } + private void InitializeStructure() { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5f723910fe..9a23411e87 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -241,6 +241,7 @@ + @@ -249,6 +250,7 @@ + @@ -271,7 +273,7 @@ - + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index f045af6a89..1887d0151c 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -133,7 +133,7 @@ namespace Umbraco.Web PropertyEditorValueConvertersResolver.Current.RemoveType(); PropertyEditorValueConvertersResolver.Current.AddType(); - PublishedContentStoreResolver.Current = new PublishedContentStoreResolver(new XmlPublishedContentStore()); + PublishedContentStoreResolver.Current = new PublishedContentStoreResolver(new DefaultPublishedContentStore()); FilteredControllerFactoriesResolver.Current = new FilteredControllerFactoriesResolver( //add all known factories, devs can then modify this list on application startup either by binding to events