Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide ability to handle multiple custom fields with the same name. #973

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/WorkItemMigrator/JiraExport/IJiraProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ public interface IJiraProvider

bool GetCustomFieldSerializer(string customType, out ICustomFieldValueSerializer serializer);

/// <inheritdoc cref="JiraProvider.GetCustomId"/>
string GetCustomId(string propertyName);
/// <inheritdoc cref="JiraProvider.GetCustomIdList"/>
List<string> GetCustomIdList(string propertyName);

Task<List<RevisionAction<JiraAttachment>>> DownloadAttachments(JiraRevision rev);

IEnumerable<JObject> GetCommitRepositories(string issueId);
Expand Down
9 changes: 6 additions & 3 deletions src/WorkItemMigrator/JiraExport/JiraMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -381,16 +381,19 @@ private HashSet<string> InitializeTypeMappings()
return types;
}

private Func<JiraRevision, (bool, object)> IfChanged<T>(string sourceField, bool isCustomField, Func<T, object> mapperFunc = null)
internal Func<JiraRevision, (bool, object)> IfChanged<T>(string sourceField, bool isCustomField, Func<T, object> mapperFunc = null)
{
List<string> sourceFields = null;
if (isCustomField)
{
sourceField = _jiraProvider.GetCustomId(sourceField) ?? sourceField;
sourceFields = _jiraProvider.GetCustomIdList(sourceField);
}

sourceFields = sourceFields ?? new List<string> { sourceField };

return (r) =>
{
if (r.Fields.TryGetValue(sourceField.ToLower(), out object value))
if (r.Fields.TryGetFirstValue(sourceFields, out object value))
{
if (mapperFunc != null)
{
Expand Down
59 changes: 42 additions & 17 deletions src/WorkItemMigrator/JiraExport/JiraProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,30 @@ public string GetUserEmail(string usernameOrAccountId)
}
}

/// <summary>
/// Return the custom field id corresponding to the given field/property name.
/// </summary>
/// <remarks>Blindly chooses the first matching value. This is often right, but doesn't handle the scenario where a
/// Jira instance has multiple custom fields with the same name. To handle that scenario, use <see cref="GetCustomIdList"/> instead.</remarks>
public string GetCustomId(string propertyName)
{
var customId = string.Empty;
var allIds = GetCustomIdList(propertyName);
var customId = allIds.FirstOrDefault();
if (allIds?.Count() > 1)
{
Logger.Log(LogLevel.Warning, $"Multiple fields found for {propertyName}. Selecting {customId}.");
}

return customId;
}

/// <summary>
/// Returns a list of custom field ids corresponding to the given field/property name.
/// </summary>
/// <param name="propertyName">The property/field name of the custom field.</param>
/// <returns>One or more custom field ids that match the name provided.</returns>
public List<string> GetCustomIdList(string propertyName)
{
JArray response = null;

if (JiraNameFieldCache == null)
Expand All @@ -423,19 +444,24 @@ public string GetCustomId(string propertyName)
JiraNameFieldCache = CreateFieldCacheLookup(response, "name", "id");
}

customId = GetItemFromFieldCache(propertyName, JiraNameFieldCache);
var customIds = GetItemListFromFieldCache(propertyName, JiraNameFieldCache);

if (string.IsNullOrEmpty(customId))
if (!customIds.Any())
{
if (JiraKeyFieldCache == null)
{
response = response ?? (JArray)_jiraServiceWrapper.RestClient.ExecuteRequestAsync(Method.GET, $"{JiraApiV2}/field").Result;
JiraKeyFieldCache = CreateFieldCacheLookup(response, "key", "id");
}
customId = GetItemFromFieldCache(propertyName, JiraKeyFieldCache);
customIds = GetItemListFromFieldCache(propertyName, JiraKeyFieldCache);
}

if (customIds.Count == 0)
{
Logger.Log(LogLevel.Warning, $"Custom field {propertyName} could not be found.");
}

return customId;
return customIds;
}

private ILookup<string, string> CreateFieldCacheLookup(JArray response, string key, string value)
Expand All @@ -446,19 +472,18 @@ private ILookup<string, string> CreateFieldCacheLookup(JArray response, string k
.ToLookup(l => l.key, l => l.value);
}

private string GetItemFromFieldCache(string propertyName, ILookup<string, string> cache)
/// <summary>
/// Returns a list of cache values corresponding to the given name.
/// </summary>
/// <param name="propertyName">The name to lookup in the cache.</param>
/// <param name="cache">The cache to be interrogated.</param>
/// <returns></returns>
private List<string> GetItemListFromFieldCache(string propertyName, ILookup<string, string> cache)
{
string customId = null;
var query = cache.FirstOrDefault(x => x.Key.Equals(propertyName.ToLower()));
if (query != null)
{
customId = query.Any() ? query.First() : null;
if (query.Count() > 1)
{
Logger.Log(LogLevel.Warning, $"Multiple fields found for {propertyName}. Selecting {customId}.");
}
}
return customId;
var fieldList = cache.FirstOrDefault(x => x.Key.Equals(propertyName.ToLower()))?.ToList()
?? new List<string>(0);

return fieldList;
}

public IEnumerable<JObject> GetCommitRepositories(string issueId)
Expand Down
25 changes: 25 additions & 0 deletions src/WorkItemMigrator/JiraExport/RevisionUtils/FieldMapperUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

[assembly: InternalsVisibleTo("Migration.Jira-Export.Tests")]


namespace JiraExport
{
public static class FieldMapperUtils
Expand Down Expand Up @@ -183,6 +187,27 @@ public static object MapSprint(string iterationPathsString)
return iterationPath;
}

/// <summary>
/// Find the first custom field with the right name that has a value.
/// </summary>
/// <param name="fields"><see cref="JiraRevision.Fields">JiraRevision.Fields</see> dictionary.</param>
/// <param name="customFieldIds">The IDs of the Custom Fields to search - more than one custom field can have the same name.</param>
/// <param name="fieldValueObject">The value of the first field in the list that has a value. Null if no values are found.</param>
/// <returns>True if one of the identified custom fields contains a value.</returns>
internal static bool TryGetFirstValue(this Dictionary<string,object> fields, List<string> customFieldIds, out object fieldValueObject)
{
foreach(var name in customFieldIds)
{
if (fields.TryGetValue(name, out fieldValueObject) && fieldValueObject != null)
{
return true;
}
}

fieldValueObject = null;
return false;
}

private static readonly Dictionary<string, decimal> CalculatedLexoRanks = new Dictionary<string, decimal>();
private static readonly Dictionary<decimal, string> CalculatedRanks = new Dictionary<decimal, string>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
Expand Down Expand Up @@ -320,5 +321,107 @@ private JiraItem CreateJiraItem()

return jiraItem;
}


