using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.ModelsBuilder.Embedded.Building; namespace Umbraco.ModelsBuilder.Embedded { public sealed class UmbracoServices { private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; private readonly IShortStringHelper _shortStringHelper; /// /// Initializes a new instance of the class. /// public UmbracoServices( IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory, IShortStringHelper shortStringHelper) { _contentTypeService = contentTypeService; _mediaTypeService = mediaTypeService; _memberTypeService = memberTypeService; _publishedContentTypeFactory = publishedContentTypeFactory; _shortStringHelper = shortStringHelper; } #region Services public IList GetAllTypes() { var types = new List(); // TODO: this will require 3 rather large SQL queries on startup in PureLive. I know that these will be cached after lookup but it will slow // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup // in more than one place. types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); return EnsureDistinctAliases(types); } public IList GetContentTypes() { var contentTypes = _contentTypeService.GetAll().Cast().ToArray(); return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here } public IList GetMediaTypes() { var contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here } public IList GetMemberTypes() { var memberTypes = _memberTypeService.GetAll().Cast().ToArray(); return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here } public static string GetClrName(IShortStringHelper shortStringHelper, string name, string alias) { // ModelsBuilder's legacy - but not ideal return alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); } private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) { var typeModels = new List(); var uniqueTypes = new HashSet(); // get the types and the properties foreach (var contentType in contentTypes) { var typeModel = new TypeModel { Id = contentType.Id, Alias = contentType.Alias, ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), ParentId = contentType.ParentId, Name = contentType.Name, Description = contentType.Description }; // of course this should never happen, but when it happens, better detect it // else we end up with weird nullrefs everywhere if (uniqueTypes.Contains(typeModel.ClrName)) throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); uniqueTypes.Add(typeModel.ClrName); var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); switch (itemType) { case PublishedItemType.Content: typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element ? TypeModel.ItemTypes.Element : TypeModel.ItemTypes.Content; break; case PublishedItemType.Media: typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element ? TypeModel.ItemTypes.Element : TypeModel.ItemTypes.Media; break; case PublishedItemType.Member: typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element ? TypeModel.ItemTypes.Element : TypeModel.ItemTypes.Member; break; default: throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); } typeModels.Add(typeModel); foreach (var propertyType in contentType.PropertyTypes) { var propertyModel = new PropertyModel { Alias = propertyType.Alias, ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), Name = propertyType.Name, Description = propertyType.Description }; var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); if (publishedPropertyType == null) throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); propertyModel.ModelClrType = publishedPropertyType.ModelClrType; typeModel.Properties.Add(propertyModel); } } // wire the base types foreach (var typeModel in typeModels.Where(x => x.ParentId > 0)) { typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might // not be a content type - here we assume that BaseType being null while ParentId > 0 means that // the parent is a container (and we don't check). typeModel.IsParent = typeModel.BaseType != null; } // discover mixins foreach (var contentType in contentTypes) { var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); if (typeModel == null) throw new PanicException("Panic: no type model matching content type."); IEnumerable compositionTypes; var contentTypeAsMedia = contentType as IMediaType; var contentTypeAsContent = contentType as IContentType; var contentTypeAsMember = contentType as IMemberType; if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); foreach (var compositionType in compositionTypes) { var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); if (compositionModel == null) throw new PanicException("Panic: composition type does not exist."); if (compositionType.Id == contentType.ParentId) continue; // add to mixins typeModel.MixinTypes.Add(compositionModel); // mark as mixin - as well as parents compositionModel.IsMixin = true; while ((compositionModel = compositionModel.BaseType) != null) compositionModel.IsMixin = true; } } return typeModels; } internal static IList EnsureDistinctAliases(IList typeModels) { var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); foreach (var group in groups.Where(x => x.Count() > 1)) throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + " One of the aliases must be modified in order to use the ModelsBuilder."); return typeModels; } #endregion } }