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(); + parameters.Add(projectCode); + parameters.Add(customFieldSpecs); + return RunClass(className, methodName, parameters, isTest); + } + public string ListUsers() { const string className = "Api\\Model\\Command\\UserCommands"; diff --git a/src/LfMerge.Core/LanguageForge/Model/LfLexEntry.cs b/src/LfMerge.Core/LanguageForge/Model/LfLexEntry.cs index 41aa85b0..9af9216d 100644 --- a/src/LfMerge.Core/LanguageForge/Model/LfLexEntry.cs +++ b/src/LfMerge.Core/LanguageForge/Model/LfLexEntry.cs @@ -43,6 +43,7 @@ public class LfLexEntry : LfFieldBase, IHasNullableGuid [BsonRepresentation(BsonType.String)] public Guid PronunciationGuid { get; set; } public LfMultiText SummaryDefinition { get; set; } + public LfStringArrayField Tags { get; set; } public LfMultiText Tone { get; set; } public LfLexEntry() diff --git a/src/LfMerge.Core/LfMerge.Core.csproj b/src/LfMerge.Core/LfMerge.Core.csproj index 59101c2e..8375badb 100644 --- a/src/LfMerge.Core/LfMerge.Core.csproj +++ b/src/LfMerge.Core/LfMerge.Core.csproj @@ -59,5 +59,9 @@ See full changelog at https://github.com/sillsdev/LfMerge/blob/master/CHANGELOG. SemDom.xml SemDom.xml + + canonical-lf-tags.xml + canonical-lf-tags.xml + \ No newline at end of file diff --git a/src/LfMerge.Core/MagicStrings.cs b/src/LfMerge.Core/MagicStrings.cs index bef4967f..350b8c7c 100644 --- a/src/LfMerge.Core/MagicStrings.cs +++ b/src/LfMerge.Core/MagicStrings.cs @@ -16,6 +16,7 @@ static MagicStrings() // Option lists that are currently used in LF (as of 2016-03-01) { LfOptionListCodeForGrammaticalInfo, "Part of Speech" }, { LfOptionListCodeForSemanticDomains, "Semantic Domain" }, + { LfOptionListCodeForLfTags, "LF Tags" }, { LfOptionListCodeForAcademicDomainTypes, "Academic Domains" }, { LfOptionListCodeForEnvironments, "Environments" }, { LfOptionListCodeForLocations, "Location" }, @@ -44,6 +45,7 @@ static MagicStrings() // Option lists that are currently used in LF (as of 2016-03-01) public const string LfOptionListCodeForGrammaticalInfo = "grammatical-info"; public const string LfOptionListCodeForSemanticDomains = "semantic-domain-ddp4"; + public const string LfOptionListCodeForLfTags = "lf-entry-tags"; public const string LfOptionListCodeForAcademicDomainTypes = "domain-type"; public const string LfOptionListCodeForEnvironments = "environments"; public const string LfOptionListCodeForLocations = "location"; @@ -89,6 +91,10 @@ static MagicStrings() public const string LanguageCodeForGenDateFields = "qaa-Qaad"; public const string LanguageCodeForIntFields = "qaa-Zmth"; + // Custom fields that we reserve for LF use in Send/Receive FLEx projects + public const string LcmCustomFieldNameForLfTags = "LF_Tags"; + public const string LcmOptionListGuidForLfTags = "6d9b3052-195b-46cb-a300-e8598e59fed5"; + // FW strings public const string FwFixitAppName = "FixFwData.exe"; diff --git a/src/LfMerge.Core/MongoConnector/MongoRegistrar.cs b/src/LfMerge.Core/MongoConnector/MongoRegistrar.cs index dab7b7ee..e404acf1 100644 --- a/src/LfMerge.Core/MongoConnector/MongoRegistrar.cs +++ b/src/LfMerge.Core/MongoConnector/MongoRegistrar.cs @@ -34,7 +34,8 @@ public void RegisterClassIgnoreExtraFields(Type type) { BsonClassMap cm = new BsonClassMap(type); cm.AutoMap(); - //cm.SetIgnoreExtraElements(true); // Let's see if this is the default + cm.SetIgnoreExtraElements(true); // Let's see if this is the default + MainClass.Logger.Error("Registed class mappings ignoring extra elements"); BsonClassMap.RegisterClassMap(cm); //BsonSerializer.RegisterDiscriminatorConvention(type, new ScalarDiscriminatorConvention("type")); } diff --git a/src/LfMerge/Program.cs b/src/LfMerge/Program.cs index 24109a59..e4f453bf 100644 --- a/src/LfMerge/Program.cs +++ b/src/LfMerge/Program.cs @@ -27,6 +27,7 @@ public static int Main(string[] args) var options = Options.ParseCommandLineArgs(args); if (options == null) return (int)ErrorCode.InvalidOptions; + MainClass.Logger.Error("Starting LfMerge"); // initialize the SLDR Sldr.Initialize(); @@ -59,6 +60,7 @@ public static int Main(string[] args) return (int)ErrorCode.GeneralError; MongoConnection.Initialize(); + MainClass.Logger.Error("Registered class mappings ignoring extra elements"); differentModelVersion = RunAction(options.ProjectCode, options.CurrentAction); }