Added ContentIdentity resolvers, batch processing and improved performance for

large imports.

--HG--
branch : 1.x
extra : source : 3a9c242225bfe36132da7f8760711574eb5a9b43
This commit is contained in:
damoclarke
2013-03-29 18:50:12 +11:00
parent 0765f1019a
commit b3924ba1d8
28 changed files with 715 additions and 100 deletions

View File

@@ -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>();

View File

@@ -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>();

View File

@@ -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()));
}
}
}

View 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);
}
}
}

View File

@@ -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" />

View File

@@ -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));
}
}
}

View File

@@ -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() {

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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" })

View File

@@ -20,6 +20,10 @@
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>4.0</OldToolsVersion>
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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) { }

View File

@@ -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) {}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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" />

View 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);
}
}

View File

@@ -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; }
}