mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Added ContentIdentity resolvers, batch processing and improved performance for
large imports. --HG-- branch : 1.x extra : source : 3a9c242225bfe36132da7f8760711574eb5a9b43
This commit is contained in:
@@ -20,6 +20,7 @@ using Orchard.Environment.Extensions.Loaders;
|
||||
using Orchard.FileSystems.AppData;
|
||||
using Orchard.FileSystems.WebSite;
|
||||
using Orchard.ImportExport.Services;
|
||||
using Orchard.Recipes.Events;
|
||||
using Orchard.Recipes.Services;
|
||||
using Orchard.Services;
|
||||
using Orchard.Tests.ContentManagement;
|
||||
@@ -73,6 +74,7 @@ namespace Orchard.Tests.Modules.ImportExport.Services {
|
||||
builder.RegisterType<Signals>().As<ISignals>();
|
||||
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
|
||||
builder.RegisterInstance(new Mock<ISettingsFormatter>().Object);
|
||||
builder.RegisterInstance(new Mock<IRecipeExecuteEventHandler>().Object);
|
||||
_session = _sessionFactory.OpenSession();
|
||||
builder.RegisterInstance(new DefaultContentManagerTests.TestSessionLocator(_session)).As<ISessionLocator>();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Orchard.Recipes.Models;
|
||||
using Orchard.Recipes.Services;
|
||||
using Orchard.Services;
|
||||
using Orchard.Tests.Stubs;
|
||||
using Orchard.Recipes.Events;
|
||||
|
||||
namespace Orchard.Tests.Modules.Recipes.Services {
|
||||
[TestFixture]
|
||||
@@ -78,6 +79,7 @@ namespace Orchard.Tests.Modules.Recipes.Services {
|
||||
builder.RegisterType<StubParallelCacheContext>().As<IParallelCacheContext>();
|
||||
builder.RegisterType<StubAsyncTokenProvider>().As<IAsyncTokenProvider>();
|
||||
builder.RegisterInstance(_folders).As<IExtensionFolders>();
|
||||
builder.RegisterInstance(new Mock<IRecipeExecuteEventHandler>().Object);
|
||||
builder.RegisterType<Environment.Extensions.ExtensionManagerTests.StubLoaders>().As<IExtensionLoader>();
|
||||
builder.RegisterType<RecipeParser>().As<IRecipeParser>();
|
||||
builder.RegisterType<StubWebSiteFolder>().As<IWebSiteFolder>();
|
||||
|
||||
@@ -26,7 +26,70 @@ namespace Orchard.Tests.ContentManagement {
|
||||
Assert.That(identity2.Get("foo"), Is.EqualTo("bar"));
|
||||
Assert.That(identity2.Get("abaz"), Is.EqualTo(@"quux/fr\ed=foo"));
|
||||
Assert.That(identity2.Get("yarg"), Is.EqualTo("yiu=foo"));
|
||||
Assert.That(identity2.ToString(), Is.EqualTo(@"/foo=bar/abaz=quux\/fr\\ed=foo/yarg=yiu=foo"));
|
||||
Assert.That(identity2.ToString(), Is.EqualTo(@"/abaz=quux\/fr\\ed=foo/foo=bar/yarg=yiu=foo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContentIdentitiesWithKeysAddedInDifferentOrderAreEqual() {
|
||||
var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
|
||||
var identity1 = new ContentIdentity("/foo=bar");
|
||||
Assert.That(comparer.Equals(identity1, new ContentIdentity(identity1.ToString())));
|
||||
|
||||
var identity2 = new ContentIdentity(@"/foo=bar/abaz=quux\/fr\\ed=foo/yarg=yiu=foo");
|
||||
Assert.That(comparer.Equals(identity2, new ContentIdentity(identity2.ToString())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IdentitiesCanBeAddedWithNoPriority() {
|
||||
var contentIdentity = new ContentIdentity();
|
||||
|
||||
contentIdentity.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1");
|
||||
contentIdentity.Add("alias", "some-unique-item-alias/sub-alias");
|
||||
|
||||
Assert.That("/alias=some-unique-item-alias\\/sub-alias/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentity.ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IdentitiesCanBeAddedWithSamePriority() {
|
||||
var contentIdentity = new ContentIdentity();
|
||||
contentIdentity.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1", 5);
|
||||
contentIdentity.Add("alias", "some-unique-item-alias/sub-alias", 5);
|
||||
|
||||
var contentIdentityNegative = new ContentIdentity();
|
||||
contentIdentityNegative.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1", -5);
|
||||
contentIdentityNegative.Add("alias", "some-unique-item-alias/sub-alias", -5);
|
||||
|
||||
Assert.That("/alias=some-unique-item-alias\\/sub-alias/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentity.ToString()));
|
||||
Assert.That("/alias=some-unique-item-alias\\/sub-alias/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentityNegative.ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LowestPriorityIdentityIsIgnored() {
|
||||
var contentIdentity = new ContentIdentity();
|
||||
contentIdentity.Add("alias", "some-unique-item-alias/sub-alias", 0);
|
||||
contentIdentity.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1", 5);
|
||||
|
||||
var contentIdentityNegative = new ContentIdentity();
|
||||
contentIdentityNegative.Add("alias", "some-unique-item-alias/sub-alias", -5);
|
||||
contentIdentityNegative.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1", 0);
|
||||
|
||||
Assert.That("/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentity.ToString()));
|
||||
Assert.That("/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentityNegative.ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HighestPriorityIdentityIsRetained() {
|
||||
var contentIdentity = new ContentIdentity();
|
||||
contentIdentity.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1", 5);
|
||||
contentIdentity.Add("alias", "some-unique-item-alias/sub-alias", 0);
|
||||
|
||||
var contentIdentityNegative = new ContentIdentity();
|
||||
contentIdentityNegative.Add("identifier", "CAEEB150-F5E9-481D-9FF9-3053D23329C1", 0);
|
||||
contentIdentityNegative.Add("alias", "some-unique-item-alias/sub-alias", -5);
|
||||
|
||||
Assert.That("/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentity.ToString()));
|
||||
Assert.That("/identifier=CAEEB150-F5E9-481D-9FF9-3053D23329C1", Is.EqualTo(contentIdentityNegative.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
src/Orchard.Tests/ContentManagement/ImportContentSessionTests.cs
Normal file
189
src/Orchard.Tests/ContentManagement/ImportContentSessionTests.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Records;
|
||||
|
||||
namespace Orchard.Tests.ContentManagement {
|
||||
[TestFixture]
|
||||
public class ImportContentSessionTests {
|
||||
private ContentIdentity _testItemIdentity1;
|
||||
private ContentIdentity _testItemIdentity2;
|
||||
private ContentIdentity _testItemIdentity3;
|
||||
private ContentIdentity _testItemIdentity4;
|
||||
private ContentIdentity _testItemIdentity5;
|
||||
private Mock<IContentManager> _contentManager;
|
||||
|
||||
#region Init
|
||||
[TestFixtureSetUp]
|
||||
public void TestInit() {
|
||||
_testItemIdentity1 = new ContentIdentity("/ItemId=1");
|
||||
_testItemIdentity2 = new ContentIdentity("/ItemId=2");
|
||||
_testItemIdentity3 = new ContentIdentity("/ItemId=3");
|
||||
_testItemIdentity4 = new ContentIdentity("/ItemId=4");
|
||||
_testItemIdentity5 = new ContentIdentity("/ItemId=5");
|
||||
var draftItem = new ContentItem { VersionRecord = new ContentItemVersionRecord { Id = 1234, Published = false, Latest = true, ContentItemRecord = new ContentItemRecord { Id = 1 } } };
|
||||
var publishedItem = new ContentItem { VersionRecord = new ContentItemVersionRecord { Id = 1234, Published = true, Latest = true, ContentItemRecord = new ContentItemRecord { Id = 1 } } };
|
||||
|
||||
var draftItem5 = new ContentItem { VersionRecord = new ContentItemVersionRecord { Id = 1234, Published = false, Latest = true, ContentItemRecord = new ContentItemRecord { Id = 5 } } };
|
||||
var publishedItem5 = new ContentItem { VersionRecord = new ContentItemVersionRecord { Id = 1234, Published = true, Latest = true, ContentItemRecord = new ContentItemRecord { Id = 5 } } };
|
||||
|
||||
_contentManager = new Mock<IContentManager>();
|
||||
_contentManager.Setup(m => m.Get(It.Is<int>(v => v == 1), It.Is<VersionOptions>(v => v.IsDraftRequired))).Returns(draftItem);
|
||||
_contentManager.Setup(m => m.Get(It.Is<int>(v => v == 1), It.Is<VersionOptions>(v => !v.IsDraftRequired))).Returns(publishedItem);
|
||||
|
||||
_contentManager.Setup(m => m.Get(It.Is<int>(v => v == 5), It.Is<VersionOptions>(v => v.IsDraftRequired))).Returns(draftItem5);
|
||||
_contentManager.Setup(m => m.Get(It.Is<int>(v => v == 5), It.Is<VersionOptions>(v => !v.IsDraftRequired))).Returns(publishedItem5);
|
||||
|
||||
_contentManager.Setup(m => m.GetItemMetadata(It.Is<IContent>(c => c.Id == 1))).Returns(new ContentItemMetadata { Identity = _testItemIdentity1 });
|
||||
_contentManager.Setup(m => m.GetItemMetadata(It.Is<IContent>(c => c.Id == 5))).Returns(new ContentItemMetadata { Identity = _testItemIdentity5 });
|
||||
|
||||
_contentManager.Setup(m => m.New(It.IsAny<string>())).Returns(draftItem5);
|
||||
|
||||
_contentManager.Setup(m => m.HasResolverForIdentity(It.Is<ContentIdentity>(id => id.Get("ItemId") != null))).Returns(true);
|
||||
_contentManager.Setup(m => m.ResolveIdentity(It.Is<ContentIdentity>(id => id.Get("ItemId") == "1"))).Returns(publishedItem);
|
||||
}
|
||||
#endregion
|
||||
|
||||
[Test]
|
||||
public void GetNextInBatchReturnsNullWhenNoItemsSet() {
|
||||
var importContentSession = new ImportContentSession(_contentManager.Object);
|
||||
|
||||
Assert.That(importContentSession.GetNextInBatch(), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetNextInBatchReturnsNullWhenInitializedButNoItemsSet() {
|
||||
var importContentSession = new ImportContentSession(_contentManager.Object);
|
||||
importContentSession.InitializeBatch(0, 20);
|
||||
Assert.That(importContentSession.GetNextInBatch(), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ItemsSetAndUninitializedReturnsAllItems() {
|
||||
var importContentSession = new ImportContentSession(_contentManager.Object);
|
||||
|
||||
importContentSession.Set("/Id=One", "TestType");
|
||||
importContentSession.Set("/Id=Two", "TestType");
|
||||
importContentSession.Set("/Id=Three", "TestType");
|
||||
|
||||
var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=One")));
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=Two")));
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=Three")));
|
||||
Assert.That(importContentSession.GetNextInBatch(), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ItemsSetAndBatchInitialisedReturnsBatchedItems() {
|
||||
var importContentSession = new ImportContentSession(_contentManager.Object);
|
||||
|
||||
importContentSession.Set("/Id=One", "TestType");
|
||||
importContentSession.Set("/Id=Two", "TestType");
|
||||
importContentSession.Set("/Id=Three", "TestType");
|
||||
importContentSession.Set("/Id=Four", "TestType");
|
||||
importContentSession.Set("/Id=Five", "TestType");
|
||||
|
||||
importContentSession.InitializeBatch(1, 2);
|
||||
|
||||
|
||||
var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=Two")));
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=Three")));
|
||||
Assert.That(importContentSession.GetNextInBatch(), Is.Null);
|
||||
|
||||
importContentSession.InitializeBatch(2, 5);
|
||||
|
||||
//item with "/Id=Three" should not be returned twice in the same session
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=Four")));
|
||||
Assert.That(comparer.Equals(importContentSession.GetNextInBatch(), new ContentIdentity("/Id=Five")));
|
||||
Assert.That(importContentSession.GetNextInBatch(), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItemExistsAndNoVersionOptionsReturnsPublishedItem() {
|
||||
var session = new ImportContentSession(_contentManager.Object);
|
||||
session.Set(_testItemIdentity1.ToString(), "TestContentType");
|
||||
var sessionItem = session.Get(_testItemIdentity1.ToString());
|
||||
Assert.IsNotNull(sessionItem);
|
||||
Assert.AreEqual(1, sessionItem.Id);
|
||||
Assert.IsTrue(sessionItem.IsPublished());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItemExistsAndLatestVersionOptionReturnsPublishedItem() {
|
||||
var session = new ImportContentSession(_contentManager.Object);
|
||||
session.Set(_testItemIdentity1.ToString(), "TestContentType");
|
||||
var sessionItem = session.Get(_testItemIdentity1.ToString());
|
||||
Assert.IsNotNull(sessionItem);
|
||||
Assert.AreEqual(1, sessionItem.Id);
|
||||
Assert.IsTrue(sessionItem.IsPublished());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItemExistsAndDraftRequiredVersionOptionReturnsDraft() {
|
||||
var session = new ImportContentSession(_contentManager.Object);
|
||||
session.Set(_testItemIdentity1.ToString(), "TestContentType");
|
||||
var sessionItem = session.Get(_testItemIdentity1.ToString(), VersionOptions.DraftRequired);
|
||||
Assert.IsNotNull(sessionItem);
|
||||
Assert.That(1, Is.EqualTo(sessionItem.Id));
|
||||
Assert.IsFalse(sessionItem.IsPublished());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetNextInBatchInitialisedWithOneItemReturnsOneItemThenNull() {
|
||||
var session = new ImportContentSession(_contentManager.Object);
|
||||
session.Set(_testItemIdentity1.ToString(), "TestContentType");
|
||||
session.InitializeBatch(0, 1);
|
||||
var firstIdentity = session.GetNextInBatch();
|
||||
var secondIdentity = session.GetNextInBatch();
|
||||
|
||||
var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
Assert.That(comparer.Equals(_testItemIdentity1, firstIdentity));
|
||||
Assert.That(secondIdentity, Is.Null);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void GetNextInBatchInitialisedTwoBatchesReturnsItemsOnceEach() {
|
||||
var session = new ImportContentSession(_contentManager.Object);
|
||||
session.Set(_testItemIdentity1.ToString(), "TestContentType");
|
||||
session.Set(_testItemIdentity2.ToString(), "TestContentType");
|
||||
session.Set(_testItemIdentity3.ToString(), "TestContentType");
|
||||
session.Set(_testItemIdentity4.ToString(), "TestContentType");
|
||||
session.Set(_testItemIdentity5.ToString(), "TestContentType");
|
||||
|
||||
session.InitializeBatch(0, 2);
|
||||
var firstIdentity = session.GetNextInBatch();
|
||||
//get later item as dependency
|
||||
var dependencyItem = session.Get(_testItemIdentity5.ToString(), VersionOptions.Latest);
|
||||
var dependencyIdentity = session.GetNextInBatch();
|
||||
var secondIdentity = session.GetNextInBatch();
|
||||
var afterBatch1 = session.GetNextInBatch();
|
||||
|
||||
session.InitializeBatch(2, 2);
|
||||
var thirdIdentity = session.GetNextInBatch();
|
||||
var fourthdentity = session.GetNextInBatch();
|
||||
var afterBatch2 = session.GetNextInBatch();
|
||||
|
||||
session.InitializeBatch(4, 2);
|
||||
var fifthIdentity = session.GetNextInBatch();
|
||||
var afterBatch3 = session.GetNextInBatch();
|
||||
|
||||
var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
Assert.That(comparer.Equals(_testItemIdentity1, firstIdentity));
|
||||
Assert.That(comparer.Equals(_testItemIdentity5, dependencyIdentity));
|
||||
Assert.That(comparer.Equals(_testItemIdentity2, secondIdentity));
|
||||
Assert.That(afterBatch1, Is.Null);
|
||||
|
||||
Assert.That(comparer.Equals(_testItemIdentity3, thirdIdentity));
|
||||
Assert.That(comparer.Equals(_testItemIdentity4, fourthdentity));
|
||||
Assert.That(afterBatch2, Is.Null);
|
||||
|
||||
Assert.That(fifthIdentity, Is.Null); //already processed as dependency
|
||||
Assert.That(afterBatch3, Is.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,7 @@
|
||||
<Compile Include="ContentManagement\Handlers\EpsilonPartHandler.cs" />
|
||||
<Compile Include="ContentManagement\Handlers\GammaPartHandler.cs" />
|
||||
<Compile Include="ContentManagement\Handlers\ModelBuilderTests.cs" />
|
||||
<Compile Include="ContentManagement\ImportContentSessionTests.cs" />
|
||||
<Compile Include="ContentManagement\MetaData\Builders\ContentTypeDefinitionBuilderTests.cs" />
|
||||
<Compile Include="ContentManagement\MetaData\Services\ContentDefinitionReaderTests.cs" />
|
||||
<Compile Include="ContentManagement\MetaData\Services\ContentDefinitionWriterTests.cs" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Models;
|
||||
@@ -8,9 +9,14 @@ using Orchard.ContentManagement.Handlers;
|
||||
namespace Orchard.Core.Common.Handlers {
|
||||
[UsedImplicitly]
|
||||
public class IdentityPartHandler : ContentHandler {
|
||||
public IdentityPartHandler(IRepository<IdentityPartRecord> identityRepository) {
|
||||
private readonly IContentManager _contentManager;
|
||||
|
||||
public IdentityPartHandler(IRepository<IdentityPartRecord> identityRepository,
|
||||
IContentManager contentManager) {
|
||||
Filters.Add(StorageFilter.For(identityRepository));
|
||||
OnInitializing<IdentityPart>(AssignIdentity);
|
||||
|
||||
_contentManager = contentManager;
|
||||
}
|
||||
|
||||
protected void AssignIdentity(InitializingContentContext context, IdentityPart part) {
|
||||
@@ -24,5 +30,24 @@ namespace Orchard.Core.Common.Handlers {
|
||||
context.Metadata.Identity.Add("Identifier", part.Identifier);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RegisterIdentityResolver(RegisterIdentityResolverContext context) {
|
||||
context.Register(id => id.Get("Identifier") != null, ResolveIdentity);
|
||||
}
|
||||
|
||||
private ContentItem ResolveIdentity(ContentIdentity identity) {
|
||||
var identifier = identity.Get("Identifier");
|
||||
|
||||
if (identifier == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var comparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
return _contentManager
|
||||
.Query<IdentityPart, IdentityPartRecord>()
|
||||
.Where(p => p.Identifier == identifier)
|
||||
.List<ContentItem>()
|
||||
.FirstOrDefault(c => comparer.Equals(identity, _contentManager.GetItemMetadata(c).Identity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Orchard.ContentManagement.MetaData;
|
||||
using Orchard.ImportExport.Services;
|
||||
using Orchard.ImportExport.ViewModels;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Recipes.Services;
|
||||
using Orchard.UI.Notify;
|
||||
using Orchard.ImportExport.Models;
|
||||
|
||||
@@ -15,16 +16,19 @@ namespace Orchard.ImportExport.Controllers {
|
||||
private readonly IImportExportService _importExportService;
|
||||
private readonly IContentDefinitionManager _contentDefinitionManager;
|
||||
private readonly ICustomExportStep _customExportStep;
|
||||
private readonly IRecipeJournal _recipeJournal;
|
||||
|
||||
public AdminController(
|
||||
IOrchardServices services,
|
||||
IImportExportService importExportService,
|
||||
IContentDefinitionManager contentDefinitionManager,
|
||||
ICustomExportStep customExportStep
|
||||
ICustomExportStep customExportStep,
|
||||
IRecipeJournal recipeJournal
|
||||
) {
|
||||
_importExportService = importExportService;
|
||||
_contentDefinitionManager = contentDefinitionManager;
|
||||
_customExportStep = customExportStep;
|
||||
_recipeJournal = recipeJournal;
|
||||
Services = services;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
@@ -49,11 +53,17 @@ namespace Orchard.ImportExport.Controllers {
|
||||
}
|
||||
|
||||
if (ModelState.IsValid) {
|
||||
_importExportService.Import(new StreamReader(Request.Files["RecipeFile"].InputStream).ReadToEnd());
|
||||
var executionId = _importExportService.Import(new StreamReader(Request.Files["RecipeFile"].InputStream).ReadToEnd());
|
||||
Services.Notifier.Information(T("Your recipe has been imported."));
|
||||
}
|
||||
|
||||
return RedirectToAction("Import");
|
||||
return RedirectToAction("ImportResult", new { ExecutionId = executionId });
|
||||
}
|
||||
return View(new ImportViewModel());
|
||||
}
|
||||
|
||||
public ActionResult ImportResult(string executionId) {
|
||||
var journal = _recipeJournal.GetRecipeJournal(executionId);
|
||||
return View(journal);
|
||||
}
|
||||
|
||||
public ActionResult Export() {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Orchard.ImportExport.Models {
|
||||
public class ExportOptions {
|
||||
public bool ExportMetadata { get; set; }
|
||||
public bool ExportData { get; set; }
|
||||
public int? ImportBatchSize { get; set; }
|
||||
public VersionHistoryOptions VersionHistoryOptions { get; set; }
|
||||
public bool ExportSiteSettings { get; set; }
|
||||
public IEnumerable<string> CustomSteps { get; set; }
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
<UpgradeBackupLocation>
|
||||
</UpgradeBackupLocation>
|
||||
<OldToolsVersion>4.0</OldToolsVersion>
|
||||
<IISExpressSSLPort />
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@@ -101,6 +105,9 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="RecipeHandlers\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Admin\ImportResult.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ImportExport.Models;
|
||||
|
||||
namespace Orchard.ImportExport.Services {
|
||||
public interface IImportExportService : IDependency {
|
||||
void Import(string recipeText);
|
||||
string Import(string recipeText);
|
||||
string Export(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, ExportOptions exportOptions);
|
||||
string Export(IEnumerable<string> contentTypes, ExportOptions exportOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Orchard.ContentManagement;
|
||||
@@ -10,7 +11,9 @@ using Orchard.FileSystems.AppData;
|
||||
using Orchard.ImportExport.Models;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Recipes.Models;
|
||||
using Orchard.Recipes.Services;
|
||||
using Orchard.Services;
|
||||
using VersionOptions = Orchard.ContentManagement.VersionOptions;
|
||||
|
||||
namespace Orchard.ImportExport.Services {
|
||||
@@ -23,6 +26,7 @@ namespace Orchard.ImportExport.Services {
|
||||
private readonly IRecipeParser _recipeParser;
|
||||
private readonly IRecipeManager _recipeManager;
|
||||
private readonly IShellDescriptorManager _shellDescriptorManager;
|
||||
private readonly IClock _clock;
|
||||
private readonly IEnumerable<IExportEventHandler> _exportEventHandlers;
|
||||
private const string ExportsDirectory = "Exports";
|
||||
|
||||
@@ -32,8 +36,9 @@ namespace Orchard.ImportExport.Services {
|
||||
IContentDefinitionWriter contentDefinitionWriter,
|
||||
IAppDataFolder appDataFolder,
|
||||
IRecipeParser recipeParser,
|
||||
IRecipeManager recipeManager,
|
||||
IRecipeManager recipeManager,
|
||||
IShellDescriptorManager shellDescriptorManager,
|
||||
IClock clock,
|
||||
IEnumerable<IExportEventHandler> exportEventHandlers) {
|
||||
_orchardServices = orchardServices;
|
||||
_contentDefinitionManager = contentDefinitionManager;
|
||||
@@ -42,6 +47,7 @@ namespace Orchard.ImportExport.Services {
|
||||
_recipeParser = recipeParser;
|
||||
_recipeManager = recipeManager;
|
||||
_shellDescriptorManager = shellDescriptorManager;
|
||||
_clock = clock;
|
||||
_exportEventHandlers = exportEventHandlers;
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
@@ -50,13 +56,24 @@ namespace Orchard.ImportExport.Services {
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void Import(string recipeText) {
|
||||
public string Import(string recipeText) {
|
||||
var recipe = _recipeParser.ParseRecipe(recipeText);
|
||||
_recipeManager.Execute(recipe);
|
||||
var executionId = _recipeManager.Execute(recipe);
|
||||
UpdateShell();
|
||||
return executionId;
|
||||
}
|
||||
|
||||
public string Export(IEnumerable<string> contentTypes, ExportOptions exportOptions) {
|
||||
//items need to be retrieved
|
||||
IEnumerable<ContentItem> contentItems = null;
|
||||
if (exportOptions.ExportData) {
|
||||
contentItems = _orchardServices.ContentManager.Query(GetContentExportVersionOptions(exportOptions.VersionHistoryOptions), contentTypes.ToArray()).List();
|
||||
}
|
||||
|
||||
return Export(contentTypes, contentItems, exportOptions);
|
||||
}
|
||||
|
||||
public string Export(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, ExportOptions exportOptions) {
|
||||
var exportDocument = CreateExportRoot();
|
||||
|
||||
var context = new ExportContext {
|
||||
@@ -67,7 +84,7 @@ namespace Orchard.ImportExport.Services {
|
||||
|
||||
_exportEventHandlers.Invoke(x => x.Exporting(context), Logger);
|
||||
|
||||
if (exportOptions.ExportMetadata) {
|
||||
if (exportOptions.ExportMetadata && (!exportOptions.ExportData || contentItems.Any())) {
|
||||
exportDocument.Element("Orchard").Add(ExportMetadata(contentTypes));
|
||||
}
|
||||
|
||||
@@ -75,8 +92,8 @@ namespace Orchard.ImportExport.Services {
|
||||
exportDocument.Element("Orchard").Add(ExportSiteSettings());
|
||||
}
|
||||
|
||||
if (exportOptions.ExportData) {
|
||||
exportDocument.Element("Orchard").Add(ExportData(contentTypes, exportOptions.VersionHistoryOptions));
|
||||
if (exportOptions.ExportData && contentItems.Any()) {
|
||||
exportDocument.Element("Orchard").Add(ExportData(contentTypes, contentItems, exportOptions.ImportBatchSize));
|
||||
}
|
||||
|
||||
_exportEventHandlers.Invoke(x => x.Exported(context), Logger);
|
||||
@@ -91,7 +108,8 @@ namespace Orchard.ImportExport.Services {
|
||||
new XElement("Orchard",
|
||||
new XElement("Recipe",
|
||||
new XElement("Name", "Generated by Orchard.ImportExport"),
|
||||
new XElement("Author", _orchardServices.WorkContext.CurrentUser.UserName)
|
||||
new XElement("Author", _orchardServices.WorkContext.CurrentUser.UserName),
|
||||
new XElement("ExportUtc", XmlConvert.ToString(_clock.UtcNow, XmlDateTimeSerializationMode.Utc))
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -148,18 +166,18 @@ namespace Orchard.ImportExport.Services {
|
||||
return settings;
|
||||
}
|
||||
|
||||
private XElement ExportData(IEnumerable<string> contentTypes, VersionHistoryOptions versionHistoryOptions) {
|
||||
private XElement ExportData(IEnumerable<string> contentTypes, IEnumerable<ContentItem> contentItems, int? batchSize) {
|
||||
var data = new XElement("Data");
|
||||
var options = GetContentExportVersionOptions(versionHistoryOptions);
|
||||
|
||||
var contentItems = _orchardServices.ContentManager.Query(options).List();
|
||||
if (batchSize.HasValue && batchSize.Value > 0)
|
||||
data.SetAttributeValue("BatchSize", batchSize);
|
||||
|
||||
foreach (var contentType in contentTypes) {
|
||||
var type = contentType;
|
||||
var items = contentItems.Where(i => i.ContentType == type);
|
||||
foreach (var contentItem in items) {
|
||||
var contentItemElement = ExportContentItem(contentItem);
|
||||
if (contentItemElement != null)
|
||||
if (contentItemElement != null)
|
||||
data.Add(contentItemElement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@model Orchard.Recipes.Models.RecipeJournal
|
||||
@{
|
||||
Layout.Title = T("Import Result").ToString();
|
||||
}
|
||||
<fieldset>
|
||||
<label>@T("Status"):</label>
|
||||
<span>@Model.Status</span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<table class="items" summary="@T("These are messages logged during the import process")">
|
||||
<colgroup>
|
||||
<col id="Col1" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">@T("Message")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var message in Model.Messages)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@message.Message
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
@Html.ActionLink(T("Close").ToString(), "Import", null, new { @class = "button" })
|
||||
@@ -20,6 +20,10 @@
|
||||
<UpgradeBackupLocation>
|
||||
</UpgradeBackupLocation>
|
||||
<OldToolsVersion>4.0</OldToolsVersion>
|
||||
<IISExpressSSLPort />
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Data;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Recipes.Models;
|
||||
@@ -8,9 +11,11 @@ using Orchard.Recipes.Services;
|
||||
namespace Orchard.Recipes.RecipeHandlers {
|
||||
public class DataRecipeHandler : IRecipeHandler {
|
||||
private readonly IOrchardServices _orchardServices;
|
||||
private readonly ITransactionManager _transactionManager;
|
||||
|
||||
public DataRecipeHandler(IOrchardServices orchardServices) {
|
||||
public DataRecipeHandler(IOrchardServices orchardServices, ITransactionManager transactionManager) {
|
||||
_orchardServices = orchardServices;
|
||||
_transactionManager = transactionManager;
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
@@ -25,27 +30,72 @@ namespace Orchard.Recipes.RecipeHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
// First pass to resolve content items from content identities for all content items, new and old.
|
||||
var importContentSession = new ImportContentSession(_orchardServices.ContentManager);
|
||||
foreach (var element in recipeContext.RecipeStep.Step.Elements()) {
|
||||
var elementId = element.Attribute("Id");
|
||||
if (elementId == null)
|
||||
continue;
|
||||
|
||||
var identity = elementId.Value;
|
||||
var status = element.Attribute("Status");
|
||||
|
||||
importContentSession.Set(identity, element.Name.LocalName);
|
||||
|
||||
var item = importContentSession.Get(identity);
|
||||
// Populate local dictionary with elements and their ids
|
||||
var elementDictionary = CreateElementDictionary(recipeContext.RecipeStep.Step);
|
||||
|
||||
//Populate import session with all identities to be imported
|
||||
foreach (var identity in elementDictionary.Keys) {
|
||||
importContentSession.Set(identity.ToString(), elementDictionary[identity].Name.LocalName);
|
||||
}
|
||||
|
||||
// Second pass to import the content items.
|
||||
foreach (var element in recipeContext.RecipeStep.Step.Elements()) {
|
||||
_orchardServices.ContentManager.Import(element, importContentSession);
|
||||
//Determine if the import is to be batched in multiple transactions
|
||||
var startIndex = 0;
|
||||
int batchSize = GetBatchSizeForDataStep(recipeContext.RecipeStep.Step);
|
||||
|
||||
//Run the import
|
||||
ContentIdentity nextIdentity = null;
|
||||
try {
|
||||
while (startIndex < elementDictionary.Count) {
|
||||
importContentSession.InitializeBatch(startIndex, batchSize);
|
||||
|
||||
//the session determines which items are included in the current batch
|
||||
//so that dependencies can be managed within the same transaction
|
||||
nextIdentity = importContentSession.GetNextInBatch();
|
||||
while (nextIdentity != null) {
|
||||
_orchardServices.ContentManager.Import(elementDictionary[nextIdentity], importContentSession);
|
||||
nextIdentity = importContentSession.GetNextInBatch();
|
||||
}
|
||||
|
||||
startIndex += batchSize;
|
||||
|
||||
//Create a new transaction for each batch
|
||||
if (startIndex < elementDictionary.Count) {
|
||||
_orchardServices.ContentManager.Clear();
|
||||
_transactionManager.RequireNew();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) {
|
||||
//Ensure a failed batch is rolled back
|
||||
_transactionManager.Cancel();
|
||||
throw;
|
||||
}
|
||||
|
||||
recipeContext.Executed = true;
|
||||
}
|
||||
|
||||
private Dictionary<ContentIdentity, XElement> CreateElementDictionary(XElement step) {
|
||||
var elementDictionary = new Dictionary<ContentIdentity, XElement>(new ContentIdentity.ContentIdentityEqualityComparer());
|
||||
foreach (var element in step.Elements()) {
|
||||
if (element.Attribute("Id") == null || string.IsNullOrEmpty(element.Attribute("Id").Value))
|
||||
continue;
|
||||
|
||||
var identity = new ContentIdentity(element.Attribute("Id").Value);
|
||||
elementDictionary[identity] = element;
|
||||
}
|
||||
return elementDictionary;
|
||||
}
|
||||
|
||||
private int GetBatchSizeForDataStep(XElement step) {
|
||||
int batchSize;
|
||||
if (step.Attribute("BatchSize") == null ||
|
||||
!int.TryParse(step.Attribute("BatchSize").Value, out batchSize) ||
|
||||
batchSize <= 0) {
|
||||
batchSize = int.MaxValue;
|
||||
}
|
||||
return batchSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Recipes.Events;
|
||||
using Orchard.Recipes.Models;
|
||||
|
||||
namespace Orchard.Recipes.Services {
|
||||
@@ -8,11 +9,14 @@ namespace Orchard.Recipes.Services {
|
||||
private readonly IRecipeStepQueue _recipeStepQueue;
|
||||
private readonly IRecipeScheduler _recipeScheduler;
|
||||
private readonly IRecipeJournal _recipeJournal;
|
||||
private readonly IRecipeExecuteEventHandler _recipeExecuteEventHandler;
|
||||
|
||||
public RecipeManager(IRecipeStepQueue recipeStepQueue, IRecipeScheduler recipeScheduler, IRecipeJournal recipeJournal) {
|
||||
public RecipeManager(IRecipeStepQueue recipeStepQueue, IRecipeScheduler recipeScheduler, IRecipeJournal recipeJournal,
|
||||
IRecipeExecuteEventHandler recipeExecuteEventHandler) {
|
||||
_recipeStepQueue = recipeStepQueue;
|
||||
_recipeScheduler = recipeScheduler;
|
||||
_recipeJournal = recipeJournal;
|
||||
_recipeExecuteEventHandler = recipeExecuteEventHandler;
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
@@ -27,6 +31,7 @@ namespace Orchard.Recipes.Services {
|
||||
|
||||
var executionId = Guid.NewGuid().ToString("n");
|
||||
_recipeJournal.ExecutionStart(executionId);
|
||||
_recipeExecuteEventHandler.ExecutionStart(executionId, recipe);
|
||||
|
||||
foreach (var recipeStep in recipe.RecipeSteps) {
|
||||
_recipeStepQueue.Enqueue(executionId, recipeStep);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
@@ -45,6 +46,9 @@ namespace Orchard.Recipes.Services {
|
||||
case "Version":
|
||||
recipe.Version = metadataElement.Value;
|
||||
break;
|
||||
case "ExportUtc":
|
||||
recipe.ExportUtc = !string.IsNullOrEmpty(metadataElement.Value) ? (DateTime?)XmlConvert.ToDateTime(metadataElement.Value, XmlDateTimeSerializationMode.Utc) : null;
|
||||
break;
|
||||
case "Tags":
|
||||
recipe.Tags = metadataElement.Value;
|
||||
break;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Recipes.Events;
|
||||
using Orchard.Recipes.Models;
|
||||
|
||||
namespace Orchard.Recipes.Services {
|
||||
@@ -9,11 +10,14 @@ namespace Orchard.Recipes.Services {
|
||||
private readonly IRecipeStepQueue _recipeStepQueue;
|
||||
private readonly IRecipeJournal _recipeJournal;
|
||||
private readonly IEnumerable<IRecipeHandler> _recipeHandlers;
|
||||
private readonly IRecipeExecuteEventHandler _recipeExecuteEventHandler;
|
||||
|
||||
public RecipeStepExecutor(IRecipeStepQueue recipeStepQueue, IRecipeJournal recipeJournal, IEnumerable<IRecipeHandler> recipeHandlers) {
|
||||
public RecipeStepExecutor(IRecipeStepQueue recipeStepQueue, IRecipeJournal recipeJournal,
|
||||
IEnumerable<IRecipeHandler> recipeHandlers, IRecipeExecuteEventHandler recipeExecuteEventHandler) {
|
||||
_recipeStepQueue = recipeStepQueue;
|
||||
_recipeJournal = recipeJournal;
|
||||
_recipeHandlers = recipeHandlers;
|
||||
_recipeExecuteEventHandler = recipeExecuteEventHandler;
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
T = NullLocalizer.Instance;
|
||||
@@ -26,29 +30,38 @@ namespace Orchard.Recipes.Services {
|
||||
var nextRecipeStep= _recipeStepQueue.Dequeue(executionId);
|
||||
if (nextRecipeStep == null) {
|
||||
_recipeJournal.ExecutionComplete(executionId);
|
||||
_recipeExecuteEventHandler.ExecutionComplete(executionId);
|
||||
return false;
|
||||
}
|
||||
_recipeJournal.WriteJournalEntry(executionId, string.Format("Executing step {0}.", nextRecipeStep.Name));
|
||||
var recipeContext = new RecipeContext { RecipeStep = nextRecipeStep, Executed = false };
|
||||
try {
|
||||
_recipeExecuteEventHandler.RecipeStepExecuting(executionId, recipeContext);
|
||||
foreach (var recipeHandler in _recipeHandlers) {
|
||||
recipeHandler.ExecuteRecipeStep(recipeContext);
|
||||
}
|
||||
_recipeExecuteEventHandler.RecipeStepExecuted(executionId, recipeContext);
|
||||
}
|
||||
catch(Exception exception) {
|
||||
Logger.Error(exception, "Recipe execution {0} was cancelled because a step failed to execute", executionId);
|
||||
while (_recipeStepQueue.Dequeue(executionId) != null) ;
|
||||
_recipeJournal.ExecutionFailed(executionId);
|
||||
throw new OrchardCoreException(T("Recipe execution with id {0} was cancelled because the \"{1}\" step failed to execute. The following exception was thrown: {2}. Refer to the recipe journal for more information.",
|
||||
executionId, nextRecipeStep.Name, exception.Message));
|
||||
var message = T("Recipe execution with id {0} was cancelled because the \"{1}\" step failed to execute. The following exception was thrown: {2}. Refer to the error logs for more information.",
|
||||
executionId, nextRecipeStep.Name, exception.Message);
|
||||
_recipeJournal.WriteJournalEntry(executionId, message.ToString());
|
||||
|
||||
throw new OrchardCoreException(message);
|
||||
}
|
||||
|
||||
if (!recipeContext.Executed) {
|
||||
Logger.Error("Could not execute recipe step '{0}' because the recipe handler was not found.", recipeContext.RecipeStep.Name);
|
||||
while (_recipeStepQueue.Dequeue(executionId) != null) ;
|
||||
_recipeJournal.ExecutionFailed(executionId);
|
||||
throw new OrchardCoreException(T("Recipe execution with id {0} was cancelled because the recipe handler for step \"{1}\" was not found. Refer to the recipe journal for more information.",
|
||||
executionId, nextRecipeStep.Name));
|
||||
var message = T("Recipe execution with id {0} was cancelled because the recipe handler for step \"{1}\" was not found. Refer to the error logs for more information.",
|
||||
executionId, nextRecipeStep.Name);
|
||||
_recipeJournal.WriteJournalEntry(executionId, message.ToString());
|
||||
|
||||
throw new OrchardCoreException(message);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,8 @@ using System.Text;
|
||||
namespace Orchard.ContentManagement {
|
||||
public class ContentIdentity {
|
||||
private readonly Dictionary<string, string> _dictionary;
|
||||
private int _currentIdentityPriority = int.MinValue; //initialise to lowest possible priority
|
||||
private string _encodedIdentity = null;
|
||||
|
||||
public ContentIdentity() {
|
||||
_dictionary = new Dictionary<string, string>();
|
||||
@@ -18,19 +20,33 @@ namespace Orchard.ContentManagement {
|
||||
foreach (var identityEntry in identityEntries) {
|
||||
var keyValuePair = GetIdentityKeyValue(identityEntry);
|
||||
if (keyValuePair != null) {
|
||||
_dictionary.Add(keyValuePair.Value.Key, UnencodeIdentityValue(keyValuePair.Value.Value));
|
||||
Add(keyValuePair.Value.Key, UnencodeIdentityValue(keyValuePair.Value.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string name, string value) {
|
||||
Add(name, value, 0/*default priority*/);
|
||||
}
|
||||
|
||||
public void Add(string name, string value, int priority) {
|
||||
if (priority < _currentIdentityPriority)
|
||||
return; //lower priority, so ignore
|
||||
if (priority > _currentIdentityPriority)
|
||||
_dictionary.Clear(); //higher, so override and delete existing
|
||||
|
||||
//save the current highest priority
|
||||
_currentIdentityPriority = priority;
|
||||
|
||||
//if equal or higher priority add to identity collection
|
||||
if (_dictionary.ContainsKey(name)) {
|
||||
_dictionary[name] = value;
|
||||
}
|
||||
else {
|
||||
_dictionary.Add(name, value);
|
||||
else {
|
||||
_dictionary.Add(name, value);
|
||||
}
|
||||
_encodedIdentity = null;
|
||||
}
|
||||
|
||||
public string Get(string name) {
|
||||
@@ -38,12 +54,16 @@ namespace Orchard.ContentManagement {
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
if (_encodedIdentity != null)
|
||||
return _encodedIdentity;
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var key in _dictionary.Keys) {
|
||||
foreach (var key in _dictionary.Keys.OrderBy(key => key)) {
|
||||
var escapedIdentity = EncodeIdentityValue(_dictionary[key]);
|
||||
stringBuilder.Append("/" + key + "=" + escapedIdentity);
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
_encodedIdentity = stringBuilder.ToString();
|
||||
return _encodedIdentity;
|
||||
}
|
||||
|
||||
private static string EncodeIdentityValue(string identityValue) {
|
||||
@@ -141,14 +161,11 @@ namespace Orchard.ContentManagement {
|
||||
|
||||
public class ContentIdentityEqualityComparer : IEqualityComparer<ContentIdentity> {
|
||||
public bool Equals(ContentIdentity contentIdentity1, ContentIdentity contentIdentity2) {
|
||||
if (contentIdentity1._dictionary.Keys.Count != contentIdentity2._dictionary.Keys.Count)
|
||||
return false;
|
||||
|
||||
return contentIdentity1._dictionary.OrderBy(kvp => kvp.Key).SequenceEqual(contentIdentity2._dictionary.OrderBy(kvp => kvp.Key));
|
||||
return contentIdentity1.ToString().Equals(contentIdentity2.ToString());
|
||||
}
|
||||
|
||||
public int GetHashCode(ContentIdentity contentIdentity) {
|
||||
return contentIdentity._dictionary.OrderBy(kvp => kvp.Key).ToString().GetHashCode();
|
||||
return contentIdentity.ToString().GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -514,6 +514,18 @@ namespace Orchard.ContentManagement {
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasResolverForIdentity(ContentIdentity contentIdentity) {
|
||||
var context = new RegisterIdentityResolverContext();
|
||||
Handlers.Invoke(handler => handler.RegisterIdentityResolver(context), Logger);
|
||||
return context.HasResolverForIdentity(contentIdentity);
|
||||
}
|
||||
|
||||
public ContentItem ResolveIdentity(ContentIdentity contentIdentity) {
|
||||
var context = new RegisterIdentityResolverContext();
|
||||
Handlers.Invoke(handler => handler.RegisterIdentityResolver(context), Logger);
|
||||
return context.ResolveIdentity(contentIdentity);
|
||||
}
|
||||
|
||||
public ContentItemMetadata GetItemMetadata(IContent content) {
|
||||
var context = new GetContentItemMetadataContext {
|
||||
ContentItem = content.ContentItem,
|
||||
@@ -593,7 +605,7 @@ namespace Orchard.ContentManagement {
|
||||
var identity = elementId.Value;
|
||||
var status = element.Attribute("Status");
|
||||
|
||||
var item = importContentSession.Get(identity, XmlConvert.DecodeName(element.Name.LocalName));
|
||||
var item = importContentSession.Get(identity, VersionOptions.DraftRequired, XmlConvert.DecodeName(element.Name.LocalName));
|
||||
if (item == null) {
|
||||
item = New(XmlConvert.DecodeName(element.Name.LocalName));
|
||||
if (status != null && status.Value == "Draft") {
|
||||
@@ -625,7 +637,7 @@ namespace Orchard.ContentManagement {
|
||||
contentHandler.Imported(context);
|
||||
}
|
||||
|
||||
var savedItem = Get(item.Id, VersionOptions.DraftRequired);
|
||||
var savedItem = Get(item.Id, VersionOptions.Latest);
|
||||
|
||||
// the item has been pre-created in the first pass of the import, create it in db
|
||||
if(savedItem == null) {
|
||||
|
||||
@@ -327,6 +327,10 @@ namespace Orchard.ContentManagement.Handlers {
|
||||
Exported(context);
|
||||
}
|
||||
|
||||
void IContentHandler.RegisterIdentityResolver(RegisterIdentityResolverContext context) {
|
||||
RegisterIdentityResolver(context);
|
||||
}
|
||||
|
||||
void IContentHandler.GetContentItemMetadata(GetContentItemMetadataContext context) {
|
||||
foreach (var filter in Filters.OfType<IContentTemplateFilter>())
|
||||
filter.GetContentItemMetadata(context);
|
||||
@@ -382,6 +386,7 @@ namespace Orchard.ContentManagement.Handlers {
|
||||
protected virtual void Exporting(ExportContentContext context) { }
|
||||
protected virtual void Exported(ExportContentContext context) { }
|
||||
|
||||
protected virtual void RegisterIdentityResolver(RegisterIdentityResolverContext context) { }
|
||||
protected virtual void GetItemMetadata(GetContentItemMetadataContext context) { }
|
||||
protected virtual void BuildDisplayShape(BuildDisplayContext context) { }
|
||||
protected virtual void BuildEditorShape(BuildEditorContext context) { }
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
public virtual void Exporting(ExportContentContext context) {}
|
||||
public virtual void Exported(ExportContentContext context) {}
|
||||
|
||||
public virtual void RegisterIdentityResolver(RegisterIdentityResolverContext context) { }
|
||||
public virtual void GetContentItemMetadata(GetContentItemMetadataContext context) {}
|
||||
public virtual void BuildDisplay(BuildDisplayContext context) {}
|
||||
public virtual void BuildEditor(BuildEditorContext context) {}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
void Exporting(ExportContentContext context);
|
||||
void Exported(ExportContentContext context);
|
||||
|
||||
void RegisterIdentityResolver(RegisterIdentityResolverContext context);
|
||||
void GetContentItemMetadata(GetContentItemMetadataContext context);
|
||||
void BuildDisplay(BuildDisplayContext context);
|
||||
void BuildEditor(BuildEditorContext context);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Orchard.ContentManagement.Handlers {
|
||||
public class RegisterIdentityResolverContext {
|
||||
private readonly IList<Tuple<Func<ContentIdentity, bool>, Func<ContentIdentity, ContentItem>>> _resolvers;
|
||||
|
||||
public RegisterIdentityResolverContext() {
|
||||
_resolvers = new List<Tuple<Func<ContentIdentity, bool>, Func<ContentIdentity, ContentItem>>>();
|
||||
}
|
||||
|
||||
public void Register(Func<ContentIdentity, bool> isResolverForIdentity, Func<ContentIdentity, ContentItem> resolveIdentity) {
|
||||
_resolvers.Add(new Tuple<Func<ContentIdentity, bool>, Func<ContentIdentity, ContentItem>>(
|
||||
isResolverForIdentity, resolveIdentity));
|
||||
}
|
||||
|
||||
public bool HasResolverForIdentity(ContentIdentity identity) {
|
||||
return _resolvers.Any(r => r.Item1(identity));
|
||||
}
|
||||
|
||||
public ContentItem ResolveIdentity(ContentIdentity identity) {
|
||||
return _resolvers.Where(r => r.Item1(identity))
|
||||
.Select(r => r.Item2(identity))
|
||||
.FirstOrDefault(r => r != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,8 @@ namespace Orchard.ContentManagement {
|
||||
GroupInfo GetEditorGroupInfo(IContent contentItem, string groupInfoId);
|
||||
GroupInfo GetDisplayGroupInfo(IContent contentItem, string groupInfoId);
|
||||
|
||||
bool HasResolverForIdentity(ContentIdentity contentIdentity);
|
||||
ContentItem ResolveIdentity(ContentIdentity contentIdentity);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the display shape of the specified content item
|
||||
|
||||
@@ -6,30 +6,102 @@ namespace Orchard.ContentManagement {
|
||||
// Maps content identities to content items on the importer.
|
||||
public class ImportContentSession {
|
||||
private readonly IContentManager _contentManager;
|
||||
private const int BulkPage = 128;
|
||||
private int _lastIndex = 0;
|
||||
private readonly ContentIdentity.ContentIdentityEqualityComparer _identityComparer;
|
||||
|
||||
private readonly Dictionary<ContentIdentity, int> _identities;
|
||||
private readonly Dictionary<int, ContentIdentity> _contentItemIds;
|
||||
private readonly Dictionary<ContentIdentity, string> _contentTypes;
|
||||
private readonly Dictionary<int, int> _draftVersionRecordIds;
|
||||
|
||||
//for batching
|
||||
private readonly List<ContentIdentity> _allIdentitiesForImport; //List to maintain order
|
||||
private readonly Dictionary<ContentIdentity, bool> _allIdentitiesForImportStatus; //For fast lookup of status
|
||||
private readonly Queue<ContentIdentity> _dependencyIdentities;
|
||||
private int _startIndex;
|
||||
private int _batchSize = int.MaxValue;
|
||||
private int _currentIndex;
|
||||
|
||||
//for identity prefetch
|
||||
private const int BulkPage = 128;
|
||||
private bool _firstRequest = true;
|
||||
private bool _allIdentitiesPrefetched = false;
|
||||
|
||||
public ImportContentSession(IContentManager contentManager) {
|
||||
_identityComparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
_contentManager = contentManager;
|
||||
_identities = new Dictionary<ContentIdentity, int>(new ContentIdentity.ContentIdentityEqualityComparer());
|
||||
_contentItemIds = new Dictionary<int, ContentIdentity>();
|
||||
_contentTypes = new Dictionary<ContentIdentity, string>(new ContentIdentity.ContentIdentityEqualityComparer());
|
||||
|
||||
_identities = new Dictionary<ContentIdentity, int>(_identityComparer);
|
||||
_contentTypes = new Dictionary<ContentIdentity, string>(_identityComparer);
|
||||
_draftVersionRecordIds = new Dictionary<int, int>();
|
||||
|
||||
_allIdentitiesForImport = new List<ContentIdentity>();
|
||||
_allIdentitiesForImportStatus = new Dictionary<ContentIdentity, bool>(_identityComparer);
|
||||
_dependencyIdentities = new Queue<ContentIdentity>();
|
||||
}
|
||||
|
||||
public void Set(string id, string contentType) {
|
||||
var contentIdentity = new ContentIdentity(id);
|
||||
_contentTypes[contentIdentity] = contentType;
|
||||
_allIdentitiesForImport.Add(contentIdentity);
|
||||
_allIdentitiesForImportStatus[contentIdentity] = false;
|
||||
}
|
||||
|
||||
public void InitializeBatch(int startIndex, int batchSize) {
|
||||
_currentIndex = _startIndex = startIndex;
|
||||
_batchSize = batchSize;
|
||||
}
|
||||
|
||||
public ContentIdentity GetNextInBatch() {
|
||||
ContentIdentity nextIdentity;
|
||||
|
||||
//always process identified dependencies regardless of batch size
|
||||
//so that they are within the same transaction
|
||||
if (_dependencyIdentities.Any()) {
|
||||
nextIdentity = _dependencyIdentities.Dequeue();
|
||||
_allIdentitiesForImportStatus[nextIdentity] = true;
|
||||
return nextIdentity;
|
||||
}
|
||||
|
||||
//check if the item has already been imported (e.g. as a dependency)
|
||||
while (_currentIndex < _allIdentitiesForImport.Count &&
|
||||
_allIdentitiesForImportStatus[_allIdentitiesForImport[_currentIndex]]) {
|
||||
_currentIndex++;
|
||||
}
|
||||
|
||||
if (_currentIndex < _startIndex + _batchSize && //within batch
|
||||
_currentIndex < _allIdentitiesForImport.Count) //still items to import
|
||||
{
|
||||
nextIdentity = _allIdentitiesForImport[_currentIndex];
|
||||
_allIdentitiesForImportStatus[nextIdentity] = true;
|
||||
_currentIndex++;
|
||||
return nextIdentity;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContentItem Get(string id, string contentTypeHint = null) {
|
||||
return Get(id, VersionOptions.Latest, contentTypeHint);
|
||||
}
|
||||
|
||||
public ContentItem Get(string id, VersionOptions versionOptions, string contentTypeHint = null) {
|
||||
var contentIdentity = new ContentIdentity(id);
|
||||
|
||||
if (_firstRequest) {
|
||||
_firstRequest = false;
|
||||
//If we know we have identities without a resolver
|
||||
//just load all up-front
|
||||
if (HasIdentitiesSetWithoutResolver()) {
|
||||
PrefetchAllIdentities(true);
|
||||
}
|
||||
}
|
||||
|
||||
// lookup in local cache
|
||||
if (_identities.ContainsKey(contentIdentity)) {
|
||||
var result = _contentManager.Get(_identities[contentIdentity], VersionOptions.DraftRequired);
|
||||
if (_draftVersionRecordIds.ContainsKey(_identities[contentIdentity])) {
|
||||
//draft was previously created. Recall.
|
||||
versionOptions = VersionOptions.VersionRecord(_draftVersionRecordIds[_identities[contentIdentity]]);
|
||||
}
|
||||
var result = _contentManager.Get(_identities[contentIdentity], versionOptions);
|
||||
|
||||
// if two identities are conflicting, then ensure that there types are the same
|
||||
// e.g., importing a blog as home page (alias=) and the current home page is a page, the blog
|
||||
@@ -39,57 +111,92 @@ namespace Orchard.ContentManagement {
|
||||
}
|
||||
}
|
||||
|
||||
// no result ? then check if there are some more content items to load from the db
|
||||
if(_lastIndex != int.MaxValue) {
|
||||
|
||||
var equalityComparer = new ContentIdentity.ContentIdentityEqualityComparer();
|
||||
IEnumerable<ContentItem> block;
|
||||
|
||||
// load identities in blocks
|
||||
while ((block = _contentManager.HqlQuery()
|
||||
.ForVersion(VersionOptions.Latest)
|
||||
.OrderBy(x => x.ContentItemVersion(), x => x.Asc("Id"))
|
||||
.Slice(_lastIndex, BulkPage)).Any()) {
|
||||
if (!_allIdentitiesPrefetched) {
|
||||
ContentItem existingItem = null;
|
||||
|
||||
foreach (var item in block) {
|
||||
_lastIndex++;
|
||||
//try retrieve the item using handlers to resolve, otherwise fall back to full scan
|
||||
if (_contentManager.HasResolverForIdentity(contentIdentity)) {
|
||||
existingItem = _contentManager.ResolveIdentity(contentIdentity);
|
||||
|
||||
// ignore content item if it has already been imported
|
||||
if (_contentItemIds.ContainsKey(item.Id)) {
|
||||
continue;
|
||||
}
|
||||
//ensure we have the correct version
|
||||
if (existingItem != null)
|
||||
existingItem = _contentManager.Get(existingItem.Id, versionOptions);
|
||||
}
|
||||
else {
|
||||
//may be a contentidentity without a resolver and all identities have not
|
||||
//been prefetched yet e.g. is a dependency without resolver.
|
||||
PrefetchAllIdentities(false);
|
||||
if (_identities.ContainsKey(contentIdentity))
|
||||
existingItem = _contentManager.Get(_identities[contentIdentity], versionOptions);
|
||||
}
|
||||
|
||||
var identity = _contentManager.GetItemMetadata(item).Identity;
|
||||
if (existingItem != null) {
|
||||
_identities[contentIdentity] = existingItem.Id;
|
||||
if (versionOptions.IsDraftRequired) {
|
||||
_draftVersionRecordIds[existingItem.Id] = existingItem.VersionRecord.Id;
|
||||
}
|
||||
return existingItem;
|
||||
}
|
||||
}
|
||||
|
||||
// ignore content item if the same identity is already present
|
||||
if (_identities.ContainsKey(identity)) {
|
||||
continue;
|
||||
}
|
||||
//create item if not found and draft was requested, or it is found later in the import queue
|
||||
if (versionOptions.IsDraftRequired || _allIdentitiesForImportStatus.ContainsKey(contentIdentity)) {
|
||||
var contentType = _contentTypes.ContainsKey(contentIdentity) ? _contentTypes[contentIdentity] : contentTypeHint;
|
||||
|
||||
_identities.Add(identity, item.Id);
|
||||
_contentItemIds.Add(item.Id, identity);
|
||||
|
||||
if (equalityComparer.Equals(identity, contentIdentity)) {
|
||||
return _contentManager.Get(item.Id, VersionOptions.DraftRequired);
|
||||
}
|
||||
}
|
||||
if (!_contentTypes.ContainsKey(contentIdentity)) {
|
||||
throw new ArgumentException("Unknown content type for " + id);
|
||||
}
|
||||
|
||||
var contentItem = _contentManager.Create(contentType, VersionOptions.Draft);
|
||||
|
||||
_identities[contentIdentity] = contentItem.Id;
|
||||
|
||||
//store versionrecordid in case a draft is requested again
|
||||
_draftVersionRecordIds[contentItem.Id] = contentItem.VersionRecord.Id;
|
||||
|
||||
//add the requested item as a dependency if it is not the currently running item
|
||||
if (_allIdentitiesForImportStatus.ContainsKey(contentIdentity) &&
|
||||
!_allIdentitiesForImportStatus[contentIdentity]) {
|
||||
_dependencyIdentities.Enqueue(contentIdentity);
|
||||
}
|
||||
|
||||
return contentItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasIdentitiesSetWithoutResolver() {
|
||||
return _allIdentitiesForImport.Any(id => !_contentManager.HasResolverForIdentity(id));
|
||||
}
|
||||
|
||||
private void PrefetchAllIdentities(bool clearContentManager) {
|
||||
if (_allIdentitiesPrefetched)
|
||||
return;
|
||||
|
||||
IEnumerable<ContentItem> block;
|
||||
int lastIndex = 0;
|
||||
|
||||
while ((block = _contentManager.HqlQuery()
|
||||
.ForVersion(VersionOptions.Latest)
|
||||
.OrderBy(x => x.ContentItemVersion(), x => x.Asc("Id"))
|
||||
.Slice(lastIndex, BulkPage)).Any()) {
|
||||
foreach (var item in block) {
|
||||
lastIndex++;
|
||||
|
||||
var identity = _contentManager.GetItemMetadata(item).Identity;
|
||||
|
||||
// store mapping for later
|
||||
_identities[identity] = item.Id;
|
||||
}
|
||||
|
||||
//Clearing the ContentManger after import has started can cause errors
|
||||
if (clearContentManager) {
|
||||
_contentManager.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
_lastIndex = int.MaxValue;
|
||||
|
||||
if(!_contentTypes.ContainsKey(contentIdentity)) {
|
||||
throw new ArgumentException("Unknown content type for " + id);
|
||||
|
||||
}
|
||||
|
||||
var contentItem = _contentManager.Create(_contentTypes[contentIdentity], VersionOptions.Draft);
|
||||
_identities[contentIdentity] = contentItem.Id;
|
||||
_contentItemIds[contentItem.Id] = contentIdentity;
|
||||
|
||||
return contentItem;
|
||||
_allIdentitiesPrefetched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
<Compile Include="ContentManagement\Aspects\ILocalizableAspect.cs" />
|
||||
<Compile Include="ContentManagement\ContentIdentity.cs" />
|
||||
<Compile Include="ContentManagement\DefaultHqlQuery.cs" />
|
||||
<Compile Include="ContentManagement\Handlers\RegisterIdentityResolverContext.cs" />
|
||||
<Compile Include="ContentManagement\Handlers\UpdateContentContext.cs" />
|
||||
<Compile Include="ContentManagement\IHqlExpression.cs" />
|
||||
<Compile Include="ContentManagement\IHqlQuery.cs" />
|
||||
@@ -264,6 +265,7 @@
|
||||
<Compile Include="Mvc\Spooling\HtmlStringWriter.cs" />
|
||||
<Compile Include="Mvc\ViewEngines\Razor\IRazorCompilationEvents.cs" />
|
||||
<Compile Include="OrchardFatalException.cs" />
|
||||
<Compile Include="Recipes\Events\IRecipeExecuteEventHandler.cs" />
|
||||
<Compile Include="Recipes\Events\IRecipeSchedulerEventHandler.cs" />
|
||||
<Compile Include="Recipes\Models\Recipe.cs" />
|
||||
<Compile Include="Recipes\Models\RecipeContext.cs" />
|
||||
|
||||
12
src/Orchard/Recipes/Events/IRecipeExecuteEventHandler.cs
Normal file
12
src/Orchard/Recipes/Events/IRecipeExecuteEventHandler.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Orchard.Events;
|
||||
using Orchard.Recipes.Models;
|
||||
|
||||
namespace Orchard.Recipes.Events {
|
||||
public interface IRecipeExecuteEventHandler : IEventHandler {
|
||||
void ExecutionStart(string executionId, Recipe recipe);
|
||||
void RecipeStepExecuting(string executionId, RecipeContext context);
|
||||
void RecipeStepExecuted(string executionId, RecipeContext context);
|
||||
void ExecutionComplete(string executionId);
|
||||
void ExecutionFailed(string executionId);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.Recipes.Models {
|
||||
public class Recipe {
|
||||
@@ -7,6 +8,7 @@ namespace Orchard.Recipes.Models {
|
||||
public string Author { get; set; }
|
||||
public string WebSite { get; set; }
|
||||
public string Version { get; set; }
|
||||
public DateTime? ExportUtc { get; set; }
|
||||
public string Tags { get; set; }
|
||||
public IEnumerable<RecipeStep> RecipeSteps { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user