From 31e018c045851488c8df2bc9c7ab54775b1e9d0a Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 28 Apr 2014 17:28:40 +1000 Subject: [PATCH] Moves XML serialization for content/media/members to a resusable class: EntityXmlSerializer to remove the circular references we have by using the ToXml extension methods from directly in the services. This has been completed for the ContentService, next is Media and Member services. Updates the ContentService to not perform CUD operations for preview or published xml, this is now done at the repository level and now done in a single transaction for the saving and publishing events. Still need to do that for the member and media services too and the remaining methods in the content service --- src/Umbraco.Core/Models/ContentXmlEntity.cs | 53 +++++ src/Umbraco.Core/Models/PropertyExtensions.cs | 38 +-- .../Repositories/ContentRepository.cs | 152 +++++++++++- .../Repositories/ContentXmlRepository.cs | 86 +++++++ .../DataTypeDefinitionRepository.cs | 47 +--- .../Interfaces/IContentRepository.cs | 15 ++ src/Umbraco.Core/Services/ContentService.cs | 111 ++++----- .../Services/EntityXmlSerializer.cs | 218 ++++++++++++++++++ src/Umbraco.Core/Services/PackagingService.cs | 139 +---------- src/Umbraco.Core/Services/ServiceContext.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../Models/DataValueSetterTests.cs | 3 +- .../Services/ContentServiceTests.cs | 43 ++++ 13 files changed, 635 insertions(+), 275 deletions(-) create mode 100644 src/Umbraco.Core/Models/ContentXmlEntity.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs create mode 100644 src/Umbraco.Core/Services/EntityXmlSerializer.cs diff --git a/src/Umbraco.Core/Models/ContentXmlEntity.cs b/src/Umbraco.Core/Models/ContentXmlEntity.cs new file mode 100644 index 0000000000..ffa3bd2ff8 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentXmlEntity.cs @@ -0,0 +1,53 @@ +using System; +using System.Xml.Linq; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models +{ + /// + /// Used in content/media/member repositories in order to add this type of entity to the persisted collection to be saved + /// in a single transaction during saving an entity + /// + internal class ContentXmlEntity : IAggregateRoot + { + private readonly bool _entityExists; + private readonly Func _xml; + + public ContentXmlEntity(bool entityExists, IContentBase content, Func xml) + { + if (content == null) throw new ArgumentNullException("content"); + _entityExists = entityExists; + _xml = xml; + Content = content; + } + + public XElement Xml + { + get { return _xml(); } + } + public IContentBase Content { get; private set; } + + public int Id + { + get { return Content.Id; } + set { throw new NotSupportedException(); } + } + + public Guid Key { get; set; } + public DateTime CreateDate { get; set; } + public DateTime UpdateDate { get; set; } + + public bool HasIdentity + { + get { return _entityExists; } + } + + public object DeepClone() + { + var clone = (ContentXmlEntity)MemberwiseClone(); + //Automatically deep clone ref properties that are IDeepCloneable + DeepCloneHelper.DeepCloneRefProperties(this, clone); + return clone; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyExtensions.cs b/src/Umbraco.Core/Models/PropertyExtensions.cs index ae01532c87..ce4aa31b72 100644 --- a/src/Umbraco.Core/Models/PropertyExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyExtensions.cs @@ -22,42 +22,10 @@ namespace Umbraco.Core.Models /// Xml of the property and its value public static XElement ToXml(this Property property) { - return property.ToXml(ApplicationContext.Current.Services.DataTypeService); + var xmlSerializer = new EntityXmlSerializer(); + return xmlSerializer.Serialize(ApplicationContext.Current.Services.DataTypeService, property); } - internal static XElement ToXml(this Property property, IDataTypeService dataTypeService) - { - var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "data" : property.Alias.ToSafeAlias(); - - var xd = new XmlDocument(); - var xmlNode = xd.CreateNode(XmlNodeType.Element, nodeName, ""); - - //Add the property alias to the legacy schema - if (UmbracoSettings.UseLegacyXmlSchema) - { - var alias = xd.CreateAttribute("alias"); - alias.Value = property.Alias.ToSafeAlias(); - xmlNode.Attributes.Append(alias); - } - - //This seems to fail during testing - //SD: With the new null checks below, this shouldn't fail anymore. - var dt = property.PropertyType.DataType(property.Id, dataTypeService); - if (dt != null && dt.Data != null) - { - //We've already got the value for the property so we're going to give it to the - // data type's data property so it doesn't go re-look up the value from the db again. - var defaultData = dt.Data as IDataValueSetter; - if (defaultData != null) - { - defaultData.SetValue(property.Value, property.PropertyType.DataTypeDatabaseType.ToString()); - } - - xmlNode.AppendChild(dt.Data.ToXMl(xd)); - } - - var element = xmlNode.GetXElement(); - return element; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 98c17b74e4..19b730dd07 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Data; using System.Globalization; using System.Linq; +using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Models; @@ -23,6 +25,8 @@ namespace Umbraco.Core.Persistence.Repositories private readonly IContentTypeRepository _contentTypeRepository; private readonly ITemplateRepository _templateRepository; private readonly CacheHelper _cacheHelper; + private readonly ContentPreviewRepository _contentPreviewRepository; + private readonly ContentXmlRepository _contentXmlRepository; public ContentRepository(IDatabaseUnitOfWork work, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, CacheHelper cacheHelper) : base(work) @@ -30,6 +34,8 @@ namespace Umbraco.Core.Persistence.Repositories _contentTypeRepository = contentTypeRepository; _templateRepository = templateRepository; _cacheHelper = cacheHelper; + _contentPreviewRepository = new ContentPreviewRepository(work, NullCacheProvider.Current); + _contentXmlRepository = new ContentXmlRepository(work, NullCacheProvider.Current); EnsureUniqueNaming = true; } @@ -40,6 +46,8 @@ namespace Umbraco.Core.Persistence.Repositories _contentTypeRepository = contentTypeRepository; _templateRepository = templateRepository; _cacheHelper = cacheHelper; + _contentPreviewRepository = new ContentPreviewRepository(work, NullCacheProvider.Current); + _contentXmlRepository = new ContentXmlRepository(work, NullCacheProvider.Current); EnsureUniqueNaming = true; } @@ -554,6 +562,30 @@ namespace Umbraco.Core.Persistence.Repositories return repo.GetPermissionsForEntity(entityId); } + /// + /// Adds/updates content/published xml + /// + /// + public void AddOrUpdateContentXml(IContent content, Func xml) + { + var contentExists = Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0; + + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(contentExists, content, xml)); + } + + /// + /// Adds/updates preview xml + /// + /// + public void AddOrUpdatePreviewXml(IContent content, Func xml) + { + var previewExists = + Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", + new { Id = content.Id, Version = content.Version }) != 0; + + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(previewExists, content, xml)); + } + #endregion /// @@ -614,5 +646,123 @@ namespace Umbraco.Core.Persistence.Repositories return currentName; } + + #region Private classes + + /// + /// Used content repository in order to add an entity to the persisted collection to be saved + /// in a single transaction during saving an entity + /// + private class ContentPreviewEntity : ContentXmlEntity + { + public ContentPreviewEntity(bool previewExists, IContentBase content, Func xml) + : base(previewExists, content, xml) + { + Version = content.Version; + } + + public Guid Version { get; private set; } + } + + /// + /// Private class to handle preview insert/update based on standard principles and units of work with transactions + /// + private class ContentPreviewRepository : PetaPocoRepositoryBase + { + public ContentPreviewRepository(IDatabaseUnitOfWork work, IRepositoryCacheProvider cache) + : base(work, cache) + { + } + + #region Not implemented (don't need to for the purposes of this repo) + protected override ContentPreviewEntity PerformGet(int id) + { + throw new NotImplementedException(); + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + throw new NotImplementedException(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + throw new NotImplementedException(); + } + + protected override string GetBaseWhereClause() + { + throw new NotImplementedException(); + } + + protected override IEnumerable GetDeleteClauses() + { + return new List(); + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistDeletedItem(ContentPreviewEntity entity) + { + throw new NotImplementedException(); + } + #endregion + + protected override void PersistNewItem(ContentPreviewEntity entity) + { + if (entity.Content.HasIdentity == false) + { + throw new InvalidOperationException("Cannot insert a preview for a content item that has no identity"); + } + + var previewPoco = new PreviewXmlDto + { + NodeId = entity.Id, + Timestamp = DateTime.Now, + VersionId = entity.Version, + Xml = entity.Xml.ToString(SaveOptions.None) + }; + + Database.Insert(previewPoco); + } + + protected override void PersistUpdatedItem(ContentPreviewEntity entity) + { + if (entity.Content.HasIdentity == false) + { + throw new InvalidOperationException("Cannot update a preview for a content item that has no identity"); + } + + var previewPoco = new PreviewXmlDto + { + NodeId = entity.Id, + Timestamp = DateTime.Now, + VersionId = entity.Version, + Xml = entity.Xml.ToString(SaveOptions.None) + }; + + Database.Update( + "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", + new + { + Xml = previewPoco.Xml, + Timestamp = previewPoco.Timestamp, + Id = previewPoco.NodeId, + Version = previewPoco.VersionId + }); + } + } + + #endregion + + } -} +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs new file mode 100644 index 0000000000..3590ad8815 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ContentXmlRepository.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Caching; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Internal class to handle content/published xml insert/update based on standard principles and units of work with transactions + /// + internal class ContentXmlRepository : PetaPocoRepositoryBase + { + public ContentXmlRepository(IDatabaseUnitOfWork work, IRepositoryCacheProvider cache) + : base(work, cache) + { + } + + #region Not implemented (don't need to for the purposes of this repo) + protected override ContentXmlEntity PerformGet(int id) + { + throw new NotImplementedException(); + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + throw new NotImplementedException(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + throw new NotImplementedException(); + } + + protected override string GetBaseWhereClause() + { + throw new NotImplementedException(); + } + + protected override IEnumerable GetDeleteClauses() + { + return new List(); + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistDeletedItem(ContentXmlEntity entity) + { + throw new NotImplementedException(); + } + #endregion + + protected override void PersistNewItem(ContentXmlEntity entity) + { + if (entity.Content.HasIdentity == false) + { + throw new InvalidOperationException("Cannot insert an xml entry for a content item that has no identity"); + } + + var poco = new ContentXmlDto { NodeId = entity.Id, Xml = entity.Xml.ToString(SaveOptions.None) }; + Database.Insert(poco); + } + + protected override void PersistUpdatedItem(ContentXmlEntity entity) + { + if (entity.Content.HasIdentity == false) + { + throw new InvalidOperationException("Cannot update an xml entry for a content item that has no identity"); + } + + var poco = new ContentXmlDto { NodeId = entity.Id, Xml = entity.Xml.ToString(SaveOptions.None) }; + Database.Update(poco); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 75bee41026..9484121ad7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -45,7 +45,7 @@ namespace Umbraco.Core.Persistence.Repositories _preValRepository = new DataTypePreValueRepository(work, NullCacheProvider.Current); } - private readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); #region Overrides of RepositoryBase @@ -166,42 +166,6 @@ namespace Umbraco.Core.Persistence.Repositories #region Unit of Work Implementation - public override void PersistUpdatedItem(IEntity entity) - { - if (entity is PreValue) - { - _preValRepository.PersistUpdatedItem(entity); - } - else - { - base.PersistUpdatedItem(entity); - } - } - - public override void PersistNewItem(IEntity entity) - { - if (entity is PreValue) - { - _preValRepository.PersistNewItem(entity); - } - else - { - base.PersistNewItem(entity); - } - } - - public override void PersistDeletedItem(IEntity entity) - { - if (entity is PreValue) - { - _preValRepository.PersistDeletedItem(entity); - } - else - { - base.PersistDeletedItem(entity); - } - } - protected override void PersistNewItem(IDataTypeDefinition entity) { ((DataTypeDefinition)entity).AddingEntity(); @@ -326,7 +290,7 @@ AND umbracoNode.id <> @id", public PreValueCollection GetPreValuesCollectionByDataTypeId(int dataTypeId) { - using (var l = new UpgradeableReadLock(Locker)) + using (var l = new UpgradeableReadLock(_locker)) { var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); if (cached != null && cached.Any()) @@ -343,7 +307,7 @@ AND umbracoNode.id <> @id", public string GetPreValueAsString(int preValueId) { - using (var l = new UpgradeableReadLock(Locker)) + using (var l = new UpgradeableReadLock(_locker)) { //We need to see if we can find the cached PreValueCollection based on the cache key above @@ -429,8 +393,9 @@ AND umbracoNode.id <> @id", existing.SortOrder = sortOrder; _preValRepository.AddOrUpdate(new PreValueEntity { - Alias = existing.Alias, + //setting an id will update it Id = existing.Id, + Alias = existing.Alias, SortOrder = existing.SortOrder, Value = existing.Value, DataType = dataType, @@ -483,7 +448,7 @@ AND umbracoNode.id <> @id", /// /// Private class to handle pre-value crud based on units of work with transactions /// - public class PreValueEntity : Entity, IAggregateRoot + private class PreValueEntity : Entity, IAggregateRoot { public string Value { get; set; } public string Alias { get; set; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 3a343febae..a47999f5c4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; @@ -37,5 +38,19 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetPermissionsForEntity(int entityId); + + /// + /// Used to add/update published xml for the content item + /// + /// + /// + void AddOrUpdateContentXml(IContent content, Func xml); + + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IContent content, Func xml); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index f6f38d6002..5336d70a42 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -29,6 +29,9 @@ namespace Umbraco.Core.Services private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly IPublishingStrategy _publishingStrategy; private readonly RepositoryFactory _repositoryFactory; + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + //Support recursive locks because some of the methods that require locking call other methods that require locking. //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); @@ -57,6 +60,18 @@ namespace Umbraco.Core.Services _uowProvider = provider; _publishingStrategy = publishingStrategy; _repositoryFactory = repositoryFactory; + _dataTypeService = new DataTypeService(provider, repositoryFactory); + } + + public ContentService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, IPublishingStrategy publishingStrategy, IDataTypeService dataTypeService) + { + if (provider == null) throw new ArgumentNullException("provider"); + if (repositoryFactory == null) throw new ArgumentNullException("repositoryFactory"); + if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); + _uowProvider = provider; + _publishingStrategy = publishingStrategy; + _repositoryFactory = repositoryFactory; + _dataTypeService = dataTypeService; } /// @@ -1302,13 +1317,14 @@ namespace Umbraco.Core.Services var shouldBePublished = new List(); var shouldBeSaved = new List(); + var asArray = items.ToArray(); using (new WriteLock(Locker)) { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { int i = 0; - foreach (var content in items) + foreach (var content in asArray) { //If the current sort order equals that of the content //we don't need to update it, so just increment the sort order @@ -1332,30 +1348,25 @@ namespace Umbraco.Core.Services shouldBeSaved.Add(content); repository.AddOrUpdate(content); + + //Generate a new preview + var local = content; + repository.AddOrUpdatePreviewXml(content, () => _entitySerializer.Serialize(this, _dataTypeService, local)); + } + + foreach (var content in shouldBePublished) + { + //Create and Save ContentXml DTO + var local = content; + repository.AddOrUpdateContentXml(content, () => _entitySerializer.Serialize(this, _dataTypeService, local)); } uow.Commit(); - - foreach (var content in shouldBeSaved) - { - //Create and Save PreviewXml DTO - var xml = content.ToXml(); - CreateAndSavePreviewXml(xml, content.Id, content.Version, uow.Database); - } - - foreach (var content in shouldBePublished) - { - //Create and Save PreviewXml DTO - var xml = content.ToXml(); - CreateAndSavePreviewXml(xml, content.Id, content.Version, uow.Database); - //Create and Save ContentXml DTO - CreateAndSaveContentXml(xml, content.Id, uow.Database); - } } } if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(items, false), this); + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); if (shouldBePublished.Any()) _publishingStrategy.PublishingFinalized(shouldBePublished, false); @@ -1468,7 +1479,8 @@ namespace Umbraco.Core.Services using (var uow = _uowProvider.GetUnitOfWork()) { - var xml = content.ToXml(); + var xml = _entitySerializer.Serialize(this, _dataTypeService, content); + var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToString(SaveOptions.None) }; var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != @@ -1526,7 +1538,7 @@ namespace Umbraco.Core.Services var xmlItems = new List(); foreach (var c in list) { - var xml = c.ToXml(); + var xml = _entitySerializer.Serialize(this, _dataTypeService, c); xmlItems.Add(new ContentXmlDto { NodeId = c.Id, Xml = xml.ToString(SaveOptions.None) }); } @@ -1650,7 +1662,7 @@ namespace Umbraco.Core.Services foreach (var c in updated) { - var xml = c.ToXml(); + var xml = _entitySerializer.Serialize(this, _dataTypeService, c); var poco = new ContentXmlDto { NodeId = c.Id, Xml = xml.ToString(SaveOptions.None) }; var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }) != null; @@ -1763,17 +1775,17 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); - uow.Commit(); - - var xml = content.ToXml(); - //Preview Xml - CreateAndSavePreviewXml(xml, content.Id, content.Version, uow.Database); - + //Generate a new preview + var local = content; + repository.AddOrUpdatePreviewXml(content, () => _entitySerializer.Serialize(this, _dataTypeService, local)); + if (published) { //Content Xml - CreateAndSaveContentXml(xml, content.Id, uow.Database); + repository.AddOrUpdateContentXml(content, () => _entitySerializer.Serialize(this, _dataTypeService, local)); } + + uow.Commit(); } if (raiseEvents) @@ -1830,11 +1842,12 @@ namespace Umbraco.Core.Services content.ChangePublishedState(PublishedState.Saved); repository.AddOrUpdate(content); - uow.Commit(); - //Preview Xml - var xml = content.ToXml(); - CreateAndSavePreviewXml(xml, content.Id, content.Version, uow.Database); + //Generate a new preview + var local = content; + repository.AddOrUpdatePreviewXml(content, () => _entitySerializer.Serialize(this, _dataTypeService, local)); + + uow.Commit(); } if (raiseEvents) @@ -1906,39 +1919,7 @@ namespace Umbraco.Core.Services } return PublishStatusType.Success; - } - - private void CreateAndSavePreviewXml(XElement xml, int id, Guid version, UmbracoDatabase db) - { - var previewPoco = new PreviewXmlDto - { - NodeId = id, - Timestamp = DateTime.Now, - VersionId = version, - Xml = xml.ToString(SaveOptions.None) - }; - var previewExists = - db.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", - new { Id = id, Version = version }) != 0; - int previewResult = previewExists - ? db.Update( - "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", - new - { - Xml = previewPoco.Xml, - Timestamp = previewPoco.Timestamp, - Id = previewPoco.NodeId, - Version = previewPoco.VersionId - }) - : Convert.ToInt32(db.Insert(previewPoco)); - } - - private void CreateAndSaveContentXml(XElement xml, int id, UmbracoDatabase db) - { - var contentPoco = new ContentXmlDto { NodeId = id, Xml = xml.ToString(SaveOptions.None) }; - var contentExists = db.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = id }) != 0; - int contentResult = contentExists ? db.Update(contentPoco) : Convert.ToInt32(db.Insert(contentPoco)); - } + } private IContentType FindContentTypeByAlias(string contentTypeAlias) { diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs new file mode 100644 index 0000000000..b16e74df4d --- /dev/null +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -0,0 +1,218 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Core.Strings; +using umbraco.interfaces; + +namespace Umbraco.Core.Services +{ + /// + /// A helper class to serialize entities to XML + /// + internal class EntityXmlSerializer + { + /// + /// Exports an item to xml as an + /// + /// + /// + /// Content to export + /// Optional parameter indicating whether to include descendents + /// containing the xml representation of the Content object + public XElement Serialize(IContentService contentService, IDataTypeService dataTypeService, IContent content, bool deep = false) + { + //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); + var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : content.ContentType.Alias.ToSafeAliasWithForcingCheck(); + + var xml = Serialize(dataTypeService, content, nodeName); + xml.Add(new XAttribute("nodeType", content.ContentType.Id)); + xml.Add(new XAttribute("creatorName", content.GetCreatorProfile().Name)); + xml.Add(new XAttribute("writerName", content.GetWriterProfile().Name)); + xml.Add(new XAttribute("writerID", content.WriterId)); + xml.Add(new XAttribute("template", content.Template == null ? "0" : content.Template.Id.ToString(CultureInfo.InvariantCulture))); + xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias)); + + if (deep) + { + var descendants = contentService.GetDescendants(content).ToArray(); + var currentChildren = descendants.Where(x => x.ParentId == content.Id); + AddChildXml(contentService, dataTypeService, descendants, currentChildren, xml); + } + + return xml; + } + + /// + /// Exports an item to xml as an + /// + /// + /// + /// Media to export + /// Optional parameter indicating whether to include descendents + /// containing the xml representation of the Media object + public XElement Serialize(IMediaService mediaService, IDataTypeService dataTypeService, IMedia media, bool deep = false) + { + //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); + var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : media.ContentType.Alias.ToSafeAliasWithForcingCheck(); + + var xml = Serialize(dataTypeService, media, nodeName); + xml.Add(new XAttribute("nodeType", media.ContentType.Id)); + xml.Add(new XAttribute("writerName", media.GetCreatorProfile().Name)); + xml.Add(new XAttribute("writerID", media.CreatorId)); + xml.Add(new XAttribute("version", media.Version)); + xml.Add(new XAttribute("template", 0)); + xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias)); + + if (deep) + { + var descendants = mediaService.GetDescendants(media).ToArray(); + var currentChildren = descendants.Where(x => x.ParentId == media.Id); + AddChildXml(mediaService, dataTypeService, descendants, currentChildren, xml); + } + + return xml; + } + + /// + /// Exports an item to xml as an + /// + /// + /// Member to export + /// containing the xml representation of the Member object + public XElement Serialize(IDataTypeService dataTypeService, IMember member) + { + //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); + var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : member.ContentType.Alias.ToSafeAliasWithForcingCheck(); + + var xml = Serialize(dataTypeService, member, nodeName); + xml.Add(new XAttribute("nodeType", member.ContentType.Id)); + xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias)); + + xml.Add(new XAttribute("loginName", member.Username)); + xml.Add(new XAttribute("email", member.Email)); + xml.Add(new XAttribute("key", member.Key)); + + return xml; + } + + public XElement Serialize(IDataTypeService dataTypeService, Property property) + { + var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "data" : property.Alias.ToSafeAlias(); + + var xd = new XmlDocument(); + var xmlNode = xd.CreateNode(XmlNodeType.Element, nodeName, ""); + + //Add the property alias to the legacy schema + if (UmbracoSettings.UseLegacyXmlSchema) + { + var alias = xd.CreateAttribute("alias"); + alias.Value = property.Alias.ToSafeAlias(); + xmlNode.Attributes.Append(alias); + } + + //This seems to fail during testing + //SD: With the new null checks below, this shouldn't fail anymore. + var dt = property.PropertyType.DataType(property.Id, dataTypeService); + if (dt != null && dt.Data != null) + { + //We've already got the value for the property so we're going to give it to the + // data type's data property so it doesn't go re-look up the value from the db again. + var defaultData = dt.Data as IDataValueSetter; + if (defaultData != null) + { + defaultData.SetValue(property.Value, property.PropertyType.DataTypeDatabaseType.ToString()); + } + + xmlNode.AppendChild(dt.Data.ToXMl(xd)); + } + + var element = xmlNode.GetXElement(); + return element; + } + + /// + /// Used by Media Export to recursively add children + /// + /// + /// + /// + /// + /// + private void AddChildXml(IMediaService mediaService, IDataTypeService dataTypeService, IMedia[] originalDescendants, IEnumerable currentChildren, XElement currentXml) + { + foreach (var child in currentChildren) + { + //add the child's xml + var childXml = Serialize(mediaService, dataTypeService, child); + currentXml.Add(childXml); + //copy local (out of closure) + var c = child; + //get this item's children + var children = originalDescendants.Where(x => x.ParentId == c.Id); + //recurse and add it's children to the child xml element + AddChildXml(mediaService, dataTypeService, originalDescendants, children, childXml); + } + } + + /// + /// Part of the export of IContent and IMedia and IMember which is shared + /// + /// + /// Base Content or Media to export + /// Name of the node + /// + private XElement Serialize(IDataTypeService dataTypeService, IContentBase contentBase, string nodeName) + { + //NOTE: that one will take care of umbracoUrlName + var url = contentBase.GetUrlSegment(); + + var xml = new XElement(nodeName, + new XAttribute("id", contentBase.Id), + new XAttribute("parentID", contentBase.Level > 1 ? contentBase.ParentId : -1), + new XAttribute("level", contentBase.Level), + new XAttribute("creatorID", contentBase.CreatorId), + new XAttribute("sortOrder", contentBase.SortOrder), + new XAttribute("createDate", contentBase.CreateDate.ToString("s")), + new XAttribute("updateDate", contentBase.UpdateDate.ToString("s")), + new XAttribute("nodeName", contentBase.Name), + new XAttribute("urlName", url), + new XAttribute("path", contentBase.Path), + new XAttribute("isDoc", "")); + + foreach (var property in contentBase.Properties.Where(p => p != null && p.Value != null && p.Value.ToString().IsNullOrWhiteSpace() == false)) + { + xml.Add(Serialize(dataTypeService, property)); + } + + return xml; + } + + /// + /// Used by Content Export to recursively add children + /// + /// + /// + /// + /// + /// + private void AddChildXml(IContentService contentService, IDataTypeService dataTypeService, IContent[] originalDescendants, IEnumerable currentChildren, XElement currentXml) + { + foreach (var child in currentChildren) + { + //add the child's xml + var childXml = Serialize(contentService, dataTypeService, child); + currentXml.Add(childXml); + //copy local (out of closure) + var c = child; + //get this item's children + var children = originalDescendants.Where(x => x.ParentId == c.Id); + //recurse and add it's children to the child xml element + AddChildXml(contentService, dataTypeService, originalDescendants, children, childXml); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index 902021b2e3..5330507fd4 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -5,9 +5,7 @@ using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading; -using System.Xml; using System.Xml.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -17,7 +15,6 @@ using Umbraco.Core.Packaging.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Strings; namespace Umbraco.Core.Services { @@ -69,79 +66,8 @@ namespace Umbraco.Core.Services /// containing the xml representation of the Content object public XElement Export(IContent content, bool deep = false) { - //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); - var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : content.ContentType.Alias.ToSafeAliasWithForcingCheck(); - - var xml = Export(content, nodeName); - xml.Add(new XAttribute("nodeType", content.ContentType.Id)); - xml.Add(new XAttribute("creatorName", content.GetCreatorProfile().Name)); - xml.Add(new XAttribute("writerName", content.GetWriterProfile().Name)); - xml.Add(new XAttribute("writerID", content.WriterId)); - xml.Add(new XAttribute("template", content.Template == null ? "0" : content.Template.Id.ToString(CultureInfo.InvariantCulture))); - xml.Add(new XAttribute("nodeTypeAlias", content.ContentType.Alias)); - - if (deep) - { - var descendants = content.Descendants().ToArray(); - var currentChildren = descendants.Where(x => x.ParentId == content.Id); - AddChildXml(descendants, currentChildren, xml); - } - - return xml; - } - - /// - /// Part of the export of IContent and IMedia and IMember which is shared - /// - /// Base Content or Media to export - /// Name of the node - /// - private XElement Export(IContentBase contentBase, string nodeName) - { - //NOTE: that one will take care of umbracoUrlName - var url = contentBase.GetUrlSegment(); - - var xml = new XElement(nodeName, - new XAttribute("id", contentBase.Id), - new XAttribute("parentID", contentBase.Level > 1 ? contentBase.ParentId : -1), - new XAttribute("level", contentBase.Level), - new XAttribute("creatorID", contentBase.CreatorId), - new XAttribute("sortOrder", contentBase.SortOrder), - new XAttribute("createDate", contentBase.CreateDate.ToString("s")), - new XAttribute("updateDate", contentBase.UpdateDate.ToString("s")), - new XAttribute("nodeName", contentBase.Name), - new XAttribute("urlName", url), - new XAttribute("path", contentBase.Path), - new XAttribute("isDoc", "")); - - foreach (var property in contentBase.Properties.Where(p => p != null && p.Value != null && p.Value.ToString().IsNullOrWhiteSpace() == false)) - { - xml.Add(property.ToXml()); - } - - return xml; - } - - /// - /// Used by Content Export to recursively add children - /// - /// - /// - /// - private void AddChildXml(IContent[] originalDescendants, IEnumerable currentChildren, XElement currentXml) - { - foreach (var child in currentChildren) - { - //add the child's xml - var childXml = Export(child); - currentXml.Add(childXml); - //copy local (out of closure) - var c = child; - //get this item's children - var children = originalDescendants.Where(x => x.ParentId == c.Id); - //recurse and add it's children to the child xml element - AddChildXml(originalDescendants, children, childXml); - } + var exporter = new EntityXmlSerializer(); + return exporter.Serialize(_contentService, _dataTypeService, content, deep); } /// @@ -964,20 +890,10 @@ namespace Umbraco.Core.Services /// /// Member to export /// containing the xml representation of the Member object - internal XElement Export(IMember member) + public XElement Export(IMember member) { - //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); - var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : member.ContentType.Alias.ToSafeAliasWithForcingCheck(); - - var xml = Export(member, nodeName); - xml.Add(new XAttribute("nodeType", member.ContentType.Id)); - xml.Add(new XAttribute("nodeTypeAlias", member.ContentType.Alias)); - - xml.Add(new XAttribute("loginName", member.Username)); - xml.Add(new XAttribute("email", member.Email)); - xml.Add(new XAttribute("key", member.Key)); - - return xml; + var exporter = new EntityXmlSerializer(); + return exporter.Serialize(_dataTypeService, member); } #endregion @@ -990,49 +906,10 @@ namespace Umbraco.Core.Services /// Media to export /// Optional parameter indicating whether to include descendents /// containing the xml representation of the Media object - internal XElement Export(IMedia media, bool deep = false) + public XElement Export(IMedia media, bool deep = false) { - //nodeName should match Casing.SafeAliasWithForcingCheck(content.ContentType.Alias); - var nodeName = UmbracoSettings.UseLegacyXmlSchema ? "node" : media.ContentType.Alias.ToSafeAliasWithForcingCheck(); - - var xml = Export(media, nodeName); - xml.Add(new XAttribute("nodeType", media.ContentType.Id)); - xml.Add(new XAttribute("writerName", media.GetCreatorProfile().Name)); - xml.Add(new XAttribute("writerID", media.CreatorId)); - xml.Add(new XAttribute("version", media.Version)); - xml.Add(new XAttribute("template", 0)); - xml.Add(new XAttribute("nodeTypeAlias", media.ContentType.Alias)); - - if (deep) - { - var descendants = media.Descendants().ToArray(); - var currentChildren = descendants.Where(x => x.ParentId == media.Id); - AddChildXml(descendants, currentChildren, xml); - } - - return xml; - } - - /// - /// Used by Media Export to recursively add children - /// - /// - /// - /// - private void AddChildXml(IMedia[] originalDescendants, IEnumerable currentChildren, XElement currentXml) - { - foreach (var child in currentChildren) - { - //add the child's xml - var childXml = Export(child); - currentXml.Add(childXml); - //copy local (out of closure) - var c = child; - //get this item's children - var children = originalDescendants.Where(x => x.ParentId == c.Id); - //recurse and add it's children to the child xml element - AddChildXml(originalDescendants, children, childXml); - } + var exporter = new EntityXmlSerializer(); + return exporter.Serialize(_mediaService, _dataTypeService, media, deep); } #endregion diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index 453b7ec648..e185c4bda4 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -124,7 +124,7 @@ namespace Umbraco.Core.Services _memberService = new Lazy(() => new MemberService(provider, repositoryFactory.Value, _memberGroupService.Value)); if (_contentService == null) - _contentService = new Lazy(() => new ContentService(provider, repositoryFactory.Value, publishingStrategy)); + _contentService = new Lazy(() => new ContentService(provider, repositoryFactory.Value, publishingStrategy, DataTypeService)); if (_mediaService == null) _mediaService = new Lazy(() => new MediaService(provider, repositoryFactory.Value)); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 20a60f7b2a..19077d15f6 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -194,6 +194,7 @@ + @@ -232,6 +233,7 @@ + @@ -788,6 +790,7 @@ + diff --git a/src/Umbraco.Tests/Models/DataValueSetterTests.cs b/src/Umbraco.Tests/Models/DataValueSetterTests.cs index 1785bda3c7..2b8c793001 100644 --- a/src/Umbraco.Tests/Models/DataValueSetterTests.cs +++ b/src/Umbraco.Tests/Models/DataValueSetterTests.cs @@ -85,7 +85,8 @@ namespace Umbraco.Tests.Models // Act - var xml = property.ToXml(dataTypeSvcMock.Object); + var entitySerializer = new EntityXmlSerializer(); + var xml = entitySerializer.Serialize(dataTypeSvcMock.Object, property); // Assert diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 41c6086d94..fe78f719ec 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -900,6 +900,49 @@ namespace Umbraco.Tests.Services Assert.That(sut.Version, Is.EqualTo(version)); } + [Test] + public void Ensure_Content_Xml_Created() + { + var contentService = ServiceContext.ContentService; + var content = contentService.CreateContent("Home US", -1, "umbTextpage", 0); + content.SetValue("author", "Barack Obama"); + + contentService.Save(content); + + var provider = new PetaPocoUnitOfWorkProvider(); + var uow = provider.GetUnitOfWork(); + using (RepositoryResolver.Current.ResolveByType(uow)) + { + Assert.IsFalse(uow.Database.Exists(content.Id)); + } + + contentService.Publish(content); + + uow = provider.GetUnitOfWork(); + using (RepositoryResolver.Current.ResolveByType(uow)) + { + Assert.IsTrue(uow.Database.Exists(content.Id)); + } + } + + [Test] + public void Ensure_Preview_Xml_Created() + { + var contentService = ServiceContext.ContentService; + var content = contentService.CreateContent("Home US", -1, "umbTextpage", 0); + content.SetValue("author", "Barack Obama"); + + contentService.Save(content); + + var provider = new PetaPocoUnitOfWorkProvider(); + var uow = provider.GetUnitOfWork(); + using (RepositoryResolver.Current.ResolveByType(uow)) + { + Assert.IsTrue(uow.Database.SingleOrDefault("WHERE nodeId=@nodeId AND versionId = @versionId", new{nodeId = content.Id, versionId = content.Version}) != null); + } + + } + private IEnumerable CreateContentHierarchy() { var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage");