diff --git a/data/canonical-lf-tags.xml b/data/canonical-lf-tags.xml
new file mode 100644
index 00000000..b52d9cdd
--- /dev/null
+++ b/data/canonical-lf-tags.xml
@@ -0,0 +1,237 @@
+
+
+ -
+ ★
+ ★
+ ★
+ ★
+ ★
+ ★
+ ★
+ ★
+ star
+
+ étoile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ red
+
+ rouge
+
+
+
+
+
+ red
+
+ rouge
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ yellow
+
+ jaune
+
+
+
+
+
+ yellow
+
+ jaune
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ green
+
+ vert
+
+
+
+
+
+ green
+
+ vert
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ blue
+
+ bleu
+
+
+
+
+
+ blue
+
+ bleu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ purple
+
+ violet
+
+
+
+
+
+ purple
+
+ violet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ brown
+
+ marron
+
+
+
+
+
+ brown
+
+ marron
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ lt. gray
+
+ gris cl.
+
+
+
+
+
+ light gray
+
+ gris clair
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ dk. gray
+
+ gris f.
+
+
+
+
+
+ dark gray
+
+ gris foncé
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalItem.cs b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalItem.cs
index 4a2e7b74..9b495a2a 100644
--- a/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalItem.cs
+++ b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalItem.cs
@@ -51,6 +51,11 @@ public CanonicalItem()
ExtraData = new Dictionary();
}
+ public override string ToString()
+ {
+ return $"{Key} ({GuidStr})";
+ }
+
///
/// Given an XmlReader positioned on this node's XML representation, populate its names, abbrevs, etc. from the XML.
/// After running PopulateFromXml, the reader should be positioned just past this node's closing element.
diff --git a/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalLfTagItem.cs b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalLfTagItem.cs
new file mode 100644
index 00000000..ff321ab7
--- /dev/null
+++ b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalLfTagItem.cs
@@ -0,0 +1,62 @@
+// Copyright (c) 2016-2018 SIL International
+// This software is licensed under the MIT license (http://opensource.org/licenses/MIT)
+using System.Xml;
+using SIL.LCModel;
+
+namespace LfMerge.Core.DataConverters.CanonicalSources
+{
+ public class CanonicalLfTagItem : CanonicalItem
+ {
+ public override void PopulateFromXml(XmlReader reader)
+ {
+ if (reader.LocalName != "item" || string.IsNullOrEmpty(reader.GetAttribute("guid")))
+ return; // If we weren't on the right kind of node, do nothing
+ GuidStr = reader.GetAttribute("guid");
+ while (reader.Read())
+ {
+ switch (reader.NodeType)
+ {
+ case XmlNodeType.Element:
+ {
+ switch (reader.LocalName)
+ {
+ case "item":
+ if (!string.IsNullOrEmpty(reader.GetAttribute("id")))
+ {
+ Key = reader.GetAttribute("id");
+ }
+ break;
+ case "abbrev":
+ AddAbbrev(reader.GetAttribute("ws"), reader.ReadInnerXml());
+ break;
+ case "term":
+ AddName(reader.GetAttribute("ws"), reader.ReadInnerXml());
+ break;
+ case "def":
+ AddDescription(reader.GetAttribute("ws"), reader.ReadInnerXml());
+ break;
+ }
+ break;
+ }
+ case XmlNodeType.EndElement:
+ {
+ if (reader.LocalName == "item")
+ {
+ if (string.IsNullOrEmpty(Key)) {
+ Key = AbbrevByWs(KeyWs);
+ }
+ reader.Read(); // Skip past the closing element before returning
+ return;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ protected override void PopulatePossibilityFromExtraData(ICmPossibility poss)
+ {
+ // CanonicalLfTagItem instances don't need anything from ExtraData
+ }
+ }
+}
diff --git a/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalLfTagSource.cs b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalLfTagSource.cs
new file mode 100644
index 00000000..62201893
--- /dev/null
+++ b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalLfTagSource.cs
@@ -0,0 +1,42 @@
+// Copyright (c) 2016 SIL International
+// This software is licensed under the MIT license (http://opensource.org/licenses/MIT)
+
+using LfMerge.Core.FieldWorks;
+using SIL.LCModel;
+using System.Collections.Generic;
+
+namespace LfMerge.Core.DataConverters.CanonicalSources
+{
+ public class CanonicalLfTagSource : CanonicalOptionListSource
+ {
+ public CanonicalLfTagSource()
+ : base("canonical-lf-tags.xml", "item")
+ {
+ }
+
+ public override void LoadCanonicalData()
+ {
+ LoadCanonicalData();
+ }
+
+ public ICmPossibilityList EnsureLcmPossibilityListExists(FwServiceLocatorCache serviceLocator, System.Guid parentListGuid, string listName, int wsForKeys) {
+ if (byKey.Count == 0) {
+ LoadCanonicalData();
+ }
+ var repo = serviceLocator.GetInstance();
+ var listFactory = serviceLocator.GetInstance();
+ var guid = new System.Guid(MagicStrings.LcmOptionListGuidForLfTags);
+ ICmPossibilityList possList;
+ if (!repo.TryGetObject(guid, out possList)) {
+ possList = listFactory.CreateUnowned(guid, MagicStrings.LcmCustomFieldNameForLfTags, wsForKeys);
+ }
+ var converter = new ConvertMongoToLcmOptionList(serviceLocator.GetInstance(), null, null, possList, wsForKeys, this);
+ foreach (KeyValuePair item in this.byKey)
+ {
+ converter.FindOrCreateFromCanonicalItem(item.Value);
+ }
+ // TODO: Write acceptance test that verifies that a CmPossibilityList with the right GUID gets created and populated.
+ return possList;
+ }
+ }
+}
diff --git a/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalOptionListSource.cs b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalOptionListSource.cs
index c8fc23c0..4985f50a 100644
--- a/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalOptionListSource.cs
+++ b/src/LfMerge.Core/DataConverters/CanonicalSources/CanonicalOptionListSource.cs
@@ -36,6 +36,8 @@ public static CanonicalOptionListSource Create(string listCode)
return new CanonicalPartOfSpeechSource();
else if (listCode == MagicStrings.LfOptionListCodeForSemanticDomains)
return new CanonicalSemanticDomainSource();
+ else if (listCode == MagicStrings.LfOptionListCodeForLfTags)
+ return new CanonicalLfTagSource();
else
return null;
}
@@ -71,6 +73,10 @@ public bool TryGetByKey(string key, out CanonicalItem result)
return (result != null);
}
+ public Dictionary.ValueCollection ValuesByGuid => this.byGuid.Values;
+ public Dictionary.ValueCollection ValuesByKey => this.byKey.Values;
+ public int Count => this.byGuid.Count;
+
// Descendants will override this to specify the generic type T.
// E.g., LoadCanonicalData();
public abstract void LoadCanonicalData();
diff --git a/src/LfMerge.Core/DataConverters/ConvertLcmToMongoCustomField.cs b/src/LfMerge.Core/DataConverters/ConvertLcmToMongoCustomField.cs
index 731d5e4c..2fbbe775 100644
--- a/src/LfMerge.Core/DataConverters/ConvertLcmToMongoCustomField.cs
+++ b/src/LfMerge.Core/DataConverters/ConvertLcmToMongoCustomField.cs
@@ -243,13 +243,15 @@ Dictionary lfCustomFieldTypes
/// Either "entry", "senses", or "examples"
/// Dictionary of ConvertLcmToMongoOptionList instances, keyed by list code
public BsonDocument GetCustomFieldsForThisCmObject(ICmObject cmObj, string objectType,
- IDictionary listConverters)
+ IDictionary listConverters,
+ params string[] fieldNamesToSkip)
{
if (cmObj == null) return null;
List customFieldIds = new List(
LcmMetaData.GetFields(cmObj.ClassID, false, (int)CellarPropertyTypeFilter.All)
- .Where(flid => cache.GetIsCustomField(flid)));
+ .Where(flid => cache.GetIsCustomField(flid))
+ .Where(flid => !fieldNamesToSkip.Contains(LcmMetaData.GetFieldName(flid))));
var customFieldData = new BsonDocument();
var customFieldGuids = new BsonDocument();
diff --git a/src/LfMerge.Core/DataConverters/ConvertLcmToMongoLexicon.cs b/src/LfMerge.Core/DataConverters/ConvertLcmToMongoLexicon.cs
index 21692017..8cf60df5 100644
--- a/src/LfMerge.Core/DataConverters/ConvertLcmToMongoLexicon.cs
+++ b/src/LfMerge.Core/DataConverters/ConvertLcmToMongoLexicon.cs
@@ -11,6 +11,7 @@
using LfMerge.Core.MongoConnector;
using MongoDB.Bson;
using SIL.LCModel;
+using SIL.LCModel.Application;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.Progress;
@@ -43,6 +44,7 @@ public class ConvertLcmToMongoLexicon
private const string SenseTypeListCode = MagicStrings.LfOptionListCodeForSenseTypes;
private const string AnthroCodeListCode = MagicStrings.LfOptionListCodeForAnthropologyCodes;
private const string StatusListCode = MagicStrings.LfOptionListCodeForStatus;
+ private const string LfTagsListCode = MagicStrings.LfOptionListCodeForLfTags;
private IDictionary ListConverters;
@@ -84,6 +86,13 @@ public ConvertLcmToMongoLexicon(ILfProject lfProject, ILogger logger, IMongoConn
ListConverters[AnthroCodeListCode] = ConvertOptionListFromLcm(LfProject, AnthroCodeListCode, ServiceLocator.LanguageProject.AnthroListOA);
ListConverters[StatusListCode] = ConvertOptionListFromLcm(LfProject, StatusListCode, ServiceLocator.LanguageProject.StatusOA);
+ // Custom field "LF Tags" in LCM needs special handling
+ var lfTagsListGuid = new System.Guid(MagicStrings.LcmOptionListGuidForLfTags);
+ var possibilityListRepo = ServiceLocator.GetInstance();
+ if (possibilityListRepo.TryGetObject(lfTagsListGuid, out var possibilityList)) {
+ ListConverters[LfTagsListCode] = ConvertOptionListFromLcm(LfProject, LfTagsListCode, possibilityList, updateMongoList: false);
+ }
+
_convertCustomField = new ConvertLcmToMongoCustomField(Cache, ServiceLocator, logger);
foreach (KeyValuePair pair in _convertCustomField.GetCustomFieldParentLists())
{
@@ -171,6 +180,22 @@ private T GetInstance() where T : class
return ServiceLocator.GetInstance();
}
+ // TODO: This is fine for entries, but it should eventually be more generic so it can handle senses and examples
+ private LfStringArrayField StringArrayFieldFromCustomField(string listCode, ILexEntry entry, string fieldName) {
+ var result = Array.Empty();
+ int flid = Cache.MetaDataCacheAccessor.GetFieldId("LexEntry", fieldName, false);
+ if (flid != 0) {
+ ISilDataAccessManaged data = (ISilDataAccessManaged)Cache.DomainDataByFlid;
+ int[] hvos = data.VecProp(entry.Hvo, flid);
+ var possibilities = hvos
+ .Where(hvo => data.get_IsValidObject(hvo))
+ .Select(hvo => Cache.GetAtomicPropObject(hvo))
+ .OfType();
+ return ToStringArrayField(listCode, possibilities);
+ }
+ return null;
+ }
+
private LfMultiText ToMultiText(IMultiAccessorBase LcmMultiString)
{
if (LcmMultiString == null) return null;
@@ -308,7 +333,9 @@ private LfLexEntry LcmLexEntryToLfLexEntry(ILexEntry LcmEntry)
lfEntry.Senses.AddRange(LcmEntry.SensesOS.Select(LcmSenseToLfSense));
lfEntry.SummaryDefinition = ToMultiText(LcmEntry.SummaryDefinition);
- BsonDocument customFieldsAndGuids = _convertCustomField.GetCustomFieldsForThisCmObject(LcmEntry, "entry", ListConverters);
+ lfEntry.Tags = StringArrayFieldFromCustomField(LfTagsListCode, LcmEntry, MagicStrings.LcmCustomFieldNameForLfTags);
+
+ BsonDocument customFieldsAndGuids = _convertCustomField.GetCustomFieldsForThisCmObject(LcmEntry, "entry", ListConverters, MagicStrings.LcmCustomFieldNameForLfTags);
BsonDocument customFieldsBson = customFieldsAndGuids["customFields"].AsBsonDocument;
BsonDocument customFieldGuids = customFieldsAndGuids["customFieldGuids"].AsBsonDocument;
diff --git a/src/LfMerge.Core/DataConverters/ConvertMongoToLcmCustomField.cs b/src/LfMerge.Core/DataConverters/ConvertMongoToLcmCustomField.cs
index a8de39d0..9e25d499 100644
--- a/src/LfMerge.Core/DataConverters/ConvertMongoToLcmCustomField.cs
+++ b/src/LfMerge.Core/DataConverters/ConvertMongoToLcmCustomField.cs
@@ -321,39 +321,11 @@ public bool SetCustomFieldData(int hvo, int flid, BsonValue value, BsonValue gui
// String.Join(", ", keysFromLF.AsEnumerable()),
// String.Join(", ", fieldObjs.Select(poss => poss.AbbrAndName))
// );
- // Step 2: Remove any objects from the "old" list that weren't in the "new" list
- // We have to look them up by HVO because that's the only public API available in LCM
- // Following logic inspired by XmlImportData.CopyCustomFieldData in FieldWorks source
+
+ // We have to replace objects by HVO because that's the only public API available in LCM
int[] oldHvosArray = data.VecProp(hvo, flid);
int[] newHvosArray = fieldObjs.Select(poss => poss.Hvo).ToArray();
- // Shortcut check
- if (oldHvosArray.SequenceEqual(newHvosArray))
- {
- // Nothing to do, so return now so that we don't cause unnecessary changes and commits in Mercurial
- return false;
- }
- HashSet newHvos = new HashSet(newHvosArray);
- HashSet combinedHvos = new HashSet();
- // Loop backwards so deleting items won't mess up indices of subsequent deletions
- for (int idx = oldHvosArray.Length - 1; idx >= 0; idx--)
- {
- int oldHvo = oldHvosArray[idx];
- if (newHvos.Contains(oldHvo))
- combinedHvos.Add(oldHvo);
- else
- data.Replace(hvo, flid, idx, idx + 1, null, 0); // Important to pass *both* null *and* 0 here to remove items
- }
-
- // Step 3: Add any objects from the "new" list that weren't in the "old" list
- foreach (int newHvo in newHvosArray)
- {
- if (combinedHvos.Contains(newHvo))
- continue;
- // This item was added in the new list
- data.Replace(hvo, flid, combinedHvos.Count, combinedHvos.Count, new int[] { newHvo }, 1);
- combinedHvos.Add(newHvo);
- }
- return true;
+ return ConvertUtilities.ReplaceHvosInCustomField(hvo, flid, data, oldHvosArray, newHvosArray);
}
case CellarPropertyType.String:
@@ -384,13 +356,14 @@ public bool SetCustomFieldData(int hvo, int flid, BsonValue value, BsonValue gui
}
}
- public void SetCustomFieldsForThisCmObject(ICmObject cmObj, string objectType, BsonDocument customFieldValues, BsonDocument customFieldGuids)
+ public void SetCustomFieldsForThisCmObject(ICmObject cmObj, string objectType, BsonDocument customFieldValues, BsonDocument customFieldGuids, params int[] customFieldIdsToSkip)
{
if (customFieldValues == null) return;
IEnumerable customFieldIds =
lcmMetaData.GetFields(cmObj.ClassID, false, (int)CellarPropertyTypeFilter.All)
- .Where(flid => cache.GetIsCustomField(flid));
+ .Where(flid => cache.GetIsCustomField(flid))
+ .Where(flid => !customFieldIdsToSkip.Contains(flid));
var remainingFieldNames = new HashSet(customFieldValues.Select(elem => elem.Name));
foreach (int flid in customFieldIds)
diff --git a/src/LfMerge.Core/DataConverters/ConvertMongoToLcmLexicon.cs b/src/LfMerge.Core/DataConverters/ConvertMongoToLcmLexicon.cs
index 4d67e33f..81e94a4e 100644
--- a/src/LfMerge.Core/DataConverters/ConvertMongoToLcmLexicon.cs
+++ b/src/LfMerge.Core/DataConverters/ConvertMongoToLcmLexicon.cs
@@ -12,6 +12,7 @@
using LfMerge.Core.Reporting;
using LfMerge.Core.Settings;
using SIL.LCModel;
+using SIL.LCModel.Application;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Core.WritingSystems;
using SIL.LCModel.DomainServices;
@@ -38,6 +39,7 @@ public class ConvertMongoToLcmLexicon
private int _wsEn;
private ConvertMongoToLcmCustomField _convertCustomField;
+ private int _lfTagsFieldId;
// Shorter names to use in this class since MagicStrings.LfOptionListCodeForGrammaticalInfo
// (etc.) are real mouthfuls
@@ -51,6 +53,7 @@ public class ConvertMongoToLcmLexicon
private const string SenseTypeListCode = MagicStrings.LfOptionListCodeForSenseTypes;
private const string AnthroCodeListCode = MagicStrings.LfOptionListCodeForAnthropologyCodes;
private const string StatusListCode = MagicStrings.LfOptionListCodeForStatus;
+ private const string LfTagsListCode = MagicStrings.LfOptionListCodeForLfTags;
private IDictionary ListConverters;
@@ -74,6 +77,8 @@ public ConvertMongoToLcmLexicon(LfMergeSettings settings, ILfProject lfproject,
//_analysisWritingSystems = ServiceLocator.LanguageProject.CurrentAnalysisWritingSystems;
//_vernacularWritingSystems = ServiceLocator.LanguageProject.CurrentVernacularWritingSystems;
+ _wsEn = ServiceLocator.WritingSystemFactory.GetWsFromStr("en");
+
ListConverters = new Dictionary();
ListConverters[GrammarListCode] = PrepareOptionListConverter(GrammarListCode);
ListConverters[SemDomListCode] = PrepareOptionListConverter(SemDomListCode);
@@ -83,8 +88,7 @@ public ConvertMongoToLcmLexicon(LfMergeSettings settings, ILfProject lfproject,
ListConverters[SenseTypeListCode] = PrepareOptionListConverter(SenseTypeListCode);
ListConverters[AnthroCodeListCode] = PrepareOptionListConverter(AnthroCodeListCode);
ListConverters[StatusListCode] = PrepareOptionListConverter(StatusListCode);
-
- _wsEn = ServiceLocator.WritingSystemFactory.GetWsFromStr("en");
+ _lfTagsFieldId = 0;
// Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), replace the code block
// above with this one (that provides TWO parameters to PrepareOptionListConverter)
@@ -112,7 +116,40 @@ private ConvertMongoToLcmOptionList PrepareOptionListConverter(string listCode)
{
LfOptionList optionListToConvert = Connection.GetLfOptionListByCode(LfProject, listCode);
return new ConvertMongoToLcmOptionList(GetInstance(),
- optionListToConvert, Logger, CanonicalOptionListSource.Create(listCode));
+ optionListToConvert, Logger, null, 0, CanonicalOptionListSource.Create(listCode));
+ }
+
+ private ConvertMongoToLcmOptionList PrepareOptionListConverterFromCanonicalSource(string listCode)
+ {
+ // 1. Check if parent list for LF Tags already exists in LCM
+ // 2. Create it if it doesn't, using canonical source data
+
+ var canonicalSource = CanonicalOptionListSource.Create(listCode);
+ var converter = new ConvertMongoToLcmOptionList(GetInstance(),
+ null, Logger, null, _wsEn, canonicalSource);
+ ICmPossibilityList parentList = converter.EnsureLcmPossibilityListExists(
+ canonicalSource,
+ ServiceLocator,
+ new System.Guid(MagicStrings.LcmOptionListGuidForLfTags),
+ MagicStrings.LcmCustomFieldNameForLfTags
+ );
+
+ return converter;
+ }
+
+ private int EnsureCustomFieldExists(Guid parentListGuid, string name)
+ {
+ // 1. Check if custom field already exists in LCM
+ // 2. Create it if it doesn't, using parent list that is now guaranteed to exist
+
+ var mdc = ServiceLocator.MetaDataCache;
+ int flid = 0;
+ if (mdc.FieldExists("LexEntry", name, false)) {
+ flid = mdc.GetFieldId("LexEntry", name, false);
+ } else {
+ flid = mdc.AddCustomField("LexEntry", name, SIL.LCModel.Core.Cellar.CellarPropertyType.ReferenceCollection, CmPossibilityTags.kClassId, "Internal Language Forge field - do not edit", _wsEn, parentListGuid);
+ }
+ return flid;
}
// Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), replace the function
@@ -143,6 +180,10 @@ public void RunConversion()
IEnumerable lexicon = GetLexicon(LfProject);
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("undo", "redo", Cache.ActionHandlerAccessor, () =>
{
+ // Can't run this in the constructor, as we need a unit of work for EnsureCustomFieldExists
+ ListConverters[LfTagsListCode] = PrepareOptionListConverterFromCanonicalSource(LfTagsListCode);
+ _lfTagsFieldId = EnsureCustomFieldExists(new System.Guid(MagicStrings.LcmOptionListGuidForLfTags), MagicStrings.LcmCustomFieldNameForLfTags);
+
#if false // Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), uncomment this block
foreach (ConvertMongoToLcmOptionList converter in ListConverters.Values)
{
@@ -570,8 +611,14 @@ private void LfLexEntryToLcmLexEntry(LfLexEntry lfEntry)
// lfEntry.Senses -> LcmEntry.SensesOS
SetLcmListFromLfList(LcmEntry, LcmEntry.SensesOS, lfEntry.Senses, LfSenseToLcmSense);
+ // TODO: Handle lf-tags custom field with something like the following
+ // ListConverters[AnthroCodeListCode].UpdatePossibilitiesFromStringArray(LcmSense.AnthroCodesRC,
+ // lfSense.AnthropologyCategories);
+
+ SetLcmCustomFieldFromLfStringArrayField(LcmEntry, _lfTagsFieldId, lfEntry.Tags);
+
_convertCustomField.SetCustomFieldsForThisCmObject(LcmEntry, "entry", lfEntry.CustomFields,
- lfEntry.CustomFieldGuids);
+ lfEntry.CustomFieldGuids, _lfTagsFieldId);
// If we got this far, we either created or modified this entry
if (createdEntry)
@@ -783,6 +830,16 @@ Action convertAction
}
}
+ private void SetLcmCustomFieldFromLfStringArrayField(ILexEntry lcmEntry, int flid, LfStringArrayField keys)
+ {
+ if (keys == null || keys.Values == null) return;
+ ISilDataAccessManaged data = (ISilDataAccessManaged)Cache.DomainDataByFlid;
+ int[] oldHvos = data.VecProp(lcmEntry.Hvo, flid);
+ var possibilities = ListConverters[LfTagsListCode].FromStringArrayField(keys);
+ int[] newHvos = possibilities.Select(poss => poss.Hvo).ToArray();
+ ConvertUtilities.ReplaceHvosInCustomField(lcmEntry.Hvo, flid, data, oldHvos, newHvos);
+ }
+
private Guid GuidFromLiftId(string liftId)
{
Guid result;
diff --git a/src/LfMerge.Core/DataConverters/ConvertMongoToLcmOptionList.cs b/src/LfMerge.Core/DataConverters/ConvertMongoToLcmOptionList.cs
index 164bbce3..a2a7ce0c 100644
--- a/src/LfMerge.Core/DataConverters/ConvertMongoToLcmOptionList.cs
+++ b/src/LfMerge.Core/DataConverters/ConvertMongoToLcmOptionList.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using LfMerge.Core.DataConverters.CanonicalSources;
+using LfMerge.Core.FieldWorks;
using LfMerge.Core.LanguageForge.Model;
using LfMerge.Core.Logging;
using SIL.LCModel;
@@ -19,24 +20,17 @@ public class ConvertMongoToLcmOptionList
protected LfOptionList _lfOptionList;
protected ILogger _logger;
protected CanonicalOptionListSource _canonicalSource;
- #if false // Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), uncomment this block
protected int _wsForKeys;
protected ICmPossibilityList _parentList;
- #endif
public Dictionary PossibilitiesByKey { get; protected set; }
- #if false // Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), uncomment this version of the constructor
public ConvertMongoToLcmOptionList(IRepository possRepo, LfOptionList lfOptionList, ILogger logger, ICmPossibilityList parentList, int wsForKeys, CanonicalOptionListSource canonicalSource = null)
- #endif
- public ConvertMongoToLcmOptionList(IRepository possRepo, LfOptionList lfOptionList, ILogger logger, CanonicalOptionListSource canonicalSource = null)
{
_possRepo = possRepo;
_logger = logger;
- #if false // Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), uncomment this block
_parentList = parentList;
_wsForKeys = wsForKeys;
- #endif
_canonicalSource = canonicalSource;
RebuildLookupTables(lfOptionList);
}
@@ -77,7 +71,6 @@ public ICmPossibility FromStringKey(string key)
return null;
}
- #if false // Once we allow LanguageForge to create optionlist items with "canonical" values (parts of speech, semantic domains, etc.), uncomment this block
public ICmPossibility CreateFromCanonicalItem(CanonicalItem item)
{
if (item.Parent != null)
@@ -86,12 +79,41 @@ public ICmPossibility CreateFromCanonicalItem(CanonicalItem item)
// and populate the parent if the parent didn't exist already).
FromStringKey(item.Parent.Key);
}
+ MainClass.Logger.Error($"Creating from canonical item with ws {_wsForKeys} from item {item.ToString()}");
ICmPossibility poss = _parentList.FindOrCreatePossibility(item.ORCDelimitedKey, _wsForKeys);
item.PopulatePossibility(poss);
PossibilitiesByKey[item.Key] = poss;
return poss;
}
- #endif
+
+ public ICmPossibility FindOrCreateFromCanonicalItem(CanonicalItem item)
+ {
+ ICmPossibility poss = LookupByCanonicalItem(item);
+ if (poss == null) {
+ poss = CreateFromCanonicalItem(item);
+ }
+ return poss;
+ }
+
+ public ICmPossibilityList EnsureLcmPossibilityListExists(CanonicalOptionListSource canonicalSource, FwServiceLocatorCache serviceLocator, System.Guid parentListGuid, string listName) {
+ if (canonicalSource.Count == 0) {
+ canonicalSource.LoadCanonicalData();
+ }
+ var repo = serviceLocator.GetInstance();
+ var listFactory = serviceLocator.GetInstance();
+ var guid = new System.Guid(MagicStrings.LcmOptionListGuidForLfTags);
+ if (_parentList == null) {
+ if (!repo.TryGetObject(guid, out _parentList)) {
+ _parentList = listFactory.CreateUnowned(guid, MagicStrings.LcmCustomFieldNameForLfTags, _wsForKeys);
+ }
+ }
+ foreach (var item in canonicalSource.ValuesByKey)
+ {
+ this.FindOrCreateFromCanonicalItem(item);
+ }
+ // TODO: Write acceptance test that verifies that a CmPossibilityList with the right GUID gets created and populated.
+ return _parentList; // TODO: May not be necessary now
+ }
public ICmPossibility FromStringField(LfStringField keyField)
{
diff --git a/src/LfMerge.Core/DataConverters/ConvertUtilities.cs b/src/LfMerge.Core/DataConverters/ConvertUtilities.cs
index 35c367ae..c911e53a 100644
--- a/src/LfMerge.Core/DataConverters/ConvertUtilities.cs
+++ b/src/LfMerge.Core/DataConverters/ConvertUtilities.cs
@@ -6,6 +6,7 @@
using LfMerge.Core.LanguageForge.Model;
using MongoDB.Bson;
using SIL.LCModel;
+using SIL.LCModel.Application;
using SIL.LCModel.Core.KernelInterfaces;
using SIL.LCModel.Core.WritingSystems;
@@ -39,6 +40,41 @@ public static LfParagraph LcmParaToLfPara(IStTxtPara lcmPara, ILgWritingSystemFa
return lfPara;
}
+ public static bool ReplaceHvosInCustomField(int hvo, int flid, ISilDataAccessManaged data, int[] oldArray, int[] newArray)
+ {
+ // Shortcut check
+ if (oldArray.SequenceEqual(newArray))
+ {
+ // Nothing to do, so return now so that we don't cause unnecessary changes and commits in Mercurial
+ return false;
+ }
+ // HashSets for O(1) lookup. Might be overkill, but better safe than sorry
+ var newHvos = new HashSet(newArray);
+ var combinedHvos = new HashSet();
+
+ // Step 1: Remove any objects from the "old" list that weren't in the "new" list
+ // Loop backwards so deleting items won't mess up indices of subsequent deletions
+ for (int idx = oldArray.Length - 1; idx >= 0; idx--)
+ {
+ int oldHvo = oldArray[idx];
+ if (newHvos.Contains(oldHvo))
+ combinedHvos.Add(oldHvo);
+ else
+ data.Replace(hvo, flid, idx, idx + 1, null, 0); // Important to pass *both* null *and* 0 here to remove items
+ }
+
+ // Step 2: Add any objects from the "new" list that weren't in the "old" list
+ foreach (int newHvo in newArray)
+ {
+ if (combinedHvos.Contains(newHvo))
+ continue;
+ // This item was added in the new list
+ data.Replace(hvo, flid, combinedHvos.Count, combinedHvos.Count, new int[] { newHvo }, 1);
+ combinedHvos.Add(newHvo);
+ }
+ return true;
+ }
+
///
/// Return a name suitable for logging from an entry
///
diff --git a/src/LfMerge.Core/LanguageForge/Config/LfProjectConfig.cs b/src/LfMerge.Core/LanguageForge/Config/LfProjectConfig.cs
index 1b203147..f45575d0 100644
--- a/src/LfMerge.Core/LanguageForge/Config/LfProjectConfig.cs
+++ b/src/LfMerge.Core/LanguageForge/Config/LfProjectConfig.cs
@@ -2,6 +2,7 @@
// This software is licensed under the MIT license (http://opensource.org/licenses/MIT)
using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
namespace LfMerge.Core.LanguageForge.Config
{
@@ -15,6 +16,8 @@ public class LfProjectConfig : ILfProjectConfig
public LfConfigFieldList Entry { get; set; }
public BsonDocument RoleViews { get; set; }
public BsonDocument UserViews { get; set; }
+ [BsonExtraElements]
+ public BsonDocument OtherConfig { get; set; }
}
}
diff --git a/src/LfMerge.Core/LanguageForge/Config/MongoRegistrarForLfConfig.cs b/src/LfMerge.Core/LanguageForge/Config/MongoRegistrarForLfConfig.cs
index b6708cb8..67e5227f 100644
--- a/src/LfMerge.Core/LanguageForge/Config/MongoRegistrarForLfConfig.cs
+++ b/src/LfMerge.Core/LanguageForge/Config/MongoRegistrarForLfConfig.cs
@@ -16,6 +16,7 @@ public override void RegisterClassMappings()
{
RegisterClassMapsForDerivedClassesOf(typeof(LfConfigFieldBase));
RegisterClassIgnoreExtraFields(typeof(LfProjectConfig));
+ MainClass.Logger.Error("Registered class mappings for LfProjectConfig");
RegisterClassIgnoreExtraFields(typeof(MongoProjectRecord));
}
}
diff --git a/src/LfMerge.Core/LanguageForge/Infrastructure/LanguageForgeProxy.cs b/src/LfMerge.Core/LanguageForge/Infrastructure/LanguageForgeProxy.cs
index 7e0cdb8e..045f281c 100644
--- a/src/LfMerge.Core/LanguageForge/Infrastructure/LanguageForgeProxy.cs
+++ b/src/LfMerge.Core/LanguageForge/Infrastructure/LanguageForgeProxy.cs
@@ -27,6 +27,20 @@ public string UpdateCustomFieldViews(string projectCode, List c
return RunClass(className, methodName, parameters, isTest);
}
+ public string UpdateCustomFieldViewsNonProxied(string projectCode, List customFieldSpecs) {
+ return UpdateCustomFieldViewsNonProxied(projectCode, customFieldSpecs, false);
+ }
+
+ public string UpdateCustomFieldViewsNonProxied(string projectCode, List customFieldSpecs, bool isTest)
+ {
+ const string className = "Api\\Model\\Languageforge\\Lexicon\\Command\\LexProjectCommands";
+ const string methodName = "updateCustomFieldViews";
+ var parameters = new List