Skip to content

Commit

Permalink
#238 ability to extend Migration Tool
Browse files Browse the repository at this point in the history
  • Loading branch information
tkrch committed Oct 6, 2024
1 parent f905fce commit 16bbfd3
Show file tree
Hide file tree
Showing 26 changed files with 1,449 additions and 423 deletions.
7 changes: 7 additions & 0 deletions KVA/Migration.Toolkit.Source/Contexts/SourceObjectContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using CMS.FormEngine;
using Migration.Toolkit.KXP.Api.Services.CmsClass;
using Migration.Toolkit.Source.Model;

namespace Migration.Toolkit.Source.Contexts;

public record DocumentSourceObjectContext(ICmsTree CmsTree, ICmsClass NodeClass, ICmsSite Site, FormInfo OldFormInfo, FormInfo NewFormInfo, int? DocumentId) : ISourceObjectContext;
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
using CMS.ContentEngine;
using CMS.DataEngine;

using CMS.FormEngine;
using MediatR;

using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;

using Migration.Toolkit.Common;
using Migration.Toolkit.Common.Abstractions;
using Migration.Toolkit.Common.Builders;
using Migration.Toolkit.Common.Helpers;
using Migration.Toolkit.Common.MigrationProtocol;
using Migration.Toolkit.KXP.Api;
using Migration.Toolkit.KXP.Api.Services.CmsClass;
using Migration.Toolkit.KXP.Models;
using Migration.Toolkit.Source.Contexts;
using Migration.Toolkit.Source.Helpers;
using Migration.Toolkit.Source.Mappers;
using Migration.Toolkit.Source.Model;
using Migration.Toolkit.Source.Services;

Expand All @@ -28,8 +31,11 @@ public class MigratePageTypesCommandHandler(
ToolkitConfiguration toolkitConfiguration,
ModelFacade modelFacade,
PageTemplateMigrator pageTemplateMigrator,
ReusableSchemaService reusableSchemaService
)
ReusableSchemaService reusableSchemaService,
IEnumerable<IClassMapping> classMappings,
IFieldMigrationService fieldMigrationService,
IEnumerable<IReusableSchemaBuilder> reusableSchemaBuilders
)
: IRequestHandler<MigratePageTypesCommand, CommandResult>
{
private const string CLASS_CMS_ROOT = "CMS.Root";
Expand All @@ -42,11 +48,141 @@ public async Task<CommandResult> Handle(MigratePageTypesCommand request, Cancell
modelFacade.Select<ICmsClass>("ClassIsDocumentType=1", "ClassID")
.OrderBy(x => x.ClassID)
);

ExecReusableSchemaBuilders();

var manualMappings = new Dictionary<string, DataClassInfo>();
foreach (var classMapping in classMappings)
{
var newDt = DataClassInfoProvider.GetDataClassInfo(classMapping.TargetClassName) ?? DataClassInfo.New();
classMapping.PatchTargetDataClass(newDt);

// might not need ClassGUID
// newDt.ClassGUID = GuidHelper.CreateDataClassGuid($"{newDt.ClassName}|{newDt.ClassTableName}");

var cmsClasses = new List<ICmsClass>();
foreach (string sourceClassName in classMapping.SourceClassNames)
{
cmsClasses.AddRange(modelFacade.SelectWhere<ICmsClass>("ClassName=@className", new SqlParameter("className", sourceClassName)));
}

var nfi = string.IsNullOrWhiteSpace(newDt.ClassFormDefinition) ? new FormInfo() : new FormInfo(newDt.ClassFormDefinition);
bool hasPrimaryKey = false;
foreach (var formFieldInfo in nfi.GetFields(true, true, true, true, false))
{
if (formFieldInfo.PrimaryKey)
{
hasPrimaryKey = true;
}
}

if (!hasPrimaryKey)
{
if (string.IsNullOrWhiteSpace(classMapping.PrimaryKey))
{
throw new InvalidOperationException($"Class mapping has no primary key set");
}
else
{
var prototype = FormHelper.GetBasicFormDefinition(classMapping.PrimaryKey);
nfi.AddFormItem(prototype.GetFormField(classMapping.PrimaryKey));
}
}

newDt.ClassFormDefinition = nfi.GetXmlDefinition();

foreach (string schemaName in classMapping.ReusableSchemaNames)
{
reusableSchemaService.AddReusableSchemaToDataClass(newDt, schemaName);
}

nfi = new FormInfo(newDt.ClassFormDefinition);

var fieldInReusableSchemas = reusableSchemaService.GetFieldsFromReusableSchema(newDt).ToDictionary(x => x.Name, x => x);

bool hasFieldsAlready = true;
foreach (var cmml in classMapping.Mappings.Where(m => m.IsTemplate).ToLookup(x => x.SourceFieldName))
{
var cmm = cmml.FirstOrDefault() ?? throw new InvalidOperationException();
if (fieldInReusableSchemas.ContainsKey(cmm.TargetFieldName))
{
// part of reusable schema
continue;
}

var sc = cmsClasses.FirstOrDefault(sc => sc.ClassName.Equals(cmm.SourceClassName, StringComparison.InvariantCultureIgnoreCase))
?? throw new NullReferenceException($"The source class '{cmm.SourceClassName}' does not exist - wrong mapping {classMapping}");

var fi = new FormInfo(sc.ClassFormDefinition);
if(nfi.GetFormField(cmm.TargetFieldName) is {})
{
}
else
{
var src = fi.GetFormField(cmm.SourceFieldName);
src.Name = cmm.TargetFieldName;
nfi.AddFormItem(src);
hasFieldsAlready = false;
}
}

if (!hasFieldsAlready)
{
FormDefinitionHelper.MapFormDefinitionFields(logger, fieldMigrationService, nfi.GetXmlDefinition(), false, true, newDt, false, false);
CmsClassMapper.PatchDataClassInfo(newDt, out _, out _);
}

if (classMapping.TargetFieldPatchers.Count > 0)
{
nfi = new FormInfo(newDt.ClassFormDefinition);
foreach (string fieldName in classMapping.TargetFieldPatchers.Keys)
{
classMapping.TargetFieldPatchers[fieldName].Invoke(nfi.GetFormField(fieldName));
}

newDt.ClassFormDefinition = nfi.GetXmlDefinition();
}

DataClassInfoProvider.SetDataClassInfo(newDt);
foreach (var gByClass in classMapping.Mappings.GroupBy(x => x.SourceClassName))
{
manualMappings.TryAdd(gByClass.Key, newDt);
}

foreach (string sourceClassName in classMapping.SourceClassNames)
{
var sourceClass = cmsClasses.First(c => c.ClassName.Equals(sourceClassName, StringComparison.InvariantCultureIgnoreCase));
foreach (var cmsClassSite in modelFacade.SelectWhere<ICmsClassSite>("ClassId = @classId", new SqlParameter("classId", sourceClass.ClassID)))
{
if (modelFacade.SelectById<ICmsSite>(cmsClassSite.SiteID) is { SiteGUID: var siteGuid })
{
if (ChannelInfoProvider.ProviderObject.Get(siteGuid) is { ChannelID: var channelId })
{
var info = new ContentTypeChannelInfo { ContentTypeChannelChannelID = channelId, ContentTypeChannelContentTypeID = newDt.ClassID };
ContentTypeChannelInfoProvider.ProviderObject.Set(info);
}
else
{
logger.LogWarning("Channel for site with SiteGUID '{SiteGuid}' not found", siteGuid);
}
}
else
{
logger.LogWarning("Source site with SiteID '{SiteId}' not found", cmsClassSite.SiteID);
}
}
}
}

while (ksClasses.GetNext(out var di))
{
var (_, ksClass) = di;

if (manualMappings.ContainsKey(ksClass.ClassName))
{
continue;
}

if (ksClass.ClassInheritsFromClassID is { } classInheritsFromClassId && !primaryKeyMappingContext.HasMapping<ICmsClass>(c => c.ClassID, classInheritsFromClassId))
{
// defer migration to later stage
Expand Down Expand Up @@ -130,6 +266,61 @@ public async Task<CommandResult> Handle(MigratePageTypesCommand request, Cancell
return new GenericCommandResult();
}

private void ExecReusableSchemaBuilders()
{
foreach (var reusableSchemaBuilder in reusableSchemaBuilders)
{
reusableSchemaBuilder.AssertIsValid();
var fieldInfos = reusableSchemaBuilder.FieldBuilders.Select(fb =>
{
switch (fb)
{
case { Factory: { } factory }:
{
return factory();
}
case { SourceFieldIdentifier: { } fieldIdentifier }:
{
var sourceClass = modelFacade.SelectWhere<ICmsClass>("ClassName=@className", new SqlParameter("className", fieldIdentifier.ClassName)).SingleOrDefault()
?? throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': DataClass not found, class name '{fieldIdentifier.ClassName}'");

if (string.IsNullOrWhiteSpace(sourceClass.ClassFormDefinition))
{
throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': Class '{fieldIdentifier.ClassName}' is missing field '{fieldIdentifier.FieldName}'");
}

// this might be cached as optimization
var patcher = new FormDefinitionPatcher(
logger,
sourceClass.ClassFormDefinition,
fieldMigrationService,
sourceClass.ClassIsForm.GetValueOrDefault(false),
sourceClass.ClassIsDocumentType,
true,
false
);

patcher.PatchFields();
patcher.RemoveCategories();

var fi = new FormInfo(patcher.GetPatched());
return fi.GetFormField(fieldIdentifier.FieldName) switch
{
{ } field => field,
_ => throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fieldIdentifier.ClassName}': Class '{fieldIdentifier.ClassName}' is missing field '{fieldIdentifier.FieldName}'")
};
}
default:
{
throw new InvalidOperationException($"Invalid reusable schema field builder for field '{fb.TargetFieldName}'");
}
}
});

reusableSchemaService.EnsureReusableFieldSchema(reusableSchemaBuilder.SchemaName, reusableSchemaBuilder.SchemaDisplayName, reusableSchemaBuilder.SchemaDescription, fieldInfos.ToArray());
}
}

private async Task MigratePageTemplateConfigurations()
{
if (modelFacade.IsAvailable<ICmsPageTemplateConfiguration>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public class MigratePagesCommandHandler(
ModelFacade modelFacade,
DeferredPathService deferredPathService,
SpoiledGuidContext spoiledGuidContext,
SourceInstanceContext sourceInstanceContext
SourceInstanceContext sourceInstanceContext,
ClassMappingProvider classMappingProvider
)
: IRequestHandler<MigratePagesCommand, CommandResult>
{
Expand Down Expand Up @@ -194,7 +195,11 @@ public async Task<CommandResult> Handle(MigratePagesCommand request, Cancellatio
? (Guid?)null
: spoiledGuidContext.EnsureNodeGuid(ksNodeParent);

var targetClass = DataClassInfoProvider.ProviderObject.Get(ksNodeClass.ClassGUID);
DataClassInfo targetClass = null!;
var classMapping = classMappingProvider.GetMapping(ksNodeClass.ClassName);
targetClass = classMapping != null
? DataClassInfoProvider.ProviderObject.Get(classMapping.TargetClassName)
: DataClassInfoProvider.ProviderObject.Get(ksNodeClass.ClassGUID);

var results = mapper.Map(new CmsTreeMapperSource(
ksNode,
Expand Down
76 changes: 76 additions & 0 deletions KVA/Migration.Toolkit.Source/Helpers/FormDefinitionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using CMS.DataEngine;
using CMS.FormEngine;
using Microsoft.Extensions.Logging;
using Migration.Toolkit.KXP.Api.Services.CmsClass;
using Migration.Toolkit.Source.Model;

namespace Migration.Toolkit.Source.Helpers;

public static class FormDefinitionHelper
{
public static void MapFormDefinitionFields(ILogger logger, IFieldMigrationService fieldMigrationService, ICmsClass source, DataClassInfo target, bool isCustomizableSystemClass, bool classIsCustom)
{
if (!string.IsNullOrWhiteSpace(source.ClassFormDefinition))
{
var patcher = new FormDefinitionPatcher(
logger,
source.ClassFormDefinition,
fieldMigrationService,
source.ClassIsForm.GetValueOrDefault(false),
source.ClassIsDocumentType,
isCustomizableSystemClass,
classIsCustom
);

patcher.PatchFields();
patcher.RemoveCategories(); // TODO tk: 2022-10-11 remove when supported

string? result = patcher.GetPatched();
if (isCustomizableSystemClass)
{
result = FormHelper.MergeFormDefinitions(target.ClassFormDefinition, result);
}

var formInfo = new FormInfo(result);
target.ClassFormDefinition = formInfo.GetXmlDefinition();
}
else
{
target.ClassFormDefinition = new FormInfo().GetXmlDefinition();
}
}

public static void MapFormDefinitionFields(ILogger logger, IFieldMigrationService fieldMigrationService,
string sourceClassDefinition, bool? classIsForm, bool classIsDocumentType,
DataClassInfo target, bool isCustomizableSystemClass, bool classIsCustom)
{
if (!string.IsNullOrWhiteSpace(sourceClassDefinition))
{
var patcher = new FormDefinitionPatcher(
logger,
sourceClassDefinition,
fieldMigrationService,
classIsForm.GetValueOrDefault(false),
classIsDocumentType,
isCustomizableSystemClass,
classIsCustom
);

patcher.PatchFields();
patcher.RemoveCategories(); // TODO tk: 2022-10-11 remove when supported

string? result = patcher.GetPatched();
if (isCustomizableSystemClass)
{
result = FormHelper.MergeFormDefinitions(target.ClassFormDefinition, result);
}

var formInfo = new FormInfo(result);
target.ClassFormDefinition = formInfo.GetXmlDefinition();
}
else
{
target.ClassFormDefinition = new FormInfo().GetXmlDefinition();
}
}
}
2 changes: 2 additions & 0 deletions KVA/Migration.Toolkit.Source/KsCoreDiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Migration.Toolkit.Source.Helpers;
using Migration.Toolkit.Source.Mappers;
using Migration.Toolkit.Source.Model;
using Migration.Toolkit.Source.Providers;
using Migration.Toolkit.Source.Services;

namespace Migration.Toolkit.Source;
Expand Down Expand Up @@ -52,6 +53,7 @@ public static IServiceCollection UseKsToolkitCore(this IServiceCollection servic
services.AddSingleton<IdentityLocator>();
services.AddSingleton<IAssetFacade, AssetFacade>();
services.AddSingleton<MediaLinkServiceFactory>();
services.AddSingleton<ClassMappingProvider>();

services.AddTransient<BulkDataCopyService>();
services.AddTransient<CmsRelationshipService>();
Expand Down
Loading

0 comments on commit 16bbfd3

Please sign in to comment.