Skip to content

Commit

Permalink
fix: correctly update word template data bindings
Browse files Browse the repository at this point in the history
Co-authored-by: Tom Ashworth <[email protected]>
  • Loading branch information
ahmet269 and tdashworth authored Jun 28, 2021
1 parent 40a7bf9 commit 1f39c9e
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ You can import word templates by adding `<documenttemplate>` elements.
```xml
<templateconfig>
<documenttemplates>
<documenttemplate path="Word Template.docx">
<documenttemplate path="Word Template.docx"/>
</documenttemplates>
</templateconfig>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,25 +140,8 @@ public ExecuteMultipleResponse UpdateStateAndStatusForEntityInBatch(EntityCollec
}

/// <inheritdoc/>
public void ImportWordTemplate(string filePath)
public void ImportWordTemplate(FileInfo fileInfo, string entityLogicalName, OptionSetValue templateType, string filePath)
{
var fileInfo = new FileInfo(filePath);
var templateType = new OptionSetValue(fileInfo.Extension.Equals("xlsx", StringComparison.OrdinalIgnoreCase) ? Constants.DocumentTemplate.DocumentTypeExcel : Constants.DocumentTemplate.DocumentTypeWord);

if (templateType.Value != 2)
{
throw new NotSupportedException("Only Word templates (.docx) files are supported.");
}

var logicalName = WordTemplateUtilities.GetEntityLogicalName(filePath);
var targetEntityTypeCode = this.crmSvc.GetEntityTypeCode(logicalName);
var entityTypeCode = WordTemplateUtilities.GetEntityTypeCode(filePath);

if (targetEntityTypeCode != entityTypeCode)
{
WordTemplateUtilities.SetEntity(filePath, logicalName, targetEntityTypeCode);
}

var retrieveMultipleResponse = this.crmSvc.RetrieveMultiple(
new QueryByAttribute(Constants.DocumentTemplate.LogicalName) { Attributes = { Constants.DocumentTemplate.Fields.Name }, Values = { Path.GetFileNameWithoutExtension(fileInfo.Name) } });

Expand All @@ -169,7 +152,7 @@ public void ImportWordTemplate(string filePath)
documentTemplate[Constants.DocumentTemplate.Fields.Name] = Path.GetFileNameWithoutExtension(fileInfo.Name);
}

documentTemplate[Constants.DocumentTemplate.Fields.AssociatedEntityTypeCode] = logicalName;
documentTemplate[Constants.DocumentTemplate.Fields.AssociatedEntityTypeCode] = entityLogicalName;
documentTemplate[Constants.DocumentTemplate.Fields.DocumentType] = templateType;
documentTemplate[Constants.DocumentTemplate.Fields.Content] = Convert.ToBase64String(File.ReadAllBytes(filePath));

Expand Down Expand Up @@ -230,6 +213,12 @@ public Guid RetrieveAzureAdObjectIdByDomainName(string domainName)
return systemUser.GetAttributeValue<Guid>(Constants.SystemUser.Fields.AzureActiveDirectoryObjectId);
}

/// <inheritdoc/>
public string GetEntityTypeCode(string entityLogicalName)
{
return this.crmSvc.GetEntityTypeCode(entityLogicalName);
}

/// <inheritdoc/>
public TResponse Execute<TResponse>(OrganizationRequest request, string username, bool fallbackToExistingUser = true)
where TResponse : OrganizationResponse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
Expand All @@ -19,8 +20,11 @@ public interface ICrmServiceAdapter : IOrganizationService
/// <summary>
/// Imports a word template.
/// </summary>
/// <param name="fileInfo">Information about file.</param>
/// <param name="entityLogicalName">The entity schema name the document is based off.</param>
/// <param name="templateType">The template extension type.</param>
/// <param name="filePath">The path to the word template.</param>
void ImportWordTemplate(string filePath);
void ImportWordTemplate(FileInfo fileInfo, string entityLogicalName, OptionSetValue templateType, string filePath);

/// <summary>
/// Query for records based on a single field matching any of the given values.
Expand Down Expand Up @@ -97,5 +101,12 @@ public TResponse Execute<TResponse>(OrganizationRequest request, string username
/// <param name="columnSet">The columns to select.</param>
/// <returns>A collection of the component entity records.</returns>
EntityCollection RetrieveDeployedSolutionComponents(IEnumerable<string> solutions, int solutionComponentType, string componentLogicalName, ColumnSet columnSet = null);

/// <summary>
/// Retrieve solution component object IDs of a given type and solution.
/// </summary>
/// <param name="entityLogicalName">The unique name of the solution.</param>
/// <returns> EntityTypeCode for a given entity type.</returns>
string GetEntityTypeCode(string entityLogicalName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Tooling.PackageDeployment.CrmPackageExtentionBase;

/// <summary>
/// Deployment functionality related to document templates.
Expand Down Expand Up @@ -41,9 +47,91 @@ public void Import(IEnumerable<string> documentTemplates, string packageFolderPa

foreach (var docTemplate in documentTemplates)
{
this.crmSvc.ImportWordTemplate(Path.Combine(packageFolderPath, docTemplate));
this.logger.LogInformation($"{nameof(DocumentTemplateDeploymentService)}: Word template imported - {docTemplate}");
try
{
this.UpdateTemplateBindingAndImport(Path.Combine(packageFolderPath, docTemplate));
this.logger.LogInformation($"{nameof(DocumentTemplateDeploymentService)}: Word template '{docTemplate}' successfully imported.");
}
catch (Exception ex)
{
this.logger.LogError(ex, $"{nameof(DocumentTemplateDeploymentService)}: Word template '{docTemplate}' failed to import.");
}
}
}

private static string FindInWordDocument(string filePath, string regexPattern)
{
using var doc = WordprocessingDocument.Open(filePath, true, new OpenSettings { AutoSave = true });
foreach (var customXmlPart in doc.MainDocumentPart.CustomXmlParts)
{
using var sr = new StreamReader(customXmlPart.GetStream());
var match = Regex.Match(sr.ReadToEnd(), regexPattern);

if (match.Groups.Count > 1)
{
return match.Groups[1].Value;
}
}

throw new PackageDeployerException("Unable to find entity logical name and type code in template.");
}

private static void SetEntity(string filePath, string logicalName, string typeCode)
{
var pattern = @"urn:microsoft-crm/document-template/.*/\d*/";
var replace = $@"urn:microsoft-crm/document-template/{logicalName}/{typeCode}/";

using var doc = WordprocessingDocument.Open(filePath, true, new OpenSettings { AutoSave = true });

foreach (var binding in doc.MainDocumentPart.Document.Descendants<DataBinding>())
{
binding.PrefixMappings = Regex.Replace(binding.PrefixMappings, pattern, replace);
}

foreach (var wordBinding in doc.MainDocumentPart.Document.Descendants<DocumentFormat.OpenXml.Office2013.Word.DataBinding>())
{
wordBinding.PrefixMappings = Regex.Replace(wordBinding.PrefixMappings, pattern, replace);
}

foreach (var customXmlPart in doc.MainDocumentPart.CustomXmlParts)
{
using var sr = new StreamReader(customXmlPart.GetStream());
var updatedXmlPart = Regex.Replace(sr.ReadToEnd(), pattern, replace);
using var ms = new MemoryStream(Encoding.UTF8.GetBytes(updatedXmlPart));
customXmlPart.FeedData(ms);
}
}

private static string GetEntityLogicalName(string filePath)
{
return FindInWordDocument(filePath, @"urn:microsoft-crm/document-template/(.*)/\d*/");
}

private static string GetEntityTypeCode(string filePath)
{
return FindInWordDocument(filePath, @"urn:microsoft-crm/document-template/.*/(\d*)/");
}

private void UpdateTemplateBindingAndImport(string filePath)
{
var fileInfo = new FileInfo(filePath);
var templateType = new OptionSetValue(fileInfo.Extension.Equals("xlsx", StringComparison.OrdinalIgnoreCase) ? Constants.DocumentTemplate.DocumentTypeExcel : Constants.DocumentTemplate.DocumentTypeWord);

if (templateType.Value != 2)
{
throw new NotSupportedException("Only Word templates (.docx) files are supported.");
}

var logicalName = GetEntityLogicalName(filePath);
var targetEntityTypeCode = this.crmSvc.GetEntityTypeCode(logicalName);
var entityTypeCode = GetEntityTypeCode(filePath);

if (targetEntityTypeCode != entityTypeCode)
{
SetEntity(filePath, logicalName, targetEntityTypeCode);
}

this.crmSvc.ImportWordTemplate(fileInfo, logicalName, templateType, filePath);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="Resources\Word Document with Bindings - 2.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources\Word Document with Bindings.docx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
namespace Capgemini.PowerApps.PackageDeployerTemplate.UnitTests.Services
{
using System;
using System.Linq;
using System.IO;
using Capgemini.PowerApps.PackageDeployerTemplate.Adapters;
using Capgemini.PowerApps.PackageDeployerTemplate.Services;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using Moq;
using Xunit;

Expand Down Expand Up @@ -58,32 +61,57 @@ public void Import_EmptyTemplatesToImport_LogNoConfig()
}

[Fact]
public void ImportWordTemplate_ValidListOfTemplates_CallImportWordTemplate()
public void ImportWordTemplate_Should_UpdateBindingsAsExpected()
{
var workTemplatesToImport = new string[] { "word_template_one", "word_template_two" };
var random = new Random();
var targetEntityTypeCode = random.Next(0, 99999);

var workTemplatesToImport = new string[] { TestUtilities.GetResourcePath("Word Document with Bindings.docx"), TestUtilities.GetResourcePath("Word Document with Bindings - 2.docx") };
var packageFolderPath = "F:/fake_directory_to_templates/";

this.crmServiceAdapterMock.Setup(x => x.GetEntityTypeCode(It.IsAny<string>())).Returns(targetEntityTypeCode.ToString());

this.wordTemplateImporterService.Import(workTemplatesToImport, packageFolderPath);

foreach (var workTemplate in workTemplatesToImport)
{
this.loggerMock.VerifyLog(
x => x.LogInformation($"{nameof(DocumentTemplateDeploymentService)}: Word template '{workTemplate}' successfully imported."), Times.Once);

this.VerifyDataBindingUpdates(workTemplate, targetEntityTypeCode.ToString());
}

this.crmServiceAdapterMock.Verify(
x => x.ImportWordTemplate(It.IsIn(
workTemplatesToImport.Select(path => packageFolderPath + path))),
x => x.ImportWordTemplate(
It.IsAny<FileInfo>(),
It.IsAny<string>(),
It.Is<OptionSetValue>(i => i.Value.Equals(Constants.DocumentTemplate.DocumentTypeWord)),
It.IsAny<string>()),
Times.Exactly(workTemplatesToImport.Length));
}

[Fact]
public void ImportWordTemplate_ValidListOfTemplates_LogsComplete()
private void VerifyDataBindingUpdates(string documentPath, string entityTypeCode)
{
var workTemplatesToImport = new string[] { "word_template_one", "word_template_two" };
var packageFolderPath = "F:/fake_directory_to_templates/";

this.wordTemplateImporterService.Import(workTemplatesToImport, packageFolderPath);

foreach (var workTemplate in workTemplatesToImport)
using (var doc = WordprocessingDocument.Open(documentPath, true, new OpenSettings { AutoSave = true }))
{
this.loggerMock.VerifyLog(
x => x.LogInformation($"{nameof(DocumentTemplateDeploymentService)}: Word Template imported - {workTemplate}"), Times.Once);
foreach (var binding in doc.MainDocumentPart.Document.Descendants<DataBinding>())
{
Assert.Matches($"urn:microsoft-crm/document-template/.*/{entityTypeCode}/", binding.PrefixMappings.Value);
}

foreach (var repeatableBinding in doc.MainDocumentPart.Document.Descendants<DocumentFormat.OpenXml.Office2013.Word.DataBinding>())
{
Assert.Matches($"urn:microsoft-crm/document-template/.*/{entityTypeCode}/", repeatableBinding.PrefixMappings.Value);
}
}

this.crmServiceAdapterMock.Verify(
x => x.ImportWordTemplate(
It.IsAny<FileInfo>(),
It.IsAny<string>(),
It.Is<OptionSetValue>(i => i.Value.Equals(Constants.DocumentTemplate.DocumentTypeWord)),
It.Is<string>(j => j.Equals(documentPath))),
Times.Once);
}
}
}

0 comments on commit 1f39c9e

Please sign in to comment.