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");