public ICmPossibility FromStringArrayFieldWithOneCase(LfStringArrayField keyField) { if (keyField == null || keyField.Values == null || keyField.IsEmpty) { return(null); } return(FromStringKey(keyField.Values.First())); }
// Used in UpdatePossibilitiesFromStringArray and UpdateInvertedPossibilitiesFromStringArray below // Generic so that they can handle lists like AnthroCodes, etc. public IEnumerable <T> FromStringArrayField <T>(LfStringArrayField source) { IEnumerable <string> keys; if (source == null || source.Values == null) { keys = new List <string>(); // Empty list } else { keys = source.Values.Where(value => !string.IsNullOrEmpty(value)); } return(keys.Select(key => (T)FromStringKey(key)).Where(poss => poss != null)); }
/// <summary> /// Convert FDO sense to LF sense. /// </summary> /// <returns>LF sense /// <param name="fdoSense">Fdo sense.</param> private LfSense FdoSenseToLfSense(ILexSense fdoSense) { var lfSense = new LfSense(); ILgWritingSystem VernacularWritingSystem = ServiceLocator.LanguageProject.DefaultVernacularWritingSystem; ILgWritingSystem AnalysisWritingSystem = ServiceLocator.LanguageProject.DefaultAnalysisWritingSystem; lfSense.Guid = fdoSense.Guid; lfSense.Gloss = ToMultiText(fdoSense.Gloss); lfSense.Definition = ToMultiText(fdoSense.Definition); // Fields below in alphabetical order by ILexSense property, except for Guid, Gloss and Definition lfSense.AcademicDomains = ToStringArrayField(AcademicDomainListCode, fdoSense.DomainTypesRC); lfSense.AnthropologyCategories = ToStringArrayField(AnthroCodeListCode, fdoSense.AnthroCodesRC); lfSense.AnthropologyNote = ToMultiText(fdoSense.AnthroNote); lfSense.DiscourseNote = ToMultiText(fdoSense.DiscourseNote); lfSense.EncyclopedicNote = ToMultiText(fdoSense.EncyclopedicInfo); if (fdoSense.ExamplesOS != null) { lfSense.Examples = new List <LfExample>(fdoSense.ExamplesOS.Select(FdoExampleToLfExample)); } lfSense.GeneralNote = ToMultiText(fdoSense.GeneralNote); lfSense.GrammarNote = ToMultiText(fdoSense.GrammarNote); if (fdoSense.LIFTid == null) { lfSense.LiftId = null; } else { lfSense.LiftId = fdoSense.LIFTid.Normalize(System.Text.NormalizationForm.FormC); // Because LIFT files on disk are NFC and we need to make sure LiftIDs match those on disk } if (fdoSense.MorphoSyntaxAnalysisRA != null) { IPartOfSpeech secondaryPos = null; // Only used in derivational affixes IPartOfSpeech pos = ConvertFdoToMongoPartsOfSpeech.FromMSA(fdoSense.MorphoSyntaxAnalysisRA, out secondaryPos); // Sometimes the part of speech can be null for legitimate reasons, so check the known class IDs before warning of an unknown MSA type if (pos == null && !ConvertFdoToMongoPartsOfSpeech.KnownMsaClassIds.Contains(fdoSense.MorphoSyntaxAnalysisRA.ClassID)) { Logger.Warning("Got MSA of unknown type {0} in sense {1} in project {2}", fdoSense.MorphoSyntaxAnalysisRA.GetType().Name, fdoSense.Guid, LfProject.ProjectCode); } else { lfSense.PartOfSpeech = ToStringField(GrammarListCode, pos); lfSense.SecondaryPartOfSpeech = ToStringField(GrammarListCode, secondaryPos); // It's fine if secondaryPos is still null here } } lfSense.PhonologyNote = ToMultiText(fdoSense.PhonologyNote); if (fdoSense.PicturesOS != null) { lfSense.Pictures = new List <LfPicture>(fdoSense.PicturesOS.Select(FdoPictureToLfPicture)); //Use the commented code for debugging into FdoPictureToLfPicture // //lfSense.Pictures = new List<LfPicture>(); //foreach (var fdoPic in fdoSense.PicturesOS) // lfSense.Pictures.Add(FdoPictureToLfPicture(fdoPic)); } lfSense.SenseBibliography = ToMultiText(fdoSense.Bibliography); lfSense.SenseRestrictions = ToMultiText(fdoSense.Restrictions); if (fdoSense.ReversalEntriesRC != null) { IEnumerable <string> reversalEntries = fdoSense.ReversalEntriesRC.Select(fdoReversalEntry => fdoReversalEntry.LongName); lfSense.ReversalEntries = LfStringArrayField.FromStrings(reversalEntries); } lfSense.ScientificName = LfMultiText.FromSingleITsString(fdoSense.ScientificName, ServiceLocator.WritingSystemFactory); lfSense.SemanticDomain = ToStringArrayField(SemDomListCode, fdoSense.SemanticDomainsRC); lfSense.SemanticsNote = ToMultiText(fdoSense.SemanticsNote); // fdoSense.SensesOS; // Not mapped because LF doesn't handle subsenses. TODO: When LF handles subsenses, map this one. lfSense.SenseType = ToStringField(SenseTypeListCode, fdoSense.SenseTypeRA); lfSense.SociolinguisticsNote = ToMultiText(fdoSense.SocioLinguisticsNote); if (fdoSense.Source != null) { lfSense.Source = LfMultiText.FromSingleITsString(fdoSense.Source, ServiceLocator.WritingSystemFactory); } lfSense.Status = ToStringArrayField(StatusListCode, fdoSense.StatusRA); lfSense.Usages = ToStringArrayField(UsageTypeListCode, fdoSense.UsageTypesRC); /* Fields not mapped because it doesn't make sense to map them (e.g., Hvo, backreferences, etc): * fdoSense.AllOwnedObjects; * fdoSense.AllSenses; * fdoSense.Cache; * fdoSense.CanDelete; * fdoSense.ChooserNameTS; * fdoSense.ClassID; * fdoSense.ClassName; * fdoSense.Entry; * fdoSense.EntryID; * fdoSense.FullReferenceName; * fdoSense.GetDesiredMsaType(); * fdoSense.Hvo; * fdoSense.ImportResidue; * fdoSense.IndexInOwner; * fdoSense.IsValidObject; * fdoSense.LexSenseReferences; * fdoSense.LongNameTSS; * fdoSense.ObjectIdName; * fdoSense.OwnedObjects; * fdoSense.Owner; * fdoSense.OwningFlid; * fdoSense.OwnOrd; * fdoSense.ReferringObjects; * fdoSense.ReversalNameForWs(wsVern); * fdoSense.SandboxMSA; // Set-only property * fdoSense.Self; * fdoSense.Services; * fdoSense.ShortName; * fdoSense.ShortNameTSS; * fdoSense.SortKey; * fdoSense.SortKey2; * fdoSense.SortKey2Alpha; * fdoSense.SortKeyWs; * fdoSense.VariantFormEntryBackRefs; * fdoSense.VisibleComplexFormBackRefs; */ /* Fields not mapped because LanguageForge doesn't handle that data: * fdoSense.AppendixesRC; * fdoSense.ComplexFormEntries; * fdoSense.ComplexFormsNotSubentries; * fdoSense.DoNotPublishInRC; * fdoSense.Subentries; * fdoSense.ThesaurusItemsRC; * fdoSense.LiftResidue; * fdoSense.LexSenseOutline; * fdoSense.PublishIn; */ BsonDocument customFieldsAndGuids = _convertCustomField.GetCustomFieldsForThisCmObject(fdoSense, "senses", ListConverters); BsonDocument customFieldsBson = customFieldsAndGuids["customFields"].AsBsonDocument; BsonDocument customFieldGuids = customFieldsAndGuids["customFieldGuids"].AsBsonDocument; // TODO: Role Views only set on initial clone if (LfProject.IsInitialClone) { ; } // If custom field was deleted in Flex, delete config here lfSense.CustomFields = customFieldsBson; lfSense.CustomFieldGuids = customFieldGuids; return(lfSense); }
// Special case: LF sense Status field is a StringArray, but FDO sense status is single possibility private LfStringArrayField ToStringArrayField(string listCode, ICmPossibility fdoPoss) { return(LfStringArrayField.FromSingleString(ListConverters[listCode].LfItemKeyString(fdoPoss, _wsEn))); }
private LfStringArrayField ToStringArrayField(string listCode, IEnumerable <ICmPossibility> fdoPossCollection) { return(LfStringArrayField.FromStrings(ListConverters[listCode].LfItemKeyStrings(fdoPossCollection, _wsEn))); }
/// <summary> /// Set custom field data for one field (specified by owner HVO and field ID). /// </summary> /// <returns><c>true</c>, if custom field data was set, <c>false</c> otherwise (e.g., if value hadn't changed, /// or value was null, or field type was one not implemented in LCM, such as CellarPropertyType.Float).</returns> /// <param name="hvo">HVO of object whose field we're setting.</param> /// <param name="flid">Field ID of custom field to set.</param> /// <param name="value">Field's new value (as returned by GetCustomFieldData).</param> /// <param name="guidOrGuids">GUID or guids associated with new value (as returned by GetCustomFieldData). /// May be null or BsonNull.Value if no GUIDs associated with this value.</param> public bool SetCustomFieldData(int hvo, int flid, BsonValue value, BsonValue guidOrGuids) { if ((value == null) || (value == BsonNull.Value) || ((value.BsonType == BsonType.Array) && (value.AsBsonArray.Count == 0))) { return(false); } List <Guid> fieldGuids = new List <Guid>(); if (guidOrGuids == null || guidOrGuids == BsonNull.Value) { // Leave fieldGuids as an empty list } else { if (guidOrGuids is BsonArray) { fieldGuids.AddRange(guidOrGuids.AsBsonArray.Select(bsonValue => ParseGuidOrDefault(bsonValue.AsString))); } else { fieldGuids.Add(ParseGuidOrDefault(guidOrGuids.AsString)); } } ISilDataAccessManaged data = (ISilDataAccessManaged)cache.DomainDataByFlid; CellarPropertyType fieldType = (CellarPropertyType)lcmMetaData.GetFieldType(flid); string fieldName = lcmMetaData.GetFieldNameOrNull(flid); // logger.Debug("Custom field named {0} has type {1}", fieldName, fieldType.ToString()); if (fieldName == null) { return(false); } // Valid field types in LCM are GenDate, Integer, String, OwningAtomic, ReferenceAtomic, and ReferenceCollection, so that's all we implement. switch (fieldType) { case CellarPropertyType.GenDate: { var valueAsMultiText = BsonSerializer.Deserialize <LfMultiText>(value.AsBsonDocument); string valueAsString = valueAsMultiText.BestString(new string[] { MagicStrings.LanguageCodeForGenDateFields }); if (string.IsNullOrEmpty(valueAsString)) { return(false); } GenDate valueAsGenDate; if (GenDate.TryParse(valueAsString, out valueAsGenDate)) { GenDate oldValue = data.get_GenDateProp(hvo, flid); if (oldValue == valueAsGenDate) { return(false); } else { data.SetGenDate(hvo, flid, valueAsGenDate); return(true); } } return(false); } case CellarPropertyType.Integer: { var valueAsMultiText = BsonSerializer.Deserialize <LfMultiText>(value.AsBsonDocument); string valueAsString = valueAsMultiText.BestString(new string[] { MagicStrings.LanguageCodeForIntFields }); if (string.IsNullOrEmpty(valueAsString)) { return(false); } int valueAsInt; if (int.TryParse(valueAsString, out valueAsInt)) { int oldValue = data.get_IntProp(hvo, flid); if (oldValue == valueAsInt) { return(false); } else { data.SetInt(hvo, flid, valueAsInt); return(true); } } return(false); } case CellarPropertyType.OwningAtomic: { // Custom field is a MultiparagraphText, which is an IStText object in LCM IStTextRepository textRepo = servLoc.GetInstance <IStTextRepository>(); Guid fieldGuid = fieldGuids.FirstOrDefault(); IStText text; if (!textRepo.TryGetObject(fieldGuid, out text)) { int currentFieldContentsHvo = data.get_ObjectProp(hvo, flid); if (currentFieldContentsHvo != LcmCache.kNullHvo) { text = (IStText)cache.GetAtomicPropObject(currentFieldContentsHvo); } else { // NOTE: I don't like the "magic" -2 number below, but LCM doesn't seem to have an enum for this. 2015-11 RM int newStTextHvo = data.MakeNewObject(cache.GetDestinationClass(flid), hvo, flid, -2); text = (IStText)cache.GetAtomicPropObject(newStTextHvo); } } // Shortcut: if text contents haven't changed, we don't want to change anything at all BsonValue currentLcmTextContents = ConvertUtilities.GetCustomStTextValues(text, flid, servLoc.WritingSystemManager, lcmMetaData, cache.DefaultUserWs); if ((currentLcmTextContents == BsonNull.Value || currentLcmTextContents == null) && (value == BsonNull.Value || value == null)) { return(false); } if (currentLcmTextContents != null && currentLcmTextContents.Equals(value)) { // No changes needed. return(false); } // BsonDocument passed in contains "paragraphs". ParseCustomStTextValuesFromBson wants only a "value" element // inside the doc, so we'll need to construct a new doc for the StTextValues. BsonDocument doc = value.AsBsonDocument; LfMultiParagraph multiPara = BsonSerializer.Deserialize <LfMultiParagraph>(doc); // Now we have another way to check for "old value and new value were the same": if the LCM multiparagraph was empty, // GetCustomStTextValues will have returned null -- so if this multiPara has no paragraphs, that's also an unchanged situation if ((multiPara.Paragraphs == null || multiPara.Paragraphs.Count <= 0) && (currentLcmTextContents == BsonNull.Value || currentLcmTextContents == null)) { return(false); } int wsId; if (multiPara.InputSystem == null) { wsId = lcmMetaData.GetFieldWs(flid); } else { wsId = servLoc.WritingSystemFactory.GetWsFromStr(multiPara.InputSystem); } ConvertUtilities.SetCustomStTextValues(text, multiPara.Paragraphs, wsId); return(true); } case CellarPropertyType.ReferenceAtomic: if (fieldGuids.FirstOrDefault() != Guid.Empty) { int referencedHvo = data.get_ObjFromGuid(fieldGuids.FirstOrDefault()); int oldHvo = data.get_ObjectProp(hvo, flid); if (referencedHvo == oldHvo) { return(false); } else { data.SetObjProp(hvo, flid, referencedHvo); // TODO: What if the value of the referenced object has changed in LanguageForge? (E.g., change that possibility's text from "foo" to "bar") // Need to implement that scenario. return(true); } } else { // It's a reference to an ICmPossibility instance: create a new entry in appropriate PossibilityList LfStringField valueAsLfStringField = BsonSerializer.Deserialize <LfStringField>(value.AsBsonDocument); string nameHierarchy = valueAsLfStringField.Value; if (nameHierarchy == null) { return(false); } int fieldWs = lcmMetaData.GetFieldWs(flid); // Oddly, this can return 0 for some custom fields. TODO: Find out why: that seems like it would be an error. if (fieldWs == 0) { fieldWs = cache.DefaultUserWs; // TODO: Investigate, because this should probably be wsEn instead so that we can create correct keys. } if (fieldWs < 0) { // FindOrCreatePossibility has a bug where it doesn't handle "magic" writing systems (e.g., -1 for default analysis, etc) and // throws an exception instead. So we need to get a real ws here. fieldWs = WritingSystemServices.ActualWs(cache, fieldWs, hvo, flid); } ICmPossibilityList parentList = GetParentListForField(flid); ICmPossibility newPoss = parentList.FindOrCreatePossibility(nameHierarchy, fieldWs); int oldHvo = data.get_ObjectProp(hvo, flid); if (newPoss.Hvo == oldHvo) { return(false); } else { data.SetObjProp(hvo, flid, newPoss.Hvo); return(true); } } case CellarPropertyType.ReferenceCollection: case CellarPropertyType.ReferenceSequence: { if (value == null || value == BsonNull.Value) { // Can't write null to a collection or sequence in LCM; it's forbidden. So data.SetObjProp(hvo, flid, LcmCache.kNullHvo) will not work. // Instead, we delete all items from the existing collection or sequence, and thus store an empty coll/seq in LCM. int oldSize = data.get_VecSize(hvo, flid); if (oldSize == 0) { // It was already empty, so leave it unchanged so we don't cause unnecessary changes in the .fwdata XML (and unnecessary Mercurial commits). return(false); } else { data.Replace(hvo, flid, 0, oldSize, null, 0); // This is how you set an empty array return(true); } } int fieldWs = lcmMetaData.GetFieldWs(flid); // TODO: Investigate why this is sometimes coming back as 0 instead of as a real writing system ID if (fieldWs == 0) { fieldWs = cache.DefaultUserWs; } ICmPossibilityList parentList = GetParentListForField(flid); LfStringArrayField valueAsStringArray = BsonSerializer.Deserialize <LfStringArrayField>(value.AsBsonDocument); // Step 1: Get ICmPossibility instances from the string keys that LF gave us // First go through all the GUIDs we have and match them up to the keys. If they match up, // then remove the keys from the list. Any remaining keys get looked up with FindOrCreatePossibility(), so now we // have a complete set of GUIDs (or ICmPossibility objects, which works out to the same thing). // TODO: This is all kind of ugly, and WAY too long for one screen. I could put it in its own function, // but there's really no real gain from that, as it simply moves the logic even further away from where // it needs to be. There's not really a *good* way to achieve simplicity with this code design, unfortunately. // The only thing that would be close to simple would be to call some functions from the LcmToMongo option list // converters, and that's pulling in code from the "wrong" direction, which has its own ugliness. Ugh. HashSet <string> keysFromLF = new HashSet <string>(valueAsStringArray.Values); var fieldObjs = new List <ICmPossibility>(); foreach (Guid guid in fieldGuids) { ICmPossibility poss; string key = ""; if (guid != default(Guid)) { poss = servLoc.GetInstance <ICmPossibilityRepository>().GetObject(guid); if (poss == null) { // TODO: Decide what to do with possibilities deleted from LCM key = ""; } else { if (poss.Abbreviation == null) { key = ""; } else { ITsString keyTss = poss.Abbreviation.get_String(wsEn); key = keyTss == null ? "" : keyTss.Text ?? ""; } fieldObjs.Add(poss); } } keysFromLF.Remove(key); // Ignoring return value (HashSet.Remove returns false if the key wasn't present), because false could mean one of two things: // 1. The CmPossibility had its English abbreviation changed in LCM, but LF doesn't know this yet. // If this is the case, the LF key won't match, but the GUID will still match. So we might end up creating // duplicate entries below with the FindOrCreatePossibility. TODO: Need to verify that LCM->LF possibility lists // get updated correctly if renames happen! (... Or use the OptionList converters, that's what they were for.) // 2. The CmPossibility was just created in LF and isn't in LCM yet. In which case we should have been using the // OptionList converters, which would hopefully have handled creating the ICmPossibility instane in LCM. // Either way, we can't really use that fact later, since we can't be certain if the possibility was renamed or created. } // Any remaining keysFromLF strings did not have corresponding GUIDs in Mongo. // This is most likely because they were added by LF, which doesn't write to the customFieldGuids field. // So we assume they exist in FW, and just look them up. foreach (string key in keysFromLF) { ICmPossibility poss = parentList.FindOrCreatePossibility(key, wsEn); // TODO: If this is a new possibility, then we need to populate it with ALL the corresponding data from LF, // which we don't necessarily have at this point. Need to make that a separate step in the Send/Receive: converting option lists first. fieldObjs.Add(poss); } // logger.Debug("Custom field {0} for CmObject {1}: BSON list was [{2}] and customFieldGuids was [{3}]. This was translated to keysFromLF = [{4}] and fieldObjs = [{5}]", // fieldName, // hvo, // String.Join(", ", valueAsStringArray.Values), // String.Join(", ", fieldGuids.Select(g => g.ToString())), // 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 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 <int> newHvos = new HashSet <int>(newHvosArray); HashSet <int> combinedHvos = new HashSet <int>(); // 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); } case CellarPropertyType.String: { var valueAsMultiText = BsonSerializer.Deserialize <LfMultiText>(value.AsBsonDocument); int wsIdForField = lcmMetaData.GetFieldWs(flid); string wsStrForField = servLoc.WritingSystemFactory.GetStrFromWs(wsIdForField); KeyValuePair <string, string> kv = valueAsMultiText.BestStringAndWs(new string[] { wsStrForField }); string foundWs = kv.Key ?? string.Empty; string foundData = kv.Value ?? string.Empty; int foundWsId = servLoc.WritingSystemFactory.GetWsFromStr(foundWs); if (foundWsId == 0) { return(false); // Skip any unidentified writing systems } ITsString oldValue = data.get_StringProp(hvo, flid); ITsString newValue = ConvertMongoToLcmTsStrings.SpanStrToTsString(foundData, foundWsId, servLoc.WritingSystemFactory); if (oldValue != null && TsStringUtils.GetDiffsInTsStrings(oldValue, newValue) == null) // GetDiffsInTsStrings() returns null when there are no changes { return(false); } else { data.SetString(hvo, flid, newValue); return(true); } } default: return(false); // TODO: Maybe issue a proper warning (or error) log message for "field type not recognized"? } }
// Assumption: "source" contains valid keys. CAUTION: No error checking is done to ensure that this is true. public void UpdatePossibilitiesFromStringArray <T>(ILcmReferenceCollection <T> dest, LfStringArrayField source) where T : class, ICmPossibility { SetPossibilitiesCollection(dest, FromStringArrayField <T>(source)); }
protected bool _ShouldSerializeLfStringArrayField(LfStringArrayField value) { return value != null && !value.IsEmpty; }