/*
* ******* IfChanged<T>() ******
*/

[Test]
public void IfChanged_SourceFieldExists_ReturnsTrueAndValue()
{
// Arrange
var jiraMapper = createJiraMapper();
//var revision = Substitute.For<JiraRevision>();
var item = _fixture.Create<JiraItem>();
var revision = new JiraRevision(parentItem: null);
revision.Fields = new Dictionary<string, object>();
revision.Fields.Add("sourceField", "value");

// Act
var (boolResult, valueResult) = jiraMapper.IfChanged<string>("sourceField", false) (revision);

// Assert
Assert.IsTrue(boolResult);
Assert.AreEqual("value", valueResult);
}

[Test]
public void IfChanged_SourceFieldDoesNotExist_ReturnsFalseAndNull()
{
// Arrange
var jiraMapper = createJiraMapper();
var item = _fixture.Create<JiraItem>();
var revision = new JiraRevision(parentItem: null);
revision.Fields = new Dictionary<string, object>();

// Act
var (boolResult, valueResult) = jiraMapper.IfChanged<string>("sourceField", false) (revision);

// Assert
Assert.IsFalse(boolResult);
Assert.IsNull(valueResult);
}

[Test]
public void IfChanged_CustomFieldExists_ReturnsTrueAndValue()
{
// Arrange
var jiraMapper = createJiraMapper();
var item = _fixture.Create<JiraItem>();
var revision = new JiraRevision(parentItem: null);
revision.Fields = new Dictionary<string, object>();
revision.Fields.Add("customField", "value");

var jiraProvider = Substitute.For<IJiraProvider>();
jiraProvider.GetCustomIdList("customField").Returns(new List<string> { "customField" });

// Act
var (boolResult, valueResult) = jiraMapper.IfChanged<string>("customField", true) (revision);

// Assert
Assert.IsTrue(boolResult);
Assert.AreEqual("value", valueResult);
}

[Test]
public void IfChanged_CustomFieldDoesNotExist_ReturnsFalseAndNull()
{
// Arrange
var jiraMapper = createJiraMapper();
var item = _fixture.Create<JiraItem>();
var revision = new JiraRevision(parentItem: null);
revision.Fields = new Dictionary<string, object>();

//_jiraProvider.GetCustomIdList("customField").Returns(new List<string> { "customField" });

// Act
var (boolResult, valueResult) = jiraMapper.IfChanged<string>("customField", true) (revision);

// Assert
Assert.IsFalse(boolResult);
Assert.IsNull(valueResult);
}

[Test]
public void IfChanged_SourceFieldExistsWithMapperFunc_ReturnsTrueAndMappedValue()
{
// Arrange
var jiraMapper = createJiraMapper();
var item = _fixture.Create<JiraItem>();
var revision = new JiraRevision(parentItem: null);
revision.Fields = new Dictionary<string, object>();
revision.Fields.Add("sourceField", 10);

// mapper takes expected 10 value and concatenates "XX" on the end.
Func<int, object> mapperFunc = (value) => value.ToString() + "XX";

// Act
var (boolResult, valueResult) = jiraMapper.IfChanged<int>("sourceField", false, mapperFunc) (revision);

// Assert
Assert.IsTrue(boolResult);
Assert.AreEqual("10XX", valueResult);
}
}
}
Loading
Loading