using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Composing; using Umbraco.Web.Models; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Web.PublishedCache.NuCache { internal class PublishedContent : PublishedContentBase { private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly ContentNode _contentNode; private readonly string _urlSegment; #region Constructors public PublishedContent( ContentNode contentNode, ContentData contentData, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, IUmbracoContextAccessor umbracoContextAccessor) : base(umbracoContextAccessor) { _contentNode = contentNode ?? throw new ArgumentNullException(nameof(contentNode)); ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _urlSegment = ContentData.UrlSegment; IsPreviewing = ContentData.Published == false; var properties = new List(); foreach (var propertyType in _contentNode.ContentType.PropertyTypes) { // add one property per property type - this is required, for the indexing to work // if contentData supplies pdatas, use them, else use null contentData.Properties.TryGetValue(propertyType.Alias, out var pdatas); // else will be null properties.Add(new Property(propertyType, this, pdatas, _publishedSnapshotAccessor)); } PropertiesArray = properties.ToArray(); } private string GetProfileNameById(int id) { var cache = GetCurrentSnapshotCache(); return cache == null ? GetProfileNameByIdNoCache(id) : (string)cache.Get(CacheKeys.ProfileName(id), () => GetProfileNameByIdNoCache(id)); } private static string GetProfileNameByIdNoCache(int id) { #if DEBUG var userService = Current.Services?.UserService; if (userService == null) return "[null]"; // for tests #else // we don't want each published content to hold a reference to the service // so where should they get the service from really? from the locator... var userService = Current.Services.UserService; #endif var user = userService.GetProfileById(id); return user?.Name; } // (see ContentNode.CloneParent) public PublishedContent( ContentNode contentNode, PublishedContent origin, IUmbracoContextAccessor umbracoContextAccessor) : base(umbracoContextAccessor) { _contentNode = contentNode; _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; VariationContextAccessor = origin.VariationContextAccessor; ContentData = origin.ContentData; _urlSegment = origin._urlSegment; IsPreviewing = origin.IsPreviewing; // here is the main benefit: we do not re-create properties so if anything // is cached locally, we share the cache - which is fine - if anything depends // on the tree structure, it should not be cached locally to begin with PropertiesArray = origin.PropertiesArray; } // clone for previewing as draft a published content that is published and has no draft private PublishedContent( PublishedContent origin, IUmbracoContextAccessor umbracoContextAccessor) : base(umbracoContextAccessor) { _publishedSnapshotAccessor = origin._publishedSnapshotAccessor; VariationContextAccessor = origin.VariationContextAccessor; _contentNode = origin._contentNode; ContentData = origin.ContentData; _urlSegment = origin._urlSegment; IsPreviewing = true; // clone properties so _isPreviewing is true PropertiesArray = origin.PropertiesArray.Select(x => (IPublishedProperty)new Property((Property)x, this)).ToArray(); } #endregion #region Get Content/Media for Parent/Children // this is for tests purposes // args are: current published snapshot (may be null), previewing, content id - returns: content internal static Func GetContentByIdFunc { get; set; } = (publishedShapshot, previewing, id) => publishedShapshot.Content.GetById(previewing, id); internal static Func GetMediaByIdFunc { get; set; } = (publishedShapshot, previewing, id) => publishedShapshot.Media.GetById(previewing, id); private IPublishedContent GetContentById(bool previewing, int id) { return GetContentByIdFunc(_publishedSnapshotAccessor.PublishedSnapshot, previewing, id); } private IEnumerable GetContentByIds(bool previewing, IEnumerable ids) { var publishedSnapshot = _publishedSnapshotAccessor.PublishedSnapshot; // beware! the loop below CANNOT be converted to query such as: //return ids.Select(x => _getContentByIdFunc(publishedSnapshot, previewing, x)).Where(x => x != null); // because it would capture the published snapshot and cause all sorts of issues // // we WANT to get the actual current published snapshot each time we run // ReSharper disable once LoopCanBeConvertedToQuery foreach (var id in ids) { var content = GetContentByIdFunc(publishedSnapshot, previewing, id); if (content != null) yield return content; } } private IPublishedContent GetMediaById(bool previewing, int id) { return GetMediaByIdFunc(_publishedSnapshotAccessor.PublishedSnapshot, previewing, id); } private IEnumerable GetMediaByIds(bool previewing, IEnumerable ids) { var publishedShapshot = _publishedSnapshotAccessor.PublishedSnapshot; // see note above for content // ReSharper disable once LoopCanBeConvertedToQuery foreach (var id in ids) { var content = GetMediaByIdFunc(publishedShapshot, previewing, id); if (content != null) yield return content; } } #endregion #region Content Type /// public override IPublishedContentType ContentType => _contentNode.ContentType; #endregion #region PublishedElement /// public override Guid Key => _contentNode.Uid; #endregion #region PublishedContent internal ContentData ContentData { get; } /// public override int Id => _contentNode.Id; /// public override string Name(string culture = null) { // invariant has invariant value (whatever the requested culture) if (!ContentType.VariesByCulture()) return ContentData.Name; // handle context culture for variant if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; // get return culture != "" && Cultures.TryGetValue(culture, out var infos) ? infos.Name : null; } /// public override string UrlSegment(string culture = null) { // invariant has invariant value (whatever the requested culture) if (!ContentType.VariesByCulture()) return _urlSegment; // handle context culture fpr variant if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; // get return culture != "" && Cultures.TryGetValue(culture, out var infos) ? infos.UrlSegment : null; } /// public override int SortOrder => _contentNode.SortOrder; /// public override int Level => _contentNode.Level; /// public override string Path => _contentNode.Path; /// public override int? TemplateId => ContentData.TemplateId; /// public override int CreatorId => _contentNode.CreatorId; /// public override string CreatorName => GetProfileNameById(_contentNode.CreatorId); /// public override DateTime CreateDate => _contentNode.CreateDate; /// public override int WriterId => ContentData.WriterId; /// public override string WriterName => GetProfileNameById(ContentData.WriterId); /// public override DateTime UpdateDate => ContentData.VersionDate; private IReadOnlyDictionary _cultureInfos; private static readonly IReadOnlyDictionary NoCultureInfos = new Dictionary(); /// public override PublishedCultureInfo GetCulture(string culture = null) { // handle context culture if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; // no invariant culture infos if (culture == "") return null; // get return Cultures.TryGetValue(culture, out var cultureInfos) ? cultureInfos : null; } /// public override IReadOnlyDictionary Cultures { get { if (!ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; if (ContentData.CultureInfos == null) throw new Exception("oops: _contentDate.CultureInfos is null."); return _cultureInfos = ContentData.CultureInfos .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.UrlSegment, x.Value.Date), StringComparer.OrdinalIgnoreCase); } } /// public override bool IsDraft(string culture = null) { // if this is the 'published' published content, nothing can be draft if (ContentData.Published) return false; // not the 'published' published content, and does not vary = must be draft if (!ContentType.VariesByCulture()) return true; // handle context culture if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; // not the 'published' published content, and varies // = depends on the culture return ContentData.CultureInfos.TryGetValue(culture, out var cvar) && cvar.IsDraft; } /// public override bool IsPublished(string culture = null) { // whether we are the 'draft' or 'published' content, need to determine whether // there is a 'published' version for the specified culture (or at all, for // invariant content items) // if there is no 'published' published content, no culture can be published var hasPublished = _contentNode.PublishedContent != null; if (!hasPublished) return false; // if there is a 'published' published content, and does not vary = published if (!ContentType.VariesByCulture()) return true; // handle context culture if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; // there is a 'published' published content, and varies // = depends on the culture return _contentNode.PublishedContent.ContentData.CultureInfos.ContainsKey(culture); } #endregion #region Tree /// public override IPublishedContent Parent { get { // have to use the "current" cache because a PublishedContent can be shared // amongst many snapshots and other content depend on the snapshots switch (_contentNode.ContentType.ItemType) { case PublishedItemType.Content: return GetContentById(IsPreviewing, _contentNode.ParentContentId); case PublishedItemType.Media: return GetMediaById(IsPreviewing, _contentNode.ParentContentId); default: throw new Exception($"Panic: unsupported item type \"{_contentNode.ContentType.ItemType}\"."); } } } /// public override IEnumerable Children { get { var cache = GetAppropriateCache(); if (cache == null || PublishedSnapshotService.CachePublishedContentChildren == false) return GetChildren(); // note: ToArray is important here, we want to cache the result, not the function! return (IEnumerable)cache.Get(ChildrenCacheKey, () => GetChildren().ToArray()); } } private string _childrenCacheKey; private string ChildrenCacheKey => _childrenCacheKey ?? (_childrenCacheKey = CacheKeys.PublishedContentChildren(Key, IsPreviewing)); private IEnumerable GetChildren() { IEnumerable c; switch (_contentNode.ContentType.ItemType) { case PublishedItemType.Content: c = GetContentByIds(IsPreviewing, _contentNode.ChildContentIds); break; case PublishedItemType.Media: c = GetMediaByIds(IsPreviewing, _contentNode.ChildContentIds); break; default: throw new Exception("oops"); } return c.OrderBy(x => x.SortOrder); // notes: // _contentNode.ChildContentIds is an unordered int[] // needs to fetch & sort - do it only once, lazily, though // Q: perfs-wise, is it better than having the store managed an ordered list } #endregion #region Properties /// public override IEnumerable Properties => PropertiesArray; /// public override IPublishedProperty GetProperty(string alias) { var index = _contentNode.ContentType.GetPropertyIndex(alias); if (index < 0) return null; // happens when 'alias' does not match a content type property alias if (index >= PropertiesArray.Length) // should never happen - properties array must be in sync with property type throw new IndexOutOfRangeException("Index points outside the properties array, which means the properties array is corrupt."); var property = PropertiesArray[index]; return property; } #endregion #region Caching // beware what you use that one for - you don't want to cache its result private IAppCache GetAppropriateCache() { var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; var cache = publishedSnapshot == null ? null : ((IsPreviewing == false || PublishedSnapshotService.FullCacheWhenPreviewing) && (ContentType.ItemType != PublishedItemType.Member) ? publishedSnapshot.ElementsCache : publishedSnapshot.SnapshotCache); return cache; } private IAppCache GetCurrentSnapshotCache() { var publishedSnapshot = (PublishedSnapshot)_publishedSnapshotAccessor.PublishedSnapshot; return publishedSnapshot?.SnapshotCache; } #endregion #region Internal // used by property internal IVariationContextAccessor VariationContextAccessor { get; } // used by navigable content internal IPublishedProperty[] PropertiesArray { get; } // used by navigable content internal int ParentId => _contentNode.ParentContentId; // used by navigable content // includes all children, published or unpublished // NavigableNavigator takes care of selecting those it wants internal IList ChildIds => _contentNode.ChildContentIds; // used by Property // gets a value indicating whether the content or media exists in // a previewing context or not, ie whether its Parent, Children, and // properties should refer to published, or draft content internal bool IsPreviewing { get; } private string _asPreviewingCacheKey; private string AsPreviewingCacheKey => _asPreviewingCacheKey ?? (_asPreviewingCacheKey = CacheKeys.PublishedContentAsPreviewing(Key)); // used by ContentCache internal IPublishedContent AsDraft() { if (IsPreviewing) return this; var cache = GetAppropriateCache(); if (cache == null) return new PublishedContent(this, UmbracoContextAccessor).CreateModel(); return (IPublishedContent)cache.Get(AsPreviewingCacheKey, () => new PublishedContent(this, UmbracoContextAccessor).CreateModel()); } // used by Navigable.Source,... internal static PublishedContent UnwrapIPublishedContent(IPublishedContent content) { PublishedContentWrapped wrapped; while ((wrapped = content as PublishedContentWrapped) != null) content = wrapped.Unwrap(); var inner = content as PublishedContent; if (inner == null) throw new InvalidOperationException("Innermost content is not PublishedContent."); return inner; } #endregion } }