diff --git a/NodeSetToAML.cs b/NodeSetToAML.cs index ec93bb2..0939b44 100644 --- a/NodeSetToAML.cs +++ b/NodeSetToAML.cs @@ -586,27 +586,109 @@ private void AddBaseNodeClassAttributes( AttributeSequence seq, UANode uanode, U uriatt.Value = myuri; } - if (uanode.DisplayName != null && - uanode.DisplayName.Length > 0 && - uanode.DisplayName[0].Value != uanode.DecodedBrowseName.Name) + BuildLocalizedTextAttribute( seq, "DisplayName", uanode.DisplayName, + uanode.DecodedBrowseName.Name, ignoreEqual: true ); + BuildLocalizedTextAttribute( seq, "Description", uanode.Description, + uanode.DecodedBrowseName.Name, ignoreEqual: false ); + + UAType uaType = uanode as UAType; + if ( uaType != null && uaType.IsAbstract ) { - AddModifyAttribute(seq, "DisplayName", "LocalizedText", - uanode.DisplayName[0].Value); + AddModifyAttribute(seq, "IsAbstract", "Boolean", uaType.IsAbstract); } + } - if (uanode.Description != null && - uanode.Description.Length > 0 && - uanode.Description[0].Value.Length > 0) + private void BuildLocalizedTextAttribute( + AttributeSequence seq, + string attributeName, + NodeSet.LocalizedText[] localizedTexts, + string equalityString, bool ignoreEqual ) + { + if( localizedTexts != null ) { - AddModifyAttribute(seq, "Description", "LocalizedText", - uanode.Description[0].Value); + if( localizedTexts.Length > 1 ) + { + AddModifyAttribute( seq, attributeName, "LocalizedText", localizedTexts[ 0 ].Value ); + + AttributeType displayNameAttribute = seq[ attributeName ]; + if( displayNameAttribute != null ) + { + string previousLocaleId = string.Empty; + string defaultLocaleId = GetLocaleId( localizedTexts[ 0 ], ref previousLocaleId ); + AttributeType arrayRoot = AddModifyAttribute( displayNameAttribute.Attribute, defaultLocaleId, "String", + localizedTexts[ 0 ].Value ); + + // Redo first element + previousLocaleId = string.Empty; + for( int index = 0; index < localizedTexts.Length; index++ ) + { + string localeId = GetLocaleId( localizedTexts[ index ], ref previousLocaleId ); + AddModifyAttribute( arrayRoot.Attribute, "aml-lang=" + localeId, + "String", localizedTexts[ index ].Value ); + } + } + } + else if( localizedTexts.Length > 0 ) + { + NodeSet.LocalizedText localizedText = localizedTexts[ 0 ]; + if( ignoreEqual == false || + localizedText.Value != equalityString ) + { + AttributeType root = AddModifyAttribute( seq, attributeName, "LocalizedText", localizedText.Value ); + + if( !String.IsNullOrEmpty( localizedText.Locale ) ) + { + AddModifyAttribute( root.Attribute, localizedText.Locale, "LocalizedText", localizedText.Value ); + } + } + } } + } - UAType uaType = uanode as UAType; - if ( uaType != null && uaType.IsAbstract ) + private string GetLocaleId( NodeSet.LocalizedText localizedText, ref string lastUnknownLocale ) + { + string localeId = string.Empty; + + if ( localizedText != null ) { - AddModifyAttribute(seq, "IsAbstract", "Boolean", uaType.IsAbstract); + if ( String.IsNullOrEmpty( localizedText.Locale ) ) + { + if ( String.IsNullOrEmpty( lastUnknownLocale ) ) + { + localeId = "qaa"; + } + else + { + if ( lastUnknownLocale.Length == 3 && lastUnknownLocale[0] == 'q' ) + { + char secondChar = lastUnknownLocale[1]; + char lastChar = lastUnknownLocale[ 2 ]; + if ( lastChar == 'z' ) + { + // It's pretty impractical to have 20*26 unknown locales for a single node. + if( secondChar < 't' ) + { + secondChar++; + localeId = "q" + secondChar + 'a'; + } + } + else + { + localeId = "q" + secondChar + (char)( lastChar + 1 ); + } + } + } + lastUnknownLocale = localeId; + } + else + { + localeId = localizedText.Locale; + } + + return localeId; } + + return localeId; } private AttributeType AddModifyAttribute(AttributeSequence seq, string name, string refDataType, Variant val, bool bListOf = false, string sURI = uaNamespaceURI) @@ -910,7 +992,13 @@ private AttributeType AddModifyAttribute(AttributeSequence seq, string name, str if( localizedText != null && localizedText.Text != null ) { a.DefaultAttributeValue = a.AttributeValue = localizedText.Text; + if ( !string.IsNullOrEmpty( localizedText.Locale ) ) + { + AddModifyAttribute(a.Attribute, localizedText.Locale, + "String", localizedText.Text); + } } + break; } diff --git a/Opc2AmlConsole/Properties/launchSettings.json b/Opc2AmlConsole/Properties/launchSettings.json index db1f0ae..71b4db4 100644 --- a/Opc2AmlConsole/Properties/launchSettings.json +++ b/Opc2AmlConsole/Properties/launchSettings.json @@ -1,7 +1,8 @@ { "profiles": { "Opc2AmlConsole": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "--Nodeset TestAml.xml" } } } \ No newline at end of file diff --git a/SystemTest/NodeSetFiles/TestAml.xml b/SystemTest/NodeSetFiles/TestAml.xml index 13c3d7d..06e97ef 100644 --- a/SystemTest/NodeSetFiles/TestAml.xml +++ b/SystemTest/NodeSetFiles/TestAml.xml @@ -2126,7 +2126,8 @@ - OneDimension + OneDimensionArray + A One Dimensional Array ns=1;i=5011 i=63 @@ -2147,7 +2148,10 @@ - TwoDimensions + TwoDimensionArray + aTwoDimensionalArray + A Different Two Dimension Array + Another Two Dimension Array ns=1;i=5011 i=63 @@ -2168,7 +2172,10 @@ - FiveDimensions + FiveDimensionArray + A Five DimensionArray + Another Five Dimension Array + ns=1;i=5011 i=63 @@ -3087,13 +3094,143 @@ + + Localized Text Attributes + + i=85 + i=61 + + + + + + i=63 + ns=1;i=5024 + + + + + Single Localized Text + A Single Description with no Locale + + i=63 + ns=1;i=5024 + + + + Attributes have display name and description, but no Locale + + + + + Single Localized Text with Locale + A Single Description with a Locale + + i=63 + ns=1;i=5024 + + + + en + Attributes have display name and description with a single Locale + + + + + First DisplayName with no Locale + First Description with no Locale + Second DisplayName with a Locale + Second Description with a Locale + Troisième texte localisé avec paramètres régionaux + Troisième description avec un paramètre régional + Letzter lokalisierter Text mit Gebietsschema + Letzte Beschreibung mit einem Gebietsschema + + i=63 + ns=1;i=5024 + + + + + Multiple + + + en + DisplayNames and Descriptions + + + en + first without a locale + + + + + + + First DisplayName with a Locale + First Description with a Locale + Deuxième DisplayName avec une locale + Deuxième description avec une locale + Dritter DisplayName mit einem Gebietsschema + Dritte Beschreibung mit einem Gebietsschema + Last DisplayName with no Locale + Last Description with no Locale + + i=63 + ns=1;i=5024 + + + + + en + Multiple + + + en + DisplayNames and Descriptions + + + last without a locale + + + + + + + First DisplayName with no Locale + First Description with no Locale + Second DisplayName with no Locale + Second Description with no Locale + Third DisplayName with no Locale + Third Description with no Locale + Last DisplayName with no Locale + Last Description with no Locale + + i=63 + ns=1;i=5024 + + + + + Multiple + + + DisplayNames and Descriptions + + + all without a locale + + + + + diff --git a/SystemTest/TestLocalizedText.cs b/SystemTest/TestLocalizedText.cs new file mode 100644 index 0000000..c5934a6 --- /dev/null +++ b/SystemTest/TestLocalizedText.cs @@ -0,0 +1,231 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; +using Aml.Engine.AmlObjects; +using Aml.Engine.CAEX; +using Aml.Engine.CAEX.Extensions; +using Opc.Ua; +using static System.Net.Mime.MediaTypeNames; + + +namespace SystemTest +{ + [TestClass] + public class TestLocalizedText + { + CAEXDocument m_document = null; + + #region Tests + + [TestMethod] + public void TestNoValue( ) + { + SystemUnitClassType objectToTest = GetTestObject( "6227" ); + AttributeType attributeType = objectToTest.Attribute[ "Value" ]; + Assert.IsNotNull( attributeType ); + Assert.IsNull( attributeType.Value, "Unexpected Value found" ); + } + + [TestMethod] + [DataRow( "6228", "", "Attributes have display name and description, but no Locale", + DisplayName = "Single value no Locale" )] + [DataRow( "6229", "en", "Attributes have display name and description with a single Locale", + DisplayName = "Single value with Locale" )] + public void TestScalarValue( string node, string locale, string text ) + { + SystemUnitClassType objectToTest = GetTestObject( node ); + AttributeType value = ValidateAttribute( objectToTest.Attribute, "Value", text ); + + if( string.IsNullOrEmpty( locale ) ) + { + Assert.AreEqual( 0, value.Attribute.Count ); + } + else + { + ValidateAttribute( value.Attribute, locale, text ); + } + } + + [TestMethod] + [DataRow( "6230", new string[] { "", "en", "en" }, + new string[] { + "Multiple", + "DisplayNames and Descriptions", + "first without a locale" }, + DisplayName = "Multiple Starting with no Locale" )] + + [DataRow( "6231", new string[] { "en", "en", "" }, + new string[] { + "Multiple", + "DisplayNames and Descriptions", + "last without a locale" }, + DisplayName = "Multiple Ending with no Locale" )] + + [DataRow( "6232", new string[] { "", "", "" }, + new string[] { + "Multiple", + "DisplayNames and Descriptions", + "all without a locale" }, + DisplayName = "Multiple all without no Locale" )] + + public void TestArrayValue( string node, string[] locales, string[] values ) + { + SystemUnitClassType objectToTest = GetTestObject( node ); + AttributeType attributeType = objectToTest.Attribute[ "Value" ]; + Assert.IsNotNull( attributeType ); + Assert.IsNull( attributeType.Value, "Unexpected Value found" ); + + for( int index = 0; index < locales.Length; index++ ) + { + AttributeType indexed = ValidateAttribute( attributeType.Attribute, index.ToString(), values[ index ] ); + if( string.IsNullOrEmpty( locales[ index ] ) ) + { + Assert.AreEqual( 0, indexed.Attribute.Count, "Unexpected Locale found" ); + } + else + { + ValidateAttribute( indexed.Attribute, locales[ index ], values[ index ] ); + } + } + } + + [TestMethod] + [DataRow( "DisplayName", DisplayName = "No LocalizedText DisplayName" )] + [DataRow( "Description", DisplayName = "No LocalizedText Description" )] + public void NoLocalizedText( string attributeName ) + { + SystemUnitClassType objectToTest = GetTestObject( "6227" ); + AttributeType attributeType = objectToTest.Attribute[ attributeName ]; + Assert.IsNull( attributeType, "Unexpected attribute found for " + attributeName ); + } + + + [TestMethod] + [DataRow("6228", "DisplayName", "", "Single Localized Text", + DisplayName = "Single no Locale DisplayName")] + [DataRow( "6228", "Description", "", "A Single Description with no Locale", + DisplayName = "Single no Locale Description" )] + [DataRow( "6229", "DisplayName", "en", "Single Localized Text with Locale", + DisplayName = "Single with Locale DisplayName" )] + [DataRow( "6229", "Description", "en", "A Single Description with a Locale", + DisplayName = "Single with Locale Description" )] + public void SingleLocalizedText(string nodeId, string attributeName, string localeId, string text ) + { + SystemUnitClassType objectToTest = GetTestObject( nodeId ); + AttributeType attributeType = ValidateAttribute( objectToTest.Attribute, attributeName, text ); + + if( string.IsNullOrEmpty( localeId ) ) + { + Assert.AreEqual( 0, attributeType.Attribute.Count, 1 ); + } + else + { + ValidateAttribute( attributeType.Attribute, localeId, text ); + } + } + + [TestMethod] + [DataRow( "6230", "DisplayName", + new string[] { "qaa", "en", "fr", "de"}, + new string[] { + "First DisplayName with no Locale", + "Second DisplayName with a Locale", + "Troisième texte localisé avec paramètres régionaux", + "Letzter lokalisierter Text mit Gebietsschema" }, + DisplayName = "Multiple Starting with no Locale DisplayName" )] + [DataRow( "6230", "Description", + new string[] { "qaa", "en", "fr", "de" }, + new string[] { + "First Description with no Locale", + "Second Description with a Locale", + "Troisième description avec un paramètre régional", + "Letzte Beschreibung mit einem Gebietsschema" }, + DisplayName = "Multiple Starting with no Locale Description" )] + [DataRow( "6231", "DisplayName", + new string[] { "en", "fr", "de", "qaa"}, + new string[] { + "First DisplayName with a Locale", + "Deuxième DisplayName avec une locale", + "Dritter DisplayName mit einem Gebietsschema", + "Last DisplayName with no Locale" }, + DisplayName = "Multiple Ending with no Locale DisplayName" )] + [DataRow( "6231", "Description", + new string[] { "en", "fr", "de", "qaa" }, + new string[] { + "First Description with a Locale", + "Deuxième description avec une locale", + "Dritte Beschreibung mit einem Gebietsschema", + "Last Description with no Locale" }, + DisplayName = "Multiple Ending with no Locale Description" )] + + + [DataRow( "6232", "DisplayName", + new string[] { "qaa", "qab", "qac", "qad" }, + new string[] { + "First DisplayName with no Locale", + "Second DisplayName with no Locale", + "Third DisplayName with no Locale", + "Last DisplayName with no Locale" }, + DisplayName = "Multiple no Locale DisplayName" )] + [DataRow( "6232", "Description", + new string[] { "qaa", "qab", "qac", "qad" }, + new string[] { + "First Description with no Locale", + "Second Description with no Locale", + "Third Description with no Locale", + "Last Description with no Locale" }, + DisplayName = "Multiple no Locale Description" )] + public void MultipleLocalizedText( string nodeId, string attributeName, string[] localeId, string[] text ) + { + SystemUnitClassType objectToTest = GetTestObject( nodeId ); + AttributeType topLevel = ValidateAttribute( objectToTest.Attribute, attributeName, text[ 0 ] ); + AttributeType textLevel = ValidateAttribute( topLevel.Attribute, localeId[0], text[ 0 ] ); + + for( int index = 0; index < localeId.Length; index++ ) + { + string subLocale = "aml-lang=" + localeId[ index ]; + ValidateAttribute( textLevel.Attribute, subLocale, text[index] ); + } + } + + private AttributeType ValidateAttribute( AttributeSequence sequence, string attributeName, string attributeValue ) + { + Assert.IsNotNull( sequence, "AttributeSequence not found" ); + AttributeType attributeType = sequence[ attributeName ]; + Assert.IsNotNull( attributeType, attributeName + " attribute not found" ); + Assert.IsNotNull( attributeType.Value, attributeName + " attribute valid not found" ); + Assert.AreEqual( attributeValue, attributeType.Value, "Unexpected value for " + attributeName ); + return attributeType; + } + + #endregion + + #region Helpers + + private CAEXDocument GetDocument() + { + if( m_document == null ) + { + m_document = TestHelper.GetReadOnlyDocument( "TestAml.xml.amlx" ); + } + Assert.IsNotNull( m_document, "Unable to retrieve Document" ); + return m_document; + } + + public SystemUnitClassType GetTestObject(string nodeId, bool foundationRoot = false) + { + CAEXDocument document = GetDocument(); + string rootName = TestHelper.GetRootName(); + if ( foundationRoot ) + { + rootName = TestHelper.GetOpcRootName(); + } + CAEXObject initialObject = document.FindByID(rootName + nodeId); + Assert.IsNotNull(initialObject, "Unable to find Initial Object"); + SystemUnitClassType theObject = initialObject as SystemUnitClassType; + Assert.IsNotNull(theObject, "Unable to Cast Initial Object"); + return theObject; + } + + #endregion + } +}