using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Xml.XPath; using Examine; using Examine.Providers; using Lucene.Net.Documents; using Umbraco.Core; using Umbraco.Core.Dynamics; using Umbraco.Core.Models; using umbraco; using umbraco.cms.businesslogic; namespace Umbraco.Web { /// /// An IPublishedMediaStore that first checks for the media in Examine, and then reverts to the database /// /// /// 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. /// internal class DefaultPublishedMediaStore : IPublishedMediaStore { public DefaultPublishedMediaStore() { } /// /// Generally used for unit testing to use an explicit examine searcher /// /// internal DefaultPublishedMediaStore(BaseSearchProvider searchProvider) { _searchProvider = searchProvider; } private readonly BaseSearchProvider _searchProvider; public virtual IPublishedContent GetDocumentById(UmbracoContext umbracoContext, int nodeId) { return GetUmbracoMedia(nodeId); } public IEnumerable GetRootDocuments(UmbracoContext umbracoContext) { var rootMedia = global::umbraco.cms.businesslogic.media.Media.GetRootMedias(); var result = new List(); //TODO: need to get a ConvertFromMedia method but we'll just use this for now. foreach (var media in rootMedia .Select(m => global::umbraco.library.GetMedia(m.Id, true)) .Where(media => media != null && media.Current != null)) { media.MoveNext(); result.Add(ConvertFromXPathNavigator(media.Current)); } return result; } private ExamineManager GetExamineManagerSafe() { try { return ExamineManager.Instance; } catch (TypeInitializationException) { return null; } } private BaseSearchProvider GetSearchProviderSafe() { if (_searchProvider != null) return _searchProvider; var eMgr = GetExamineManagerSafe(); if (eMgr != null) { try { //by default use the InternalSearcher return eMgr.SearchProviderCollection["InternalSearcher"]; } 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! } } return null; } private IPublishedContent GetUmbracoMedia(int id) { var searchProvider = GetSearchProviderSafe(); if (searchProvider != null) { try { //first check in Examine as this is WAY faster var criteria = searchProvider.CreateSearchCriteria("media"); var filter = criteria.Id(id); var results = searchProvider.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(); var moved = media.Current.MoveToFirstChild(); //first check if we have an error if (moved) { if (media.Current.Name.InvariantEquals("error")) { return null; } } if (moved) { //move back to the parent and return media.Current.MoveToParent(); } return ConvertFromXPathNavigator(media.Current); } return null; } internal IPublishedContent ConvertFromSearchResult(SearchResult searchResult) { //TODO: Some fields will not be included, that just the way it is unfortunatley until this is fixed: // http://examine.codeplex.com/workitem/10350 var values = new Dictionary(searchResult.Fields); //we need to ensure some fields exist, because of the above issue if (!new []{"template", "templateId"}.Any(values.ContainsKey)) values.Add("template", 0.ToString()); if (!new[] { "sortOrder" }.Any(values.ContainsKey)) values.Add("sortOrder", 0.ToString()); if (!new[] { "urlName" }.Any(values.ContainsKey)) values.Add("urlName", ""); if (!new[] { "nodeType" }.Any(values.ContainsKey)) values.Add("nodeType", 0.ToString()); if (!new[] { "creatorName" }.Any(values.ContainsKey)) values.Add("creatorName", ""); if (!new[] { "writerID" }.Any(values.ContainsKey)) values.Add("writerID", 0.ToString()); if (!new[] { "creatorID" }.Any(values.ContainsKey)) values.Add("creatorID", 0.ToString()); if (!new[] { "createDate" }.Any(values.ContainsKey)) values.Add("createDate", default(DateTime).ToString("yyyy-MM-dd HH:mm:ss")); if (!new[] { "level" }.Any(values.ContainsKey)) { values.Add("level", values["__Path"].Split(',').Length.ToString()); } return new DictionaryPublishedContent(values, d => d.ParentId != -1 //parent should be null if -1 ? GetUmbracoMedia(d.ParentId) : null, //callback to return the children of the current node d => GetChildrenMedia(d.Id), GetProperty, true); } internal IPublishedContent ConvertFromXPathNavigator(XPathNavigator xpath) { if (xpath == null) throw new ArgumentNullException("xpath"); var values = new Dictionary {{"nodeName", xpath.GetAttribute("nodeName", "")}}; if (!UmbracoSettings.UseLegacyXmlSchema) { values.Add("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)) { values.Add(result.Current.Name, result.Current.Value); } while (result.Current.MoveToNextAttribute()) { if (!values.ContainsKey(result.Current.Name)) { 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 DictionaryPublishedContent(values, d => d.ParentId != -1 //parent should be null if -1 ? GetUmbracoMedia(d.ParentId) : null, //callback to return the children of the current node based on the xml structure already found d => GetChildrenMedia(d.Id, xpath), GetProperty, false); } /// /// 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. /// /// /// /// private IPublishedContentProperty GetProperty(DictionaryPublishedContent dd, string alias) { if (dd.LoadedFromExamine) { //if this is from Examine, lets check if the alias does not exist on the document if (dd.Properties.All(x => x.Alias != alias)) { //ok it doesn't exist, we might assume now that Examine didn't index this property because the index is not set up correctly //so before we go loading this from the database, we can check if the alias exists on the content type at all, this information //is cached so will be quicker to look up. if (dd.Properties.Any(x => x.Alias == "__NodeTypeAlias")) { var aliasesAndNames = ContentType.GetAliasesAndNames(dd.Properties.First(x => x.Alias.InvariantEquals("__NodeTypeAlias")).Value.ToString()); if (aliasesAndNames != null) { if (!aliasesAndNames.ContainsKey(alias)) { //Ok, now we know it doesn't exist on this content type anyways return null; } } } //if we've made it here, that means it does exist on the content type but not in examine, we'll need to query the db :( var media = global::umbraco.library.GetMedia(dd.Id, true); if (media != null && media.Current != null) { media.MoveNext(); var mediaDoc = ConvertFromXPathNavigator(media.Current); return mediaDoc.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } } } return dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } /// /// A Helper methods to return the children for media whther it is based on examine or xml /// /// /// /// private IEnumerable GetChildrenMedia(int parentId, XPathNavigator xpath = null) { //if there is no navigator, try examine first, then re-look it up if (xpath == null) { var searchProvider = GetSearchProviderSafe(); if (searchProvider != null) { try { //first check in Examine as this is WAY faster var criteria = searchProvider.CreateSearchCriteria("media"); var filter = criteria.ParentId(parentId); var results = searchProvider.Search(filter.Compile()); if (results.Any()) { return results.Select(ConvertFromSearchResult); } } 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 } } var media = library.GetMedia(parentId, true); if (media != null && media.Current != null) { xpath = media.Current; } else { return null; } } //The xpath might be the whole xpath including the current ones ancestors so we need to select the current node var item = xpath.Select("//*[@id='" + parentId + "']"); if (item.Current == null) { return null; } var children = item.Current.SelectChildren(XPathNodeType.Element); var mediaList = new List(); foreach(XPathNavigator x in children) { //NOTE: I'm not sure why this is here, it is from legacy code of ExamineBackedMedia, but // will leave it here as it must have done something! if (x.Name != "contents") { //make sure it's actually a node, not a property if (!string.IsNullOrEmpty(x.GetAttribute("path", "")) && !string.IsNullOrEmpty(x.GetAttribute("id", ""))) { mediaList.Add(ConvertFromXPathNavigator(x)); } } } return mediaList; } /// /// An IPublishedContent 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 IPublishedContent exist in the dictionary by specific aliases. /// internal class DictionaryPublishedContent : IPublishedContent { public DictionaryPublishedContent( IDictionary valueDictionary, Func getParent, Func> getChildren, Func getProperty, bool fromExamine) { if (valueDictionary == null) throw new ArgumentNullException("valueDictionary"); if (getParent == null) throw new ArgumentNullException("getParent"); if (getProperty == null) throw new ArgumentNullException("getProperty"); _getParent = getParent; _getChildren = getChildren; _getProperty = getProperty; LoadedFromExamine = fromExamine; 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", "writerName"); //this is a bit of a hack fix for: U4-1132 ValidateAndSetProperty(valueDictionary, val => WriterId = int.Parse(val), "writerID"); ValidateAndSetProperty(valueDictionary, val => CreatorId = int.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 = int.Parse(val), "level"); ValidateAndSetProperty(valueDictionary, val => { int pId; ParentId = -1; 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 DateTime ParseDateTimeValue(string val) { if (LoadedFromExamine) { try { //we might need to parse the date time using Lucene converters return DateTools.StringToDate(val); } catch (FormatException) { //swallow exception, its not formatted correctly so revert to just trying to parse } } return DateTime.Parse(val); } /// /// Flag to get/set if this was laoded from examine cache /// internal bool LoadedFromExamine { get; private set; } private readonly Func _getParent; private readonly Func> _getChildren; private readonly Func _getProperty; public IPublishedContent 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 { return _getChildren(this); } } public IPublishedContentProperty GetProperty(string alias) { return _getProperty(this, alias); } private readonly List _keysAdded = new List(); private void ValidateAndSetProperty(IDictionary valueDictionary, Action 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); } } } }