diff --git a/DistFiles/Language Explorer/Export Templates/Interlinear/FlexInterlinear.xsd b/DistFiles/Language Explorer/Export Templates/Interlinear/FlexInterlinear.xsd index 53179901c6..ebcefe7114 100644 --- a/DistFiles/Language Explorer/Export Templates/Interlinear/FlexInterlinear.xsd +++ b/DistFiles/Language Explorer/Export Templates/Interlinear/FlexInterlinear.xsd @@ -1,4 +1,4 @@ - + @@ -7,6 +7,21 @@ + + + + + + + + + + + + + + + @@ -126,6 +141,7 @@ + @@ -153,7 +169,16 @@ - + + + + + + + + + + diff --git a/Src/LexText/Interlinear/BIRDInterlinearImporter.cs b/Src/LexText/Interlinear/BIRDInterlinearImporter.cs index 7bef0eba84..e71052d0c6 100644 --- a/Src/LexText/Interlinear/BIRDInterlinearImporter.cs +++ b/Src/LexText/Interlinear/BIRDInterlinearImporter.cs @@ -2,24 +2,24 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Windows.Forms; -using SIL.LCModel.Core.Text; -using SIL.LCModel.Core.WritingSystems; -using SIL.LCModel.Core.KernelInterfaces; +using SIL.Extensions; using SIL.FieldWorks.Common.FwUtils; -using SIL.LCModel; -using SIL.LCModel.DomainServices; using SIL.FieldWorks.IText.FlexInterlinModel; +using SIL.LCModel; using SIL.LCModel.Application.ApplicationServices; using SIL.LCModel.Core.Cellar; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Core.Text; +using SIL.LCModel.Core.WritingSystems; +using SIL.LCModel.DomainServices; using SIL.LCModel.Infrastructure; using SIL.LCModel.Utils; -using SIL.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows.Forms; namespace SIL.FieldWorks.IText { @@ -87,13 +87,14 @@ private static bool PopulateTextFromBIRDDoc(ref LCModel.IText newText, TextCreat //handle the header(info or meta) information SetTextMetaAndMergeMedia(cache, interlinText, wsFactory, newText, false); + if (newText.ContentsOA == null) + { + // Create ContentsOA even if there are no paragraphs. + newText.ContentsOA = cache.ServiceLocator.GetInstance().Create(); + } //create all the paragraphs foreach (var paragraph in interlinText.paragraphs) { - if (newText.ContentsOA == null) - { - newText.ContentsOA = cache.ServiceLocator.GetInstance().Create(); - } IStTxtPara newTextPara = newText.ContentsOA.AddNewTextPara(""); int offset = 0; if (paragraph.phrases == null) @@ -1138,25 +1139,27 @@ private static void UpgradeToWordGloss(Word word, ref IAnalysis wordForm) private static void SetTextMetaAndMergeMedia(LcmCache cache, Interlineartext interlinText, ILgWritingSystemFactory wsFactory, LCModel.IText newText, bool merging) { - if (interlinText.Items != null) // apparently it is null if there are no items. + InterlinearObjects objects = new InterlinearObjects(); + + // Set top-level metadata properties. + SetObjectPropertyValues(newText, interlinText.Items, objects.GetXmlPropertyMap("Text"), cache); + + // Create objects except for links. + if (interlinText.objects != null) { - foreach (var item in interlinText.Items) + foreach (var obj in interlinText.objects) { - switch (item.type) - { - case "title": - newText.Name.set_String(GetWsEngine(wsFactory, item.lang).Handle, item.Value); - break; - case "title-abbreviation": - newText.Abbreviation.set_String(GetWsEngine(wsFactory, item.lang).Handle, item.Value); - break; - case "source": - newText.Source.set_String(GetWsEngine(wsFactory, item.lang).Handle, item.Value); - break; - case "comment": - newText.Description.set_String(GetWsEngine(wsFactory, item.lang).Handle, item.Value); - break; - } + CreateFullObject(obj, objects, cache); + } + } + + // Create links after all objects have been created. + CreateItemLinks(interlinText.guid, interlinText.Items, objects, cache); + if (interlinText.objects != null) + { + foreach (var obj in interlinText.objects) + { + CreateItemLinks(obj.guid, obj.item, objects, cache); } } @@ -1194,5 +1197,290 @@ private static void SetTextMetaAndMergeMedia(LcmCache cache, Interlineartext int } } } + + /// + /// Create object and fill in item properties. + /// + private static void CreateFullObject(Interlineartext.Object obj, InterlinearObjects objects, LcmCache cache) + { + Guid guid = new Guid(obj.guid); + ICmObjectRepository repository = cache.ServiceLocator.GetInstance(); + if (!repository.TryGetObject(guid, out ICmObject icmObject)) + { + icmObject = CreateObject(obj, objects, cache); + Dictionary xmlPropertyMap = objects.GetXmlPropertyMap(obj.type); + SetObjectPropertyValues(icmObject, obj.item, xmlPropertyMap, cache); + } + } + + private static void SetObjectPropertyValues(ICmObject icmObject, item[] items, Dictionary xmlPropertyMap, LcmCache cache) + { + if (items == null) return; + Type objType = icmObject.GetType(); + /// Set item properties. + foreach (var item in items) + { + if (item.guid != null) + { + continue; + } + if (item.type == "owner" && icmObject is ICmPossibility possibility) + { + // Add possibility to a possibility list rooted in LanguageProject. + foreach (PropertyInfo langPropInfo in cache.LanguageProject.GetType().GetProperties()) + { + string langPropName = langPropInfo.Name; + if (langPropName.EndsWith("OA")) + langPropName = langPropName.Substring(0, langPropName.Length - 2); + if (langPropName == item.Value) + { + var langPropValue = langPropInfo.GetValue(cache.LanguageProject); + if (langPropValue is ICmPossibilityList possibilityList) + { + possibilityList.PossibilitiesOS.Add(possibility); + } + } + } + continue; + } + object value = null; + string propName = null; + if (xmlPropertyMap.ContainsKey(item.type)) + propName = xmlPropertyMap[item.type]; + else if (item.type == "date-created") + propName = "DateCreated"; + else if (item.type == "date-modified") + propName = "DateModified"; + PropertyInfo propInfo = objType.GetProperty(propName); + object currentValue = propInfo.GetValue(icmObject, null); + if (currentValue is bool) + { + value = item.Value.ToLower() == "true"; + } + else if (currentValue is DateTime) + { + value = DateTime.Parse(item.Value, null, System.Globalization.DateTimeStyles.AssumeUniversal); + } + else if (currentValue is IMultiString) + { + // value is an ITsString. + int ws = GetWsEngine(cache.WritingSystemFactory, item.lang).Handle; + value = TsStringUtils.MakeString(item.Value, ws); + } + else if (currentValue is IMultiUnicode) + { + // value is an ITsString. + int ws = GetWsEngine(cache.WritingSystemFactory, item.lang).Handle; + value = TsStringUtils.MakeString(item.Value, ws); + } + else if (currentValue is int) + { + int intValue = 0; + if (int.TryParse(item.Value, out intValue)) + value = intValue; + } + SetPropertyValue(icmObject, propName, value); + } + } + + /// + /// Create object. + /// + private static ICmObject CreateObject(Interlineartext.Object obj, InterlinearObjects objects, LcmCache cache) + { + string objType = objects.XmlTypeMap[obj.type]; + switch (objType) + { + case "CmAnthroItem": + return cache.ServiceLocator.GetInstance().Create(new Guid(obj.guid)); + case "CmLocation": + return cache.ServiceLocator.GetInstance().Create(new Guid(obj.guid)); + case "CmPerson": + return cache.ServiceLocator.GetInstance().Create(new Guid(obj.guid)); + case "CmPossibility": + return cache.ServiceLocator.GetInstance().Create(new Guid(obj.guid)); + case "RnGenericRec": + IRnGenericRec record = cache.ServiceLocator.GetInstance().Create(new Guid(obj.guid)); + cache.LanguageProject.ResearchNotebookOA.RecordsOC.Add(record); + return record; + case "RnRoledPartic": + return cache.ServiceLocator.GetInstance().Create(new Guid(obj.guid)); + } + return null; + } + + private static void CreateItemLinks(string objGuid, item[] items, InterlinearObjects objects, LcmCache cache) + { + if (items != null) + { + foreach (var item in items) + { + if (item.guid != null) + { + CreateLink(objGuid, item.type, item.guid, item.lang, item.Value, objects, cache); + } + } + } + } + + + /// + /// Create given link. + /// + private static void CreateLink(string objGuid, string xmlPropName, string valueGuid, string lang, string valueName, InterlinearObjects objects, LcmCache cache) + { + ICmObjectRepository repository = cache.ServiceLocator.GetInstance(); + ICmObject obj = repository.GetObject(new Guid(objGuid)); + Dictionary xmlPropertyMap = objects.InvertMap(objects.GetPropertyMap(obj.GetType().Name)); + string propName = (xmlPropName == "owner") ? "Owner" : xmlPropertyMap[xmlPropName]; + ICmObject value = null; + if (!repository.TryGetObject(new Guid(valueGuid), out value)) + { + value = CreateObjectByName(obj, propName, lang, valueName, valueGuid, cache); + } + SetPropertyValue(obj, propName, value); + } + + private static ICmObject CreateObjectByName(ICmObject obj, string propName, string lang, string valueName, string valueGuid, LcmCache cache) + { + ICmPossibilityList possibilityList = null; + string possibilityType = "ICmPossibility"; + switch (propName) + { + case "AnthroCodesRC": + { + possibilityList = cache.LanguageProject.AnthroListOA; + possibilityType = "ICmAnthroItem"; + break; + } + case "GenresRC": + { + possibilityList = cache.LanguageProject.GenreListOA; + break; + } + case "ParticipantsRC": + case "ResearchersRC": + case "Source": + { + possibilityList = cache.LanguageProject.PeopleOA; + possibilityType = "ICmPerson"; + break; + } + case "RoleRA": + { + possibilityList = cache.LanguageProject.RolesOA; + break; + } + case "LocationsRC": + { + possibilityList = cache.LanguageProject.LocationsOA; + possibilityType = "ICmLocation"; + break; + } + } + if (possibilityList == null) + { + return null; + } + // Look for an existing possibility with the given name. + int ws = GetWsEngine(cache.WritingSystemFactory, lang).Handle; + foreach (var candidate in possibilityList.ReallyReallyAllPossibilities) + { + ITsString name = candidate.Name.BestAnalysisAlternative; + if (name.Text == valueName && name.get_WritingSystemAt(0) == ws) + { + // Should we set the candidate's guid to valueGuid? + return candidate; + } + } + // Create a new possibility. + ICmPossibility newPossibility = null; + Guid guid = new Guid(valueGuid); + switch (possibilityType) + { + case "ICmAnthroItem": + newPossibility = cache.ServiceLocator.GetInstance().Create(guid); + break; + case "ICmLocation": + newPossibility = cache.ServiceLocator.GetInstance().Create(guid); + break; + case "ICmPerson": + newPossibility = cache.ServiceLocator.GetInstance().Create(guid); + break; + case "ICmPossibility": + newPossibility = cache.ServiceLocator.GetInstance().Create(guid); + break; + } + if (newPossibility != null) + { + newPossibility.Name.set_String(ws, valueName); + possibilityList.PossibilitiesOS.Add(newPossibility); + } + return newPossibility; + } + + /// + /// Set object property to value. + private static void SetPropertyValue(ICmObject obj, string propName, object value) + { + if (value == null) + return; + if (propName == "Owner") + { + // We store Owner but set SubPossibilitiesOS. + SetPropertyValue((ICmObject)value, "SubPossibilitiesOS", obj); + return; + } + if (propName == "AssociatedNotebookRecord") + { + SetPropertyValue((ICmObject)value, "TextRA", obj); + return; + } + PropertyInfo propInfo = obj.GetType().GetProperty(propName); + object currentValue = propInfo.GetValue(obj, null); + if (currentValue == null) + { + propInfo.SetValue(obj, value); + return; + } + + Type currentValueType = currentValue.GetType(); + if (value.GetType().IsInstanceOfType(currentValueType)) + { + propInfo.SetValue(obj, value); + } + else if (currentValueType.IsGenericType && + (currentValueType.GetGenericTypeDefinition().Name == "LcmOwningCollection`1" || + currentValueType.GetGenericTypeDefinition().Name == "LcmOwningSequence`1" || + currentValueType.GetGenericTypeDefinition().Name == "LcmReferenceCollection`1" || + currentValueType.GetGenericTypeDefinition().Name == "LcmReferenceSequence`1")) + { + Type itemType = currentValueType.GetGenericArguments()[0]; + if (itemType.IsAssignableFrom(value.GetType())) + { + var addMethod = currentValueType.GetMethod("Add"); + addMethod?.Invoke(currentValue, new[] { value }); + } + } + else if (currentValue is IMultiString multiString) + { + if (value is ITsString itsString) + { + multiString.set_String(itsString.get_WritingSystemAt(0), itsString); + } + } + else if (currentValue is IMultiUnicode multiUnicode) + { + if (value is ITsString itsString) + { + multiUnicode.set_String(itsString.get_WritingSystemAt(0), itsString.Text); + } + } + else + { + propInfo.SetValue(obj, value); + } + } + } } \ No newline at end of file diff --git a/Src/LexText/Interlinear/FlexInterlinModel/FlexInterlinear.cs b/Src/LexText/Interlinear/FlexInterlinModel/FlexInterlinear.cs index bd8f863236..1e44348471 100644 --- a/Src/LexText/Interlinear/FlexInterlinModel/FlexInterlinear.cs +++ b/Src/LexText/Interlinear/FlexInterlinModel/FlexInterlinear.cs @@ -1,5 +1,6 @@ //------------------------------------------------------------------------------ // +// Generated by: xsd FlexInterlinear.xsd /classes in Visual Studio's Developer Command Prompt // This code was generated by a tool - however, it has been heavily massaged, since the tool is kind of broken -NaylorJ // Runtime Version:2.0.50727.5446 // @@ -109,6 +110,8 @@ public partial class Interlineartext private item[] itemField; + private Object[] objectsField; + private Paragraph[] paragraphsField; private Languages languagesField; @@ -137,6 +140,79 @@ public item[] Items } } + /// + [System.Xml.Serialization.XmlArrayAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] + [System.Xml.Serialization.XmlArrayItemAttribute("object", Form = System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable = false)] + public Object[] objects + { + get + { + return this.objectsField; + } + set + { + this.objectsField = value; + } + } + + /// + [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] + [System.SerializableAttribute()] + [System.Diagnostics.DebuggerStepThroughAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class Object + { + + private item[] itemField; + + private string guidField; + + private string typeField; + + /// + [System.Xml.Serialization.XmlElementAttribute("item", IsNullable = true)] + public item[] item + { + get + { + return this.itemField; + } + set + { + this.itemField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string guid + { + get + { + return this.guidField; + } + set + { + this.guidField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string type + { + get + { + return this.typeField; + } + set + { + this.typeField = value; + } + } + } + /// [System.Xml.Serialization.XmlArrayAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] [System.Xml.Serialization.XmlArrayItemAttribute("paragraph", Form = System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable = false)] @@ -247,6 +323,8 @@ public partial class item private string typeField; + private string guidField; + private string langField; private analysisStatusTypes analysisStatusField; @@ -283,6 +361,20 @@ public string lang } } + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string guid + { + get + { + return this.guidField; + } + set + { + this.guidField = value; + } + } + /// [System.Xml.Serialization.XmlAttributeAttribute()] public analysisStatusTypes analysisStatus diff --git a/Src/LexText/Interlinear/ITextDll.csproj b/Src/LexText/Interlinear/ITextDll.csproj index ee00d22841..f8bc92779d 100644 --- a/Src/LexText/Interlinear/ITextDll.csproj +++ b/Src/LexText/Interlinear/ITextDll.csproj @@ -460,6 +460,7 @@ InterlinDocForAnalysis.cs + Form diff --git a/Src/LexText/Interlinear/ITextDllTests/BIRDFormatImportTests.cs b/Src/LexText/Interlinear/ITextDllTests/BIRDFormatImportTests.cs index dc0e205ba1..4d1f0f27dd 100644 --- a/Src/LexText/Interlinear/ITextDllTests/BIRDFormatImportTests.cs +++ b/Src/LexText/Interlinear/ITextDllTests/BIRDFormatImportTests.cs @@ -761,10 +761,19 @@ public void OneOfEachElementTypeTest() { string title = "atrocious"; string abbr = "atroc"; + string source = "source"; + string description = "description"; + string dateCreated = "2006-08-23 19:31:09.500"; + string dateModified = "2006-09-14 13:46:01.247"; //an interliner text example xml string string xml = "" + "" + title + "" + "" + abbr + "" + + "" + source + "" + + "" + description + "" + + "true" + + "" + dateCreated + "" + + "" + dateModified + "" + "" + "1 Musical" + "origem: mary poppins" + @@ -785,6 +794,68 @@ public void OneOfEachElementTypeTest() Assert.True(imported.Name.get_String(Cache.WritingSystemFactory.get_Engine("en").Handle).Text.Equals(title)); //The title abbreviation imported Assert.True(imported.Abbreviation.get_String(Cache.WritingSystemFactory.get_Engine("en").Handle).Text.Equals(abbr)); + //The source imported + Assert.True(imported.Source.get_String(Cache.WritingSystemFactory.get_Engine("en").Handle).Text.Equals(source)); + //The description imported + Assert.True(imported.Description.get_String(Cache.WritingSystemFactory.get_Engine("en").Handle).Text.Equals(description)); + //The isTranslated imported + Assert.True(imported.IsTranslated); + //The Dates imported + string importedDateCreated = imported.DateCreated.ToLCMTimeFormatWithMillisString(); + Assert.True(importedDateCreated.Equals(dateCreated)); + string importedDateModified = imported.DateModified.ToLCMTimeFormatWithMillisString(); + Assert.True(importedDateModified.Equals(dateModified)); + } + } + } + + [Test] + public void TestGenres() + { + string title = "atrocious"; + string textGuid = "a122d9bb-2d43-4e4c-b74f-6fe44d1c6cb3"; + string genre1Guid = "b405f3c0-58e1-4492-8a40-e955774a6912"; + string genre2Guid = "45e6f056-98ac-45d6-858e-59450993f269"; + string genre1Name = "genre1"; + string genre2Name = "genre2"; + //an interliner text example xml string + string xml = "" + + "" + title + "" + + "" + genre1Name + "" + + "" + genre2Name + "" + + "" + + "\n" + + "" + + "1 Musical" + + "origem: mary poppins" + + "supercalifragilisticexpialidocious" + + "absurdo" + + ""; + + NonUndoableUnitOfWorkHelper.Do(Cache.ActionHandlerAccessor, () => + { + Cache.LanguageProject.GenreListOA = Cache.ServiceLocator.GetInstance().Create(); + }); + LinguaLinksImport li = new LinguaLinksImport(Cache, null, null); + LCModel.IText text = null; + using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(xml.ToCharArray()))) + { + li.ImportInterlinear(new DummyProgressDlg(), stream, 0, ref text); + using (var firstEntry = Cache.LanguageProject.Texts.GetEnumerator()) + { + firstEntry.MoveNext(); + var imported = firstEntry.Current; + Assert.AreEqual(2, imported.GenresRC.Count); + Assert.AreEqual(genre1Guid, imported.GenresRC.First().Guid.ToString()); + Assert.AreEqual(genre1Name, imported.GenresRC.First().Name.BestAnalysisAlternative.Text); + Assert.AreEqual(genre2Guid, imported.GenresRC.Last().Guid.ToString()); + Assert.AreEqual(genre2Name, imported.GenresRC.Last().Name.BestAnalysisAlternative.Text); + ILcmOwningSequence genres = imported.Cache.LanguageProject.GenreListOA.PossibilitiesOS; + Assert.AreEqual(2, genres.Count); + Assert.AreEqual(genre1Guid, genres.First().Guid.ToString()); + Assert.AreEqual(genre1Name, genres.First().Name.BestAnalysisAlternative.Text); + Assert.AreEqual(genre2Guid, genres.Last().Guid.ToString()); + Assert.AreEqual(genre2Name, genres.Last().Name.BestAnalysisAlternative.Text); } } } diff --git a/Src/LexText/Interlinear/ITextDllTests/FlexTextImport/FlexTextMetadataImport.flextext b/Src/LexText/Interlinear/ITextDllTests/FlexTextImport/FlexTextMetadataImport.flextext new file mode 100644 index 0000000000..56452ede7d --- /dev/null +++ b/Src/LexText/Interlinear/ITextDllTests/FlexTextImport/FlexTextMetadataImport.flextext @@ -0,0 +1,35 @@ + + + + Derivation Test + Test of derivations + true + + Paradigm + Prose + 2024-08-27 16:46:12.972 + 2025-09-30 16:04:20.323 + + + John Smith + + + John Smith + California + derivations + + + John Smith + + + John Smith + Arbitrator + + + + + + + + + \ No newline at end of file diff --git a/Src/LexText/Interlinear/ITextDllTests/ITextDllTests.csproj b/Src/LexText/Interlinear/ITextDllTests/ITextDllTests.csproj index ba76083fff..d7cf406c08 100644 --- a/Src/LexText/Interlinear/ITextDllTests/ITextDllTests.csproj +++ b/Src/LexText/Interlinear/ITextDllTests/ITextDllTests.csproj @@ -329,6 +329,7 @@ + diff --git a/Src/LexText/Interlinear/ITextDllTests/InterlinearExporterTests.cs b/Src/LexText/Interlinear/ITextDllTests/InterlinearExporterTests.cs index d3fb24c376..e75c733bee 100644 --- a/Src/LexText/Interlinear/ITextDllTests/InterlinearExporterTests.cs +++ b/Src/LexText/Interlinear/ITextDllTests/InterlinearExporterTests.cs @@ -2,21 +2,22 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -using System; -using System.Collections; -using System.IO; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Xsl; using NUnit.Framework; -using SIL.LCModel.Core.Text; -using SIL.LCModel.Core.WritingSystems; -using SIL.LCModel.Core.KernelInterfaces; using SIL.FieldWorks.Common.FwUtils; using SIL.LCModel; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Core.Text; +using SIL.LCModel.Core.WritingSystems; using SIL.LCModel.DomainServices; using SIL.PlatformUtilities; using SIL.TestUtilities; +using System; +using System.Collections; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Xsl; namespace SIL.FieldWorks.IText { @@ -55,9 +56,14 @@ public void Exit() protected XmlDocument ExportToXml(string mode) { XmlDocument exportedXml = new XmlDocument(); + var settings = new XmlWriterSettings + { + Encoding = System.Text.Encoding.UTF8, + Indent = true + }; using (var vc = new InterlinVc(Cache)) using (var stream = new MemoryStream()) - using (var writer = new XmlTextWriter(stream, System.Text.Encoding.UTF8)) + using (var writer = XmlWriter.Create(stream, settings)) { vc.LineChoices = m_choices; var exporter = InterlinearExporter.Create(mode, Cache, writer, m_text1.ContentsOA, m_choices, vc); @@ -545,6 +551,8 @@ public void ExportVariantTypeInformation_LT9374_xml2OO_multipleWss() freeVarType.ReverseAbbr.SetAnalysisDefaultWritingSystem("fr. var."); pa.SetVariantOf(0, 1, leGo, freeVarType); pa.ReparseParagraph(); + m_text1.DateCreated = DateTime.MinValue; + m_text1.DateModified = DateTime.MinValue; exportedDoc = ExportToXml(); //validate export xml against schema @@ -762,6 +770,8 @@ public void ExportIrrInflVariantTypeInformation_LT7581_glsAppend_xml2OO_multiple IStTxtPara para1 = m_text1.ContentsOA.ParagraphsOS[1] as IStTxtPara; ParagraphAnnotator pa = new ParagraphAnnotator(para1); pa.ReparseParagraph(); + m_text1.DateCreated = DateTime.MinValue; + m_text1.DateModified = DateTime.MinValue; var exportedDoc = ExportToXml(); string formLexEntry = "go"; @@ -827,6 +837,8 @@ public void ExportIrrInflVariantTypeInformation_LT7581_glsAppend_varianttypes_xm IStTxtPara para1 = m_text1.ContentsOA.ParagraphsOS[1] as IStTxtPara; ParagraphAnnotator pa = new ParagraphAnnotator(para1); pa.ReparseParagraph(); + m_text1.DateCreated = DateTime.MinValue; + m_text1.DateModified = DateTime.MinValue; var exportedDoc = ExportToXml(); string formLexEntry = "go"; @@ -888,6 +900,8 @@ public void ExportIrrInflVariantTypeInformation_LT7581_glsAppend_varianttypes_xm IStTxtPara para1 = m_text1.ContentsOA.ParagraphsOS[1] as IStTxtPara; ParagraphAnnotator pa = new ParagraphAnnotator(para1); pa.ReparseParagraph(); + m_text1.DateCreated = DateTime.MinValue; + m_text1.DateModified = DateTime.MinValue; var exportedDoc = ExportToXml(); string formLexEntry = "go"; @@ -1111,6 +1125,69 @@ public void ValidateMultipleComments() AssertThatXmlIn.Dom(exportedDoc).HasSpecifiedNumberOfMatchesForXpath("//interlinear-text/item[@type=\"comment\"]", 2); } + [Test] + public void ValidateMultipleGenres() + { + Cache.LanguageProject.GenreListOA = Cache.ServiceLocator.GetInstance().Create(); + var genre1 = Cache.LanguageProject.GenreListOA.FindOrCreatePossibility("genre1", Cache.DefaultAnalWs); + var genre2 = Cache.LanguageProject.GenreListOA.FindOrCreatePossibility("genre2", Cache.DefaultAnalWs); + m_text1.GenresRC.Add(genre1); + m_text1.GenresRC.Add(genre2); + XmlDocument exportedDoc = ExportToXml(); + AssertThatXmlIn.Dom(exportedDoc).HasSpecifiedNumberOfMatchesForXpath("//interlinear-text/item[@type=\"genre\"]", 2); + } + + [Test] + public void TestMetadataRoundTrip() + { + //an interliner text example xml string + string path = Path.Combine(FwDirectoryFinder.SourceDirectory, @"LexText/Interlinear/ITextDllTests/FlexTextImport"); + string file = Path.Combine(path, "FlexTextMetadataImport.flextext"); + XmlDocument doc = new XmlDocument(); + doc.Load(file); + string xml = doc.OuterXml; + ILgWritingSystemFactory wsFactory = Cache.WritingSystemFactory; + + var writingSystem = wsFactory.get_Engine("es"); + Cache.LanguageProject.AddToCurrentAnalysisWritingSystems((CoreWritingSystemDefinition)writingSystem); + writingSystem = wsFactory.get_Engine("en"); + Cache.LanguageProject.AddToCurrentVernacularWritingSystems((CoreWritingSystemDefinition)writingSystem); + Cache.LanguageProject.GenreListOA = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PositionsOA = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.RestrictionsOA = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.EducationOA = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.RolesOA = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.StatusOA = Cache.ServiceLocator.GetInstance().Create(); + LinguaLinksImport li = new LinguaLinksImport(Cache, null, null); + LCModel.IText text = null; + using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(xml.ToCharArray()))) + { + li.ImportInterlinear(new DummyProgressDlg(), stream, 0, ref text); + m_text1 = text; + XmlDocument exportedDoc = ExportToXml("elan"); + string exportedXml = exportedDoc.OuterXml; + Assert.That(exportedXml, Is.EqualTo(xml)); + } + } + + [Test] + public void ValidateIsTranslated() + { + m_text1.IsTranslated = true; + XmlDocument exportedDoc = ExportToXml(); + AssertThatXmlIn.Dom(exportedDoc).HasSpecifiedNumberOfMatchesForXpath("//interlinear-text/item[@type=\"text-is-translation\"]", 1); + } + + [Test] + public void ValidateDates() + { + m_text1.DateCreated = DateTime.Now; + m_text1.DateModified = DateTime.Now; + XmlDocument exportedDoc = ExportToXml(); + AssertThatXmlIn.Dom(exportedDoc).HasSpecifiedNumberOfMatchesForXpath("//interlinear-text/item[@type=\"date-created\"]", 1); + AssertThatXmlIn.Dom(exportedDoc).HasSpecifiedNumberOfMatchesForXpath("//interlinear-text/item[@type=\"date-modified\"]", 1); + } + /// /// Create two paragraphs with two identical sentences. The first paragraph has real analyses, the second has only guesses. /// Validate that the guids for each paragraph, and each phrase and word annotation are unique. diff --git a/Src/LexText/Interlinear/InterlinearExporter.cs b/Src/LexText/Interlinear/InterlinearExporter.cs index e0524ff75c..593b63e85a 100644 --- a/Src/LexText/Interlinear/InterlinearExporter.cs +++ b/Src/LexText/Interlinear/InterlinearExporter.cs @@ -2,17 +2,20 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Xml; -using SIL.LCModel.Core.WritingSystems; -using SIL.LCModel.Core.KernelInterfaces; -using SIL.FieldWorks.Common.ViewsInterfaces; using SIL.FieldWorks.Common.RootSites; +using SIL.FieldWorks.Common.ViewsInterfaces; using SIL.LCModel; +using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Core.Text; +using SIL.LCModel.Core.WritingSystems; using SIL.LCModel.DomainServices; using SIL.LCModel.Infrastructure; +using SIL.LCModel.Utils; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Xml; namespace SIL.FieldWorks.IText { @@ -40,6 +43,8 @@ public class InterlinearExporter : CollectorEnv List pendingSources = new List(); private List pendingAbbreviations = new List(); List pendingComments = new List(); + bool pendingIsTranslated = false; + Queue pendingObjects = new Queue(); int m_flidStTextTitle; int m_flidStTextSource; InterlinVc m_vc = null; @@ -50,6 +55,7 @@ public class InterlinearExporter : CollectorEnv IMoMorphType m_mmtProclitic; protected WritingSystemManager m_wsManager; protected ICmObjectRepository m_repoObj; + InterlinearObjects m_objects; public static InterlinearExporter Create(string mode, LcmCache cache, XmlWriter writer, ICmObject objRoot, InterlinLineChoices lineChoices, InterlinVc vc) @@ -90,6 +96,7 @@ protected InterlinearExporter(LcmCache cache, XmlWriter writer, ICmObject objRoo m_wsManager = m_cache.ServiceLocator.WritingSystemManager; m_repoObj = m_cache.ServiceLocator.GetInstance(); + m_objects = new InterlinearObjects(); } public void ExportDisplay() @@ -546,6 +553,162 @@ private void OpenItem(string itemType) m_fItemIsOpen = true; } + /// + /// Write a link to an object. + /// + private void WritePendingLink(string linkType, ICmObject obj) + { + if (obj == null) + return; + m_writer.WriteStartElement("item"); + m_writer.WriteAttributeString("type", linkType); + m_writer.WriteAttributeString("guid", obj.Guid.ToString()); + // Include name in case the guid isn't defined. + ITsString name; + if (obj is ICmPossibility possibility) + { + name = possibility.Name.BestAnalysisVernacularAlternative; + // Don't store possibility as object. + } + else + { + name = TsStringUtils.EmptyString(m_cache.DefaultAnalWs); + pendingObjects.Enqueue(obj); + } + WriteLangAndContent(GetWsFromTsString(name), name); + m_writer.WriteEndElement(); + } + + /// + /// Write an ICmObject as an object. + /// + private void WritePendingObject(ICmObject obj) + { + Type objType = obj.GetType(); + string typeName = objType.Name; + if (!m_objects.TypeMap.ContainsKey(typeName)) + return; + m_writer.WriteStartElement("object"); + m_writer.WriteAttributeString("type", m_objects.TypeMap[typeName]); + m_writer.WriteAttributeString("guid", obj.Guid.ToString()); + WritePendingObjectProperties(obj, null); + m_writer.WriteEndElement(); + } + + private void WritePendingObjectProperties(ICmObject obj, IList skipXmlProperties) + { + Type objType = obj.GetType(); + string typeName = objType.Name; + Dictionary propertyMap = m_objects.GetPropertyMap(typeName); + foreach (string propName in propertyMap.Keys) + { + PropertyInfo property = objType.GetProperty(propName); + object value = property.GetValue(obj, null); + if (value == null) + { + continue; + } + string xmlProperty = propertyMap[propName]; + if (skipXmlProperties != null && skipXmlProperties.Contains(xmlProperty)) + { + continue; + } + WritePendingProperty(xmlProperty, value); + } + if (obj is ICmMajorObject majorObj) + { + WritePendingProperty("date-created", majorObj.DateCreated); + WritePendingProperty("date-modified", majorObj.DateModified); + } + if (obj is ICmPossibility) + { + if (obj.Owner is ICmPossibilityList possibilityList) + { + WritePendingProperty("owner", GetPossibilityListName(possibilityList)); + } + else + { + WritePendingProperty("owner", obj.Owner); + } + } + } + + private ITsString GetPossibilityListName(ICmPossibilityList possibilityList) + { + foreach (var propInfo in m_cache.LangProject.GetType().GetProperties()) + { + object propValue = null; + try + { + propValue = propInfo.GetValue(m_cache.LangProject, null); + } + catch (Exception e) + { + + } + if (propValue == possibilityList) + { + string name = propInfo.Name; + if (name.EndsWith("OA")) + name = name.Substring(0, name.Length - 2); + return TsStringUtils.MakeString(name, m_cache.DefaultAnalWs); + } + } + return null; + } + + /// + /// Write property and value, dispatching on value type. + /// + /// + /// + private void WritePendingProperty(string propType, object value) + { + if (value == null) return; + if (value is ICmObject objectValue) + { + WritePendingLink(propType, objectValue); + } + else if (value is ITsString) + { + ITsString hystericalRaisens = (ITsString)value; + WritePendingItem(propType, ref hystericalRaisens); + } + else if (value is ITsMultiString multiString) + { + for (int i = 0; i < multiString.StringCount; i++) + { + ITsString hystericalRaisens = multiString.GetStringFromIndex(i, out int ws); + WritePendingItem(propType, ref hystericalRaisens); + } + } + else if (value is bool boolValue) + { + var hystericalRaisens = TsStringUtils.MakeString(boolValue ? "true" : "false", m_cache.DefaultAnalWs); + WritePendingItem(propType, ref hystericalRaisens); + } + else if (value is int intValue) + { + var hystericalRaisens = TsStringUtils.MakeString(intValue.ToString(), m_cache.DefaultAnalWs); + WritePendingItem(propType, ref hystericalRaisens); + } + else if (value is DateTime dateTime) + { + if (dateTime != DateTime.MinValue) + { + ITsString dateTimeString = TsStringUtils.MakeString(dateTime.ToLCMTimeFormatWithMillisString(), m_cache.DefaultAnalWs); + WritePendingItem(propType, ref dateTimeString); + } + } + else if (value is System.Collections.IEnumerable enumerable) + { + foreach (var item in enumerable) + { + WritePendingProperty(propType, item); + } + } + } + /// /// This method (as far as I know) will be first called on the StText object, and then recursively from the /// base implementation for vector items in component objects. @@ -572,16 +735,13 @@ public override void AddObjVecItems(int tag, IVwViewConstructor vc, int frag) { m_writer.WriteAttributeString("guid", text.Guid.ToString()); } + + // The next few properties can come from text or from the display. foreach (var mTssPendingTitle in pendingTitles) { var hystericalRaisens = mTssPendingTitle; WritePendingItem("title", ref hystericalRaisens); } - foreach (var mTssPendingAbbrev in pendingAbbreviations) - { - var hystericalRaisens = mTssPendingAbbrev; - WritePendingItem("title-abbreviation", ref hystericalRaisens); - } foreach(var source in pendingSources) { var hystericalRaisens = source; @@ -592,6 +752,27 @@ public override void AddObjVecItems(int tag, IVwViewConstructor vc, int frag) var hystericalRaisens = desc; WritePendingItem("comment", ref hystericalRaisens); } + // Only write out IsTranslated if it is true. + if (pendingIsTranslated) + { + WritePendingProperty("text-is-translation", true); + } + IList skipXmlProperties = new List() { "title", "source", "comment", "text-is-translation"}; + WritePendingObjectProperties(text, skipXmlProperties); + if (pendingObjects.Count > 0) + { + // Write out any objects that were referenced in an item. + HashSet writtenObjects = new HashSet(); + m_writer.WriteStartElement("objects"); + while (pendingObjects.Count > 0) + { + ICmObject pendingObject = pendingObjects.Dequeue(); + if (!writtenObjects.Contains(pendingObject)) + WritePendingObject(pendingObject); + writtenObjects.Add(pendingObject); + } + m_writer.WriteEndElement(); + } m_writer.WriteStartElement("paragraphs"); break; case InterlinVc.kfragParaSegment: @@ -785,11 +966,11 @@ private void SetTextTitleAndMetadata(IStText txt) { pendingComments.Add(text.Description.get_String(writingSystemId)); } + pendingIsTranslated = text.IsTranslated; } else if (TextSource.IsScriptureText(txt)) { pendingTitles.Add(txt.ShortNameTSS); - pendingAbbreviations.Add(null); } } } @@ -802,7 +983,7 @@ private void SetTextTitleAndMetadata(IStText txt) /// public class InterlinearExporterForElan : InterlinearExporter { - private const int kDocVersion = 2; + private const int kDocVersion = 3; protected internal InterlinearExporterForElan(LcmCache cache, XmlWriter writer, ICmObject objRoot, InterlinLineChoices lineChoices, InterlinVc vc) : base(cache, writer, objRoot, lineChoices, vc) diff --git a/Src/LexText/Interlinear/InterlinearObjects.cs b/Src/LexText/Interlinear/InterlinearObjects.cs new file mode 100644 index 0000000000..585cf4faab --- /dev/null +++ b/Src/LexText/Interlinear/InterlinearObjects.cs @@ -0,0 +1,100 @@ +using SIL.LCModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace SIL.FieldWorks.IText +{ + /// + /// InterlinearObjects provides a mapping between type and property names and XML names + /// for objects associated with interlinear texts. The standard mapping is used for export, + /// and the inverted mapping is used for import. + /// + internal class InterlinearObjects + { + private Dictionary m_typeMap; + private Dictionary m_xmlTypeMap; + + private readonly Dictionary> m_propertyMaps; + private readonly Dictionary> m_xmlPropertyMaps; + + internal Dictionary TypeMap + { get { return m_typeMap; } } + + internal Dictionary XmlTypeMap + { get { return m_xmlTypeMap; } } + + internal InterlinearObjects() + { + m_typeMap = new Dictionary + { + { "RnGenericRec", "NotebookRecord" }, + { "RnRoledPartic", "RoledParticipants" }, + { "Text", "Text" }, + }; + m_xmlTypeMap = new Dictionary(); + foreach (string type in m_typeMap.Keys) + { + m_xmlTypeMap[m_typeMap[type]] = type; + } + + m_propertyMaps = new Dictionary> + { + ["RnGenericRec"] = new Dictionary() + { + { "ResearchersRC", "researcher" }, + { "ParticipantsOC", "roled-participants" }, + { "SourcesRC", "source" }, + { "LocationsRC", "location" }, + { "AnthroCodesRC", "anthro-code" }, + }, + ["RnRoledPartic"] = new Dictionary() + { + { "ParticipantsRC", "participant" }, + { "RoleRA", "role" }, + }, + ["Text"] = new Dictionary() + { + { "Abbreviation", "title-abbreviation" }, + { "AssociatedNotebookRecord", "notebook-record" }, + { "Description", "comment" }, + { "GenresRC", "genre" }, + { "IsTranslated", "text-is-translation" }, + { "Name", "title" }, + { "Source", "source" }, + } + }; + m_xmlPropertyMaps = new Dictionary>(); + } + + internal Dictionary GetPropertyMap(string type) + { + return m_propertyMaps[type]; + } + + internal Dictionary GetXmlPropertyMap(string type) + { + if (m_xmlPropertyMaps.ContainsKey(type)) + return m_xmlPropertyMaps[type]; + + Dictionary propertyMap = GetPropertyMap(XmlTypeMap[type]); + Dictionary xmlPropertyMap = InvertMap(propertyMap); + m_xmlPropertyMaps.Add(type, xmlPropertyMap); + return xmlPropertyMap; + } + + internal Dictionary InvertMap(Dictionary propertyMap) + { + Dictionary invertedPropertyMap = new Dictionary(); + foreach (string name in propertyMap.Keys) + { + invertedPropertyMap[propertyMap[name]] = name; + } + return invertedPropertyMap; + } + + } +} diff --git a/Src/LexText/Interlinear/LinguaLinksImport.cs b/Src/LexText/Interlinear/LinguaLinksImport.cs index 57e3e76c2a..cb91451162 100644 --- a/Src/LexText/Interlinear/LinguaLinksImport.cs +++ b/Src/LexText/Interlinear/LinguaLinksImport.cs @@ -27,6 +27,7 @@ using System.Xml.Serialization; using SIL.LCModel.Core.Text; using SIL.LCModel.Core.KernelInterfaces; +using SIL.LCModel.Application; namespace SIL.FieldWorks.IText @@ -290,70 +291,72 @@ public bool ImportInterlinear(ImportInterlinearOptions options, ref LCModel.ITex firstNewText = null; BIRDDocument doc; int initialProgress = progress.Position; + LCModel.IText localFirstNewText = firstNewText; try { - m_cache.DomainDataByFlid.BeginNonUndoableTask(); - progress.Message = ITextStrings.ksInterlinImportPhase1of2; - var serializer = new XmlSerializer(typeof(BIRDDocument)); - doc = (BIRDDocument)serializer.Deserialize(birdData); - Normalize(doc); - int version = 0; - if (!string.IsNullOrEmpty(doc.version)) - int.TryParse(doc.version, out version); - progress.Position = initialProgress + allottedProgress / 2; - progress.Message = ITextStrings.ksInterlinImportPhase2of2; - if (doc.interlineartext != null) + NonUndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW(m_cache.ServiceLocator.GetInstance(), () => { - int step = 0; - foreach (var interlineartext in doc.interlineartext) + progress.Message = ITextStrings.ksInterlinImportPhase1of2; + var serializer = new XmlSerializer(typeof(BIRDDocument)); + doc = (BIRDDocument)serializer.Deserialize(birdData); + Normalize(doc); + int version = 0; + if (!string.IsNullOrEmpty(doc.version)) + int.TryParse(doc.version, out version); + progress.Position = initialProgress + allottedProgress / 2; + progress.Message = ITextStrings.ksInterlinImportPhase2of2; + if (doc.interlineartext != null) { - step++; - ILangProject langProject = m_cache.LangProject; - LCModel.IText newText = null; - if (!String.IsNullOrEmpty(interlineartext.guid)) + int step = 0; + foreach (var interlineartext in doc.interlineartext) { - ICmObject repoObj; - m_cache.ServiceLocator.ObjectRepository.TryGetObject(new Guid(interlineartext.guid), out repoObj); - newText = repoObj as LCModel.IText; - if (newText != null && ShowPossibleMergeDialog(progress) == DialogResult.Yes) - { - continueMerge = MergeTextWithBIRDDoc(ref newText, - new TextCreationParams - { - Cache = m_cache, - InterlinText = interlineartext, - Progress = progress, - ImportOptions = options, - Version = version - }); - } - else if (newText == null) + step++; + ILangProject langProject = m_cache.LangProject; + LCModel.IText newText = null; + if (!String.IsNullOrEmpty(interlineartext.guid)) { - newText = m_cache.ServiceLocator.GetInstance().Create(m_cache, new Guid(interlineartext.guid)); - continueMerge = PopulateTextIfPossible(options, ref newText, interlineartext, progress, version); + ICmObject repoObj; + m_cache.ServiceLocator.ObjectRepository.TryGetObject(new Guid(interlineartext.guid), out repoObj); + newText = repoObj as LCModel.IText; + if (newText != null && ShowPossibleMergeDialog(progress) == DialogResult.Yes) + { + continueMerge = MergeTextWithBIRDDoc(ref newText, + new TextCreationParams + { + Cache = m_cache, + InterlinText = interlineartext, + Progress = progress, + ImportOptions = options, + Version = version + }); + } + else if (newText == null) + { + newText = m_cache.ServiceLocator.GetInstance().Create(m_cache, new Guid(interlineartext.guid)); + continueMerge = PopulateTextIfPossible(options, ref newText, interlineartext, progress, version); + } + else //user said do not merge. + { + //ignore the Guid; we shouldn't create another text with the same guid + newText = m_cache.ServiceLocator.GetInstance().Create(); + continueMerge = PopulateTextIfPossible(options, ref newText, interlineartext, progress, version); + } } - else //user said do not merge. + else { - //ignore the Guid; we shouldn't create another text with the same guid newText = m_cache.ServiceLocator.GetInstance().Create(); continueMerge = PopulateTextIfPossible(options, ref newText, interlineartext, progress, version); } - } - else - { - newText = m_cache.ServiceLocator.GetInstance().Create(); - continueMerge = PopulateTextIfPossible(options, ref newText, interlineartext, progress, version); - } - if (!continueMerge) - break; - progress.Position = initialProgress + allottedProgress/2 + allottedProgress*step/2/doc.interlineartext.Length; - if (firstNewText == null) - firstNewText = newText; + if (!continueMerge) + break; + progress.Position = initialProgress + allottedProgress / 2 + allottedProgress * step / 2 / doc.interlineartext.Length; + if (localFirstNewText == null) + localFirstNewText = newText; + } + mergeSucceeded = continueMerge; } - mergeSucceeded = continueMerge; - - } + }); } catch (Exception e) { @@ -361,10 +364,7 @@ public bool ImportInterlinear(ImportInterlinearOptions options, ref LCModel.ITex Debug.Print(e.Message); Debug.Print(e.StackTrace); } - finally - { - m_cache.DomainDataByFlid.EndNonUndoableTask(); - } + firstNewText = localFirstNewText; return mergeSucceeded; }