Files
Umbraco-CMS/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs

989 lines
44 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
2015-06-23 13:57:00 +02:00
using System.Configuration;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml.XPath;
using Examine;
using Examine.LuceneEngine.SearchCriteria;
using Examine.Providers;
using Lucene.Net.Documents;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Dynamics;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
2013-09-05 17:47:13 +02:00
using Umbraco.Core.Models.PublishedContent;
2013-02-05 06:31:13 -01:00
using Umbraco.Core.Xml;
using Umbraco.Web.Models;
using UmbracoExamine;
using umbraco;
2015-06-18 15:36:04 +02:00
using Umbraco.Core.Cache;
using Umbraco.Core.Sync;
using Umbraco.Web.Cache;
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>
2013-09-05 17:47:13 +02:00
internal class PublishedMediaCache : IPublishedMediaCache
{
public PublishedMediaCache(ApplicationContext applicationContext)
{
if (applicationContext == null) throw new ArgumentNullException("applicationContext");
_applicationContext = applicationContext;
}
/// <summary>
/// Generally used for unit testing to use an explicit examine searcher
/// </summary>
/// <param name="applicationContext"></param>
/// <param name="searchProvider"></param>
/// <param name="indexProvider"></param>
internal PublishedMediaCache(ApplicationContext applicationContext, BaseSearchProvider searchProvider, BaseIndexProvider indexProvider)
{
if (applicationContext == null) throw new ArgumentNullException("applicationContext");
if (searchProvider == null) throw new ArgumentNullException("searchProvider");
if (indexProvider == null) throw new ArgumentNullException("indexProvider");
_applicationContext = applicationContext;
_searchProvider = searchProvider;
_indexProvider = indexProvider;
}
static PublishedMediaCache()
{
InitializeCacheConfig();
}
2015-06-23 13:57:00 +02:00
private readonly ApplicationContext _applicationContext;
private readonly BaseSearchProvider _searchProvider;
private readonly BaseIndexProvider _indexProvider;
public virtual IPublishedContent GetById(UmbracoContext umbracoContext, bool preview, int nodeId)
{
return GetUmbracoMedia(nodeId);
}
public virtual IEnumerable<IPublishedContent> GetAtRoot(UmbracoContext umbracoContext, bool preview)
{
//TODO: We should be able to look these ids first in Examine!
var rootMedia = _applicationContext.Services.MediaService.GetRootMedia();
return rootMedia.Select(m => GetUmbracoMedia(m.Id));
}
public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars)
2013-02-05 06:31:13 -01:00
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
2013-02-05 06:31:13 -01:00
}
public virtual IPublishedContent GetSingleByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
}
public virtual IEnumerable<IPublishedContent> GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
}
public virtual IEnumerable<IPublishedContent> GetByXPath(UmbracoContext umbracoContext, bool preview, XPathExpression xpath, XPathVariable[] vars)
2013-02-05 06:31:13 -01:00
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
2013-02-05 06:31:13 -01:00
}
public virtual XPathNavigator GetXPathNavigator(UmbracoContext umbracoContext, bool preview)
{
throw new NotImplementedException("PublishedMediaCache does not support XPath.");
}
public bool XPathNavigatorIsNavigable { get { return false; } }
public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); }
2013-02-05 06:31:13 -01:00
private ExamineManager GetExamineManagerSafe()
{
try
{
return ExamineManager.Instance;
}
catch (TypeInitializationException)
{
return null;
}
}
private BaseIndexProvider GetIndexProviderSafe()
{
if (_indexProvider != null)
return _indexProvider;
var eMgr = GetExamineManagerSafe();
if (eMgr != null)
{
try
{
//by default use the InternalSearcher
var indexer = eMgr.IndexProviderCollection[Constants.Examine.InternalIndexer];
if (indexer.IndexerData.IncludeNodeTypes.Any() || indexer.IndexerData.ExcludeNodeTypes.Any())
{
LogHelper.Warn<PublishedMediaCache>("The InternalIndexer for examine is configured incorrectly, it should not list any include/exclude node types or field names, it should simply be configured as: " + "<IndexSet SetName=\"InternalIndexSet\" IndexPath=\"~/App_Data/TEMP/ExamineIndexes/Internal/\" />");
}
return indexer;
}
catch (Exception ex)
{
2014-03-25 20:44:47 -07:00
LogHelper.Error<PublishedMediaCache>("Could not retrieve the InternalIndexer", ex);
//something didn't work, continue returning null.
}
}
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[Constants.Examine.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!
}
catch (NullReferenceException)
{
//This will occur when the search provider cannot be initialized. In newer examine versions the initialization is lazy and therefore
2015-01-07 15:36:17 +11:00
// 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.
}
}
return null;
}
private IPublishedContent GetUmbracoMedia(int id)
{
2015-06-18 15:36:04 +02:00
// 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);
2015-06-18 15:36:04 +02:00
return cacheValues == null ? null : CreateFromCacheValues(cacheValues);
}
2015-06-18 15:36:04 +02:00
private CacheValues GetUmbracoMediaCacheValues(int id)
{
var searchProvider = GetSearchProviderSafe();
2016-08-16 14:29:51 +02:00
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.CreateSearchCriteria("media");
var filter = criteria.Id(id).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard());
var result = searchProvider.Search(filter.Compile()).FirstOrDefault();
if (result != null) return ConvertFromSearchResult(result);
}
catch (FileNotFoundException ex)
{
//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!
LogHelper.Error<PublishedMediaCache>("Could not load data from Examine index for media", ex);
}
}
// 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
2016-06-23 13:33:54 +02:00
var media = ApplicationContext.Current.Services.MediaService.GetById(id);
if (media == null) 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)
LogHelper.Warn<PublishedMediaCache>("Failed ({0} 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);
2016-06-23 13:33:54 +02:00
}
private const int ExamineIndexMissMax = 10;
private int _examineIndexMiss;
2015-06-18 15:36:04 +02:00
internal CacheValues ConvertFromXPathNodeIterator(XPathNodeIterator media, int id)
{
if (media != null && media.Current != null)
{
return media.Current.Name.InvariantEquals("error")
? null
: ConvertFromXPathNavigator(media.Current);
}
LogHelper.Warn<PublishedMediaCache>(
"Could not retrieve media {0} from Examine index or from legacy library.GetMedia method",
() => id);
return null;
}
internal CacheValues ConvertFromSearchResult(SearchResult searchResult)
{
//NOTE: Some fields will not be included if the config section for the internal index has been
//mucked around with. It should index everything and so the index definition should simply be:
// <IndexSet SetName="InternalIndexSet" IndexPath="~/App_Data/TEMP/ExamineIndexes/Internal/" />
var values = new Dictionary<string, string>(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());
}
// because, migration
if (values.ContainsKey("key") == false)
values["key"] = Guid.Empty.ToString();
return new CacheValues
{
Values = values,
2015-06-18 15:36:04 +02:00
FromExamine = true
};
2015-06-18 15:36:04 +02:00
//var content = 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);
//return content.CreateModel();
}
internal CacheValues ConvertFromXPathNavigator(XPathNavigator xpath, bool forceNav = false)
{
if (xpath == null) throw new ArgumentNullException("xpath");
var values = new Dictionary<string, string> { { "nodeName", xpath.GetAttribute("nodeName", "") } };
if (!UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema)
{
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))
{
values[result.Current.Name] = result.Current.Value;
}
while (result.Current.MoveToNextAttribute())
{
if (!values.ContainsKey(result.Current.Name))
{
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)
{
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[result.Current.Name] = value;
}
}
return new CacheValues
{
2015-06-18 15:36:04 +02:00
Values = values,
XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator!
};
//var content = 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);
//return content.CreateModel();
}
internal CacheValues ConvertFromIMedia(IMedia media)
{
var values = new Dictionary<string, string>();
var creator = _applicationContext.Services.UserService.GetProfileById(media.CreatorId);
2016-06-23 17:58:01 +02:00
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;
2016-06-23 17:58:01 +02:00
values["writerID"] = media.CreatorId.ToString();
values["writerName"] = creatorName;
2016-06-23 13:33:54 +02:00
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;
2016-06-23 13:33:54 +02:00
// add the user props
foreach (var prop in media.Properties)
values[prop.Alias] = prop.Value == null ? null : prop.Value.ToString();
2016-06-23 13:33:54 +02:00
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.PropertyTypeAlias.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.PropertyTypeAlias.InvariantEquals(UmbracoContentIndexer.RawFieldPrefix + alias));
return rawValue
?? dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias));
}
//if its not loaded from examine, then just return the property
return dd.Properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias));
}
/// <summary>
/// A Helper methods to return the children for media whther 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 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).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard());
//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
ISearchResults results;
//we want to check if the indexer for this searcher has "sortOrder" flagged as sortable.
//if so, we'll use Lucene to do the sorting, if not we'll have to manually sort it (slower).
var indexer = GetIndexProviderSafe();
var useLuceneSort = indexer != null && indexer.IndexerData.StandardFields.Any(x => x.Name.InvariantEquals("sortOrder") && x.EnableSorting);
if (useLuceneSort)
{
//we have a sortOrder field declared to be sorted, so we'll use Examine
results = searchProvider.Search(
filter.And().OrderBy(new SortableField("sortOrder", SortType.Int)).Compile());
}
else
{
results = searchProvider.Search(filter.Compile());
}
if (results.Any())
{
2015-06-18 15:36:04 +02:00
// 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 useLuceneSort ? medias : medias.OrderBy(x => x.SortOrder);
}
else
{
//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 descendents 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 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
}
}
//falling back to get media
var media = library.GetMedia(parentId, true);
if (media != null && media.Current != null)
{
xpath = media.Current;
}
else
{
return Enumerable.Empty<IPublishedContent>();
}
}
2015-06-18 15:36:04 +02:00
var mediaList = new List<IPublishedContent>();
// this is so bad, really
var item = xpath.Select("//*[@id='" + parentId + "']");
if (item.Current == null)
return Enumerable.Empty<IPublishedContent>();
2015-06-18 15:36:04 +02:00
var items = item.Current.SelectChildren(XPathNodeType.Element);
// and this does not work, because... meh
//var q = "//* [@id='" + parentId + "']/* [@id]";
2015-06-18 15:36:04 +02:00
//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));
}
2015-06-18 15:36:04 +02:00
////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 Enumerable.Empty<IPublishedContent>();
//}
//var children = item.Current.SelectChildren(XPathNodeType.Element);
//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
2015-06-18 15:36:04 +02:00
// if (!string.IsNullOrEmpty(x.GetAttribute("path", "")) &&
// !string.IsNullOrEmpty(x.GetAttribute("id", "")))
// {
// mediaList.Add(ConvertFromXPathNavigator(x));
// }
// }
2015-06-18 15:36:04 +02:00
//}
return mediaList;
}
/// <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 : PublishedContentWithKeyBase
{
2013-09-05 17:47:13 +02:00
// 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(
IDictionary<string, string> valueDictionary,
Func<int, IPublishedContent> getParent,
Func<int, XPathNavigator, IEnumerable<IPublishedContent>> getChildren,
Func<DictionaryPublishedContent, string, IPublishedProperty> getProperty,
2015-06-18 15:36:04 +02:00
XPathNavigator nav,
bool fromExamine)
{
if (valueDictionary == null) throw new ArgumentNullException("valueDictionary");
if (getParent == null) throw new ArgumentNullException("getParent");
if (getProperty == null) throw new ArgumentNullException("getProperty");
_getParent = new Lazy<IPublishedContent>(() => getParent(ParentId));
_getChildren = new Lazy<IEnumerable<IPublishedContent>>(() => getChildren(Id, nav));
_getProperty = getProperty;
LoadedFromExamine = fromExamine;
ValidateAndSetProperty(valueDictionary, val => _id = int.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int!
ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key");
// wtf are we dealing with templates for medias?!
2015-06-18 15:36:04 +02:00
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", UmbracoContentIndexer.NodeTypeAliasFieldName);
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");
_contentType = PublishedContentType.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.PropertyTypeAlias;
_keysAdded.Add(alias);
string value;
const bool isPreviewing = false; // false :: never preview a media
2016-06-23 13:33:54 +02:00
var property = valueDictionary.TryGetValue(alias, out value) == false || value == null
? new XmlPublishedProperty(propertyType, isPreviewing)
: new XmlPublishedProperty(propertyType, 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
{
2013-09-05 17:47:13 +02:00
if (i.Key.InvariantStartsWith("__"))
{
Merge branch '6.2.0-pubcontent' into 7.0.0-pubcontent Conflicts: src/Umbraco.Core/Cache/CacheProviderBase.cs src/Umbraco.Core/Cache/HttpRuntimeCacheProvider.cs src/Umbraco.Core/Cache/NullCacheProvider.cs src/Umbraco.Core/Cache/StaticCacheProvider.cs src/Umbraco.Core/Configuration/UmbracoSettings.cs src/Umbraco.Core/CoreBootManager.cs src/Umbraco.Core/Dynamics/PropertyResult.cs src/Umbraco.Core/Models/IPublishedContentProperty.cs src/Umbraco.Core/Models/PublishedItemType.cs src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs src/Umbraco.Core/PropertyEditors/PropertyValueConvertersResolver.cs src/Umbraco.Core/PublishedContentExtensions.cs src/Umbraco.Core/PublishedContentHelper.cs src/Umbraco.Core/Umbraco.Core.csproj src/Umbraco.Tests/CodeFirst/StronglyTypedMapperTest.cs src/Umbraco.Tests/LibraryTests.cs src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs src/Umbraco.Tests/PublishedCache/PublishedContentCacheTests.cs src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs src/Umbraco.Web/ExamineExtensions.cs src/Umbraco.Web/Models/DynamicPublishedContent.cs src/Umbraco.Web/Models/XmlPublishedContent.cs src/Umbraco.Web/Models/XmlPublishedContentProperty.cs src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs src/Umbraco.Web/PublishedContentExtensions.cs src/Umbraco.Web/Templates/TemplateUtilities.cs src/Umbraco.Web/Umbraco.Web.csproj src/Umbraco.Web/WebBootManager.cs src/Umbraco.Web/umbraco.presentation/macro.cs src/umbraco.MacroEngines/RazorDynamicNode/PropertyResult.cs src/umbraco.MacroEngines/RazorDynamicNode/PublishedContentExtensions.cs
2013-09-23 21:57:22 +02:00
// no type for that one, dunno how to convert
IPublishedProperty property = new PropertyResult(i.Key, i.Value, PropertyResultType.CustomProperty);
_properties.Add(property);
}
2013-09-05 17:47:13 +02:00
else
{
// this is a property that does not correspond to anything, ignore and log
LogHelper.Warn<PublishedMediaCache>("Dropping property \"" + i.Key + "\" because it does not belong to the content type.");
2013-09-05 17:47:13 +02:00
}
}
}
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);
}
/// <summary>
/// Flag to get/set if this was laoded from examine cache
/// </summary>
internal bool LoadedFromExamine { get; private set; }
//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;
/// <summary>
/// Returns 'Media' as the item type
/// </summary>
public override PublishedItemType ItemType
{
get { return PublishedItemType.Media; }
}
public override IPublishedContent Parent
{
get { return _getParent.Value; }
}
public int ParentId { get; private set; }
public override int Id
{
get { return _id; }
}
public override Guid Key { get { return _key; } }
public override int TemplateId
{
get
{
//TODO: should probably throw a not supported exception since media doesn't actually support this.
return _templateId;
}
}
public override int SortOrder
{
get { return _sortOrder; }
}
public override string Name
{
get { return _name; }
}
public override string UrlName
{
get { return _urlName; }
}
public override string DocumentTypeAlias
{
get { return _documentTypeAlias; }
}
public override int DocumentTypeId
{
get { return _documentTypeId; }
}
public override string WriterName
{
get { return _writerName; }
}
public override string CreatorName
{
get { return _creatorName; }
}
public override int WriterId
{
get { return _writerId; }
}
public override int CreatorId
{
get { return _creatorId; }
}
public override string Path
{
get { return _path; }
}
public override DateTime CreateDate
{
get { return _createDate; }
}
public override DateTime UpdateDate
{
get { return _updateDate; }
}
public override Guid Version
{
get { return _version; }
}
public override int Level
{
get { return _level; }
}
2013-09-05 17:47:13 +02:00
public override bool IsDraft
{
get { return false; }
}
public override ICollection<IPublishedProperty> Properties
{
get { return _properties; }
}
public override IEnumerable<IPublishedContent> Children
{
get { return _getChildren.Value; }
}
public override IPublishedProperty GetProperty(string alias)
{
return _getProperty(this, alias);
}
2013-09-05 17:47:13 +02:00
public override PublishedContentType ContentType
{
get { return _contentType; }
}
// override to implement cache
// cache at context level, ie once for the whole request
// but cache is not shared by requests because we wouldn't know how to clear it
public override IPublishedProperty GetProperty(string alias, bool recurse)
{
if (recurse == false) return GetProperty(alias);
IPublishedProperty property;
string key = null;
var cache = UmbracoContextCache.Current;
2013-09-05 17:47:13 +02:00
if (cache != null)
{
key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant());
object o;
if (cache.TryGetValue(key, out o))
{
property = o as IPublishedProperty;
if (property == null)
throw new InvalidOperationException("Corrupted cache.");
return property;
}
}
// else get it for real, no cache
property = base.GetProperty(alias, true);
if (cache != null)
cache[key] = property;
return property;
}
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(IDictionary<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);
}
2013-09-05 17:47:13 +02:00
}
2015-06-18 15:36:04 +02:00
// 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
{
2015-06-18 15:36:04 +02:00
public IDictionary<string, string> Values { get; set; }
public XPathNavigator XPath { get; set; }
public bool FromExamine { get; set; }
}
2015-06-18 15:36:04 +02:00
public const string PublishedMediaCacheKey = "MediaCacheMeh.";
private const int PublishedMediaCacheTimespanSeconds = 4 * 60; // 4 mins
2015-06-23 13:57:00 +02:00
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;
}
}
2015-06-18 15:36:04 +02:00
internal IPublishedContent CreateFromCacheValues(CacheValues cacheValues)
{
var content = new DictionaryPublishedContent(
cacheValues.Values,
parentId => parentId < 0 ? null : GetUmbracoMedia(parentId),
GetChildrenMedia,
GetProperty,
cacheValues.XPath, // though, outside of tests, that should be null
cacheValues.FromExamine
);
return content.CreateModel();
}
private static CacheValues GetCacheValues(int id, Func<int, CacheValues> func)
{
if (_publishedMediaCacheEnabled == false)
return func(id);
2015-06-23 13:57:00 +02:00
var cache = ApplicationContext.Current.ApplicationCache.RuntimeCache;
2015-06-18 15:36:04 +02:00
var key = PublishedMediaCacheKey + id;
return (CacheValues)cache.GetCacheItem(key, () => func(id), _publishedMediaCacheTimespan);
}
2015-06-18 15:36:04 +02:00
internal static void ClearCache(int id)
{
2015-06-18 15:36:04 +02:00
var cache = ApplicationContext.Current.ApplicationCache.RuntimeCache;
var sid = id.ToString();
2015-06-18 15:36:04 +02:00
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 ;-(
2015-06-18 15:36:04 +02:00
// clear the parent - NOT (why?)
//var exist = (CacheValues) cache.GetCacheItem(key);
//if (exist != null)
// cache.ClearCacheItem(PublishedMediaCacheKey + GetValuesValue(exist.Values, "parentID"));
2015-06-18 15:36:04 +02:00
// clear the item
cache.ClearCacheItem(key);
2015-06-18 15:36:04 +02:00
// clear all children - in case we moved and their path has changed
var fid = "/" + sid + "/";
2015-06-18 15:36:04 +02:00
cache.ClearCacheObjectTypes<CacheValues>((k, v) =>
GetValuesValue(v.Values, "path", "__Path").Contains(fid));
}
private static string GetValuesValue(IDictionary<string, string> d, params string[] keys)
{
string value = null;
var ignored = keys.Any(x => d.TryGetValue(x, out value));
2015-06-18 15:36:04 +02:00
return value ?? "";
}
2015-06-18 15:36:04 +02:00
}
2014-03-25 20:44:47 -07:00
}