From 5ed873f44b6b447ccf6f8c5ce894a122656c8b63 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 8 Oct 2018 14:42:49 +1300 Subject: [PATCH] Added EncodeSpecialCharacters setting to XmlNodeConverter (#1866) --- .../Converters/XmlNodeConverterTest.cs | 250 ++++++++++++++++++ .../Converters/XmlNodeConverter.cs | 165 +++++++----- Src/Newtonsoft.Json/JsonConvert.cs | 50 +++- 3 files changed, 396 insertions(+), 69 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs b/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs index 9d9789ba..b935e471 100644 --- a/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs +++ b/Src/Newtonsoft.Json.Tests/Converters/XmlNodeConverterTest.cs @@ -159,6 +159,116 @@ namespace Newtonsoft.Json.Tests.Converters Assert.AreEqual("", xmlNode.OuterXml); } + + [Test] + public void XmlNode_Roundtrip_PropertyNameWithColon() + { + const string initialJson = @"{""Be:fore:After!"":""Value!""}"; + + XmlDocument xmlNode = JsonConvert.DeserializeXmlNode(initialJson, null, false, true); + + Assert.AreEqual("Value!", xmlNode.OuterXml); + + string json = JsonConvert.SerializeXmlNode(xmlNode); + + Assert.AreEqual(initialJson, json); + } + + [Test] + public void XmlNode_Roundtrip_PropertyNameWithEscapedValue() + { + const string initialJson = @"{""BeforeAfter!"":""Value!""}"; + + XmlDocument xmlNode = JsonConvert.DeserializeXmlNode(initialJson); + + Assert.AreEqual("Value!", xmlNode.OuterXml); + + string json = JsonConvert.SerializeXmlNode(xmlNode); + + Assert.AreEqual(initialJson, json); + } + + [Test] + public void XmlNode_EncodeSpecialCharacters() + { + string initialJson = @"{ + ""?xml"": { + ""@version"": ""1.0"", + ""@standalone"": ""no"" + }, + ""?xml-stylesheet"": ""href=\""classic.xsl\"" type=\""text/xml\"""", + ""span"": { + ""@class"": ""vevent"", + ""a"": { + ""@class"": ""url"", + ""@href"": ""http://www.web2con.com/"", + ""span"": [ + { + ""@class"": ""summary"", + ""#text"": ""Web 2.0 Conference"", + ""#cdata-section"": ""my escaped text"" + }, + { + ""@class"": ""location"", + ""#text"": ""Argent Hotel, San Francisco, CA"" + } + ], + ""abbr"": [ + { + ""@class"": ""dtstart"", + ""@title"": ""2005-10-05"", + ""#text"": ""October 5"" + }, + { + ""@class"": ""dtend"", + ""@title"": ""2005-10-08"", + ""#text"": ""7"" + } + ] + } + } +}"; + + XmlDocument xmlNode = JsonConvert.DeserializeXmlNode(initialJson, "root", false, true); + + StringAssert.AreEqual(@" + <_x003F_xml> + <_x0040_version>1.0 + <_x0040_standalone>no + + <_x003F_xml-stylesheet>href=""classic.xsl"" type=""text/xml"" + + <_x0040_class>vevent + + <_x0040_class>url + <_x0040_href>http://www.web2con.com/ + + <_x0040_class>summary + <_x0023_text>Web 2.0 Conference + <_x0023_cdata-section>my escaped text + + + <_x0040_class>location + <_x0023_text>Argent Hotel, San Francisco, CA + + + <_x0040_class>dtstart + <_x0040_title>2005-10-05 + <_x0023_text>October 5 + + + <_x0040_class>dtend + <_x0040_title>2005-10-08 + <_x0023_text>7 + + + +", IndentXml(xmlNode.OuterXml)); + + string json = JsonConvert.SerializeXmlNode(xmlNode, Formatting.Indented, true); + + Assert.AreEqual(initialJson, json); + } #endif #if !NET20 @@ -184,6 +294,146 @@ namespace Newtonsoft.Json.Tests.Converters } #if !NET20 + [Test] + public void XNode_Roundtrip_PropertyNameWithColon() + { + const string initialJson = @"{""Be:fore:After!"":""Value!""}"; + + XDocument xmlNode = JsonConvert.DeserializeXNode(initialJson, null, false, true); + + Assert.AreEqual("Value!", xmlNode.ToString()); + + string json = JsonConvert.SerializeXNode(xmlNode); + + Assert.AreEqual(initialJson, json); + } + + [Test] + public void XNode_Roundtrip_PropertyNameWithEscapedValue() + { + const string initialJson = @"{""BeforeAfter!"":""Value!""}"; + + XDocument xmlNode = JsonConvert.DeserializeXNode(initialJson); + + Assert.AreEqual("Value!", xmlNode.ToString()); + + string json = JsonConvert.SerializeXNode(xmlNode); + + Assert.AreEqual(initialJson, json); + } + + [Test] + public void XNode_EncodeSpecialCharacters() + { + string initialJson = @"{ + ""?xml"": { + ""@version"": ""1.0"", + ""@standalone"": ""no"" + }, + ""?xml-stylesheet"": ""href=\""classic.xsl\"" type=\""text/xml\"""", + ""span"": { + ""@class"": ""vevent"", + ""a"": { + ""@class"": ""url"", + ""@href"": ""http://www.web2con.com/"", + ""span"": [ + { + ""@class"": ""summary"", + ""#text"": ""Web 2.0 Conference"", + ""#cdata-section"": ""my escaped text"" + }, + { + ""@class"": ""location"", + ""#text"": ""Argent Hotel, San Francisco, CA"" + } + ], + ""abbr"": [ + { + ""@class"": ""dtstart"", + ""@title"": ""2005-10-05"", + ""#text"": ""October 5"" + }, + { + ""@class"": ""dtend"", + ""@title"": ""2005-10-08"", + ""#text"": ""7"" + } + ] + } + } +}"; + + XDocument xmlNode = JsonConvert.DeserializeXNode(initialJson, "root", false, true); + + StringAssert.AreEqual(@" + <_x003F_xml> + <_x0040_version>1.0 + <_x0040_standalone>no + + <_x003F_xml-stylesheet>href=""classic.xsl"" type=""text/xml"" + + <_x0040_class>vevent + + <_x0040_class>url + <_x0040_href>http://www.web2con.com/ + + <_x0040_class>summary + <_x0023_text>Web 2.0 Conference + <_x0023_cdata-section>my escaped text + + + <_x0040_class>location + <_x0023_text>Argent Hotel, San Francisco, CA + + + <_x0040_class>dtstart + <_x0040_title>2005-10-05 + <_x0023_text>October 5 + + + <_x0040_class>dtend + <_x0040_title>2005-10-08 + <_x0023_text>7 + + + +", xmlNode.ToString()); + + string json = JsonConvert.SerializeXNode(xmlNode, Formatting.Indented, true); + + Assert.AreEqual(initialJson, json); + } + + [Test] + public void XNode_MetadataArray_EncodeSpecialCharacters() + { + string initialJson = @"{ + ""$id"": ""1"", + ""$values"": [ + ""1"", + ""2"", + ""3"", + ""4"", + ""5"" + ] +}"; + + XDocument xmlNode = JsonConvert.DeserializeXNode(initialJson, "root", false, true); + + StringAssert.AreEqual(@" + <_x0024_id>1 + <_x0024_values>1 + <_x0024_values>2 + <_x0024_values>3 + <_x0024_values>4 + <_x0024_values>5 +", xmlNode.ToString()); + + string json = JsonConvert.SerializeXNode(xmlNode, Formatting.Indented, true); + + Assert.AreEqual(initialJson, json); + } + [Test] public void SerializeDollarProperty() { diff --git a/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs b/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs index 4229f1da..ac598929 100644 --- a/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs +++ b/Src/Newtonsoft.Json/Converters/XmlNodeConverter.cs @@ -957,7 +957,7 @@ namespace Newtonsoft.Json.Converters public string DeserializeRootElementName { get; set; } /// - /// Gets or sets a flag to indicate whether to write the Json.NET array attribute. + /// Gets or sets a value to indicate whether to write the Json.NET array attribute. /// This attribute helps preserve arrays when converting the written XML back to JSON. /// /// true if the array attribute is written to the XML; otherwise, false. @@ -969,7 +969,16 @@ namespace Newtonsoft.Json.Converters /// true if the JSON root object is omitted; otherwise, false. public bool OmitRootObject { get; set; } -#region Writing + /// + /// Gets or sets a value indicating whether to encode special characters when converting JSON to XML. + /// If true, special characters like ':', '@', '?', '#' and '$' in JSON property names aren't used to specify + /// XML namespaces, attributes or processing directives. Instead special characters are encoded and written + /// as part of the XML element name. + /// + /// true if special characters are encoded; otherwise, false. + public bool EncodeSpecialCharacters { get; set; } + + #region Writing /// /// Writes the JSON representation of the object. /// @@ -1554,47 +1563,50 @@ namespace Newtonsoft.Json.Converters private void DeserializeValue(JsonReader reader, IXmlDocument document, XmlNamespaceManager manager, string propertyName, IXmlNode currentNode) { - switch (propertyName) + if (!EncodeSpecialCharacters) { - case TextName: - currentNode.AppendChild(document.CreateTextNode(ConvertTokenToXmlValue(reader))); - break; - case CDataName: - currentNode.AppendChild(document.CreateCDataSection(ConvertTokenToXmlValue(reader))); - break; - case WhitespaceName: - currentNode.AppendChild(document.CreateWhitespace(ConvertTokenToXmlValue(reader))); - break; - case SignificantWhitespaceName: - currentNode.AppendChild(document.CreateSignificantWhitespace(ConvertTokenToXmlValue(reader))); - break; - default: - // processing instructions and the xml declaration start with ? - if (!string.IsNullOrEmpty(propertyName) && propertyName[0] == '?') - { - CreateInstruction(reader, document, currentNode, propertyName); - } -#if HAVE_XML_DOCUMENT_TYPE - else if (string.Equals(propertyName, "!DOCTYPE", StringComparison.OrdinalIgnoreCase)) - { - CreateDocumentType(reader, document, currentNode); - } -#endif - else - { - if (reader.TokenType == JsonToken.StartArray) + switch (propertyName) + { + case TextName: + currentNode.AppendChild(document.CreateTextNode(ConvertTokenToXmlValue(reader))); + return; + case CDataName: + currentNode.AppendChild(document.CreateCDataSection(ConvertTokenToXmlValue(reader))); + return; + case WhitespaceName: + currentNode.AppendChild(document.CreateWhitespace(ConvertTokenToXmlValue(reader))); + return; + case SignificantWhitespaceName: + currentNode.AppendChild(document.CreateSignificantWhitespace(ConvertTokenToXmlValue(reader))); + return; + default: + // processing instructions and the xml declaration start with ? + if (!string.IsNullOrEmpty(propertyName) && propertyName[0] == '?') { - // handle nested arrays - ReadArrayElements(reader, document, propertyName, currentNode, manager); + CreateInstruction(reader, document, currentNode, propertyName); return; } - - // have to wait until attributes have been parsed before creating element - // attributes may contain namespace info used by the element - ReadElement(reader, document, currentNode, propertyName, manager); - } - break; +#if HAVE_XML_DOCUMENT_TYPE + else if (string.Equals(propertyName, "!DOCTYPE", StringComparison.OrdinalIgnoreCase)) + { + CreateDocumentType(reader, document, currentNode); + return; + } +#endif + break; + } } + + if (reader.TokenType == JsonToken.StartArray) + { + // handle nested arrays + ReadArrayElements(reader, document, propertyName, currentNode, manager); + return; + } + + // have to wait until attributes have been parsed before creating element + // attributes may contain namespace info used by the element + ReadElement(reader, document, currentNode, propertyName, manager); } private void ReadElement(JsonReader reader, IXmlDocument document, IXmlNode currentNode, string propertyName, XmlNamespaceManager manager) @@ -1604,36 +1616,50 @@ namespace Newtonsoft.Json.Converters throw JsonSerializationException.Create(reader, "XmlNodeConverter cannot convert JSON with an empty property name to XML."); } - Dictionary attributeNameValues = ReadAttributeElements(reader, manager); + Dictionary attributeNameValues = null; + string elementPrefix = null; - string elementPrefix = MiscellaneousUtils.GetPrefix(propertyName); - - if (propertyName.StartsWith('@')) + if (!EncodeSpecialCharacters) { - string attributeName = propertyName.Substring(1); - string attributePrefix = MiscellaneousUtils.GetPrefix(attributeName); + attributeNameValues = ShouldReadInto(reader) + ? ReadAttributeElements(reader, manager) + : null; + elementPrefix = MiscellaneousUtils.GetPrefix(propertyName); - AddAttribute(reader, document, currentNode, propertyName, attributeName, manager, attributePrefix); - return; - } - - if (propertyName.StartsWith('$')) - { - switch (propertyName) + if (propertyName.StartsWith('@')) { - case JsonTypeReflector.ArrayValuesPropertyName: - propertyName = propertyName.Substring(1); - elementPrefix = manager.LookupPrefix(JsonNamespaceUri); - CreateElement(reader, document, currentNode, propertyName, manager, elementPrefix, attributeNameValues); - return; - case JsonTypeReflector.IdPropertyName: - case JsonTypeReflector.RefPropertyName: - case JsonTypeReflector.TypePropertyName: - case JsonTypeReflector.ValuePropertyName: - string attributeName = propertyName.Substring(1); - string attributePrefix = manager.LookupPrefix(JsonNamespaceUri); - AddAttribute(reader, document, currentNode, propertyName, attributeName, manager, attributePrefix); - return; + string attributeName = propertyName.Substring(1); + string attributePrefix = MiscellaneousUtils.GetPrefix(attributeName); + + AddAttribute(reader, document, currentNode, propertyName, attributeName, manager, attributePrefix); + return; + } + + if (propertyName.StartsWith('$')) + { + switch (propertyName) + { + case JsonTypeReflector.ArrayValuesPropertyName: + propertyName = propertyName.Substring(1); + elementPrefix = manager.LookupPrefix(JsonNamespaceUri); + CreateElement(reader, document, currentNode, propertyName, manager, elementPrefix, attributeNameValues); + return; + case JsonTypeReflector.IdPropertyName: + case JsonTypeReflector.RefPropertyName: + case JsonTypeReflector.TypePropertyName: + case JsonTypeReflector.ValuePropertyName: + string attributeName = propertyName.Substring(1); + string attributePrefix = manager.LookupPrefix(JsonNamespaceUri); + AddAttribute(reader, document, currentNode, propertyName, attributeName, manager, attributePrefix); + return; + } + } + } + else + { + if (ShouldReadInto(reader)) + { + reader.ReadAndAssert(); } } @@ -1813,7 +1839,7 @@ namespace Newtonsoft.Json.Converters #endif } - private Dictionary ReadAttributeElements(JsonReader reader, XmlNamespaceManager manager) + private bool ShouldReadInto(JsonReader reader) { // a string token means the element only has a single text child switch (reader.TokenType) @@ -1826,9 +1852,14 @@ namespace Newtonsoft.Json.Converters case JsonToken.Date: case JsonToken.Bytes: case JsonToken.StartConstructor: - return null; + return false; } + return true; + } + + private Dictionary ReadAttributeElements(JsonReader reader, XmlNamespaceManager manager) + { Dictionary attributeNameValues = null; bool finished = false; @@ -2021,7 +2052,7 @@ namespace Newtonsoft.Json.Converters private IXmlElement CreateElement(string elementName, IXmlDocument document, string elementPrefix, XmlNamespaceManager manager) { - string encodeName = XmlConvert.EncodeName(elementName); + string encodeName = EncodeSpecialCharacters ? XmlConvert.EncodeLocalName(elementName) : XmlConvert.EncodeName(elementName); string ns = string.IsNullOrEmpty(elementPrefix) ? manager.DefaultNamespace : manager.LookupNamespace(elementPrefix); IXmlElement element = (!string.IsNullOrEmpty(ns)) ? document.CreateElement(encodeName, ns) : document.CreateElement(encodeName); diff --git a/Src/Newtonsoft.Json/JsonConvert.cs b/Src/Newtonsoft.Json/JsonConvert.cs index 2e5935d7..fa6c8484 100644 --- a/Src/Newtonsoft.Json/JsonConvert.cs +++ b/Src/Newtonsoft.Json/JsonConvert.cs @@ -942,15 +942,38 @@ namespace Newtonsoft.Json /// The JSON string. /// The name of the root element to append when deserializing. /// - /// A flag to indicate whether to write the Json.NET array attribute. + /// A value to indicate whether to write the Json.NET array attribute. /// This attribute helps preserve arrays when converting the written XML back to JSON. /// /// The deserialized . public static XmlDocument DeserializeXmlNode(string value, string deserializeRootElementName, bool writeArrayAttribute) + { + return DeserializeXmlNode(value, deserializeRootElementName, writeArrayAttribute, false); + } + + /// + /// Deserializes the from a JSON string nested in a root element specified by , + /// writes a Json.NET array attribute for collections, and encodes special characters. + /// + /// The JSON string. + /// The name of the root element to append when deserializing. + /// + /// A value to indicate whether to write the Json.NET array attribute. + /// This attribute helps preserve arrays when converting the written XML back to JSON. + /// + /// + /// A value to indicate whether to encode special characters when converting JSON to XML. + /// If true, special characters like ':', '@', '?', '#' and '$' in JSON property names aren't used to specify + /// XML namespaces, attributes or processing directives. Instead special characters are encoded and written + /// as part of the XML element name. + /// + /// The deserialized . + public static XmlDocument DeserializeXmlNode(string value, string deserializeRootElementName, bool writeArrayAttribute, bool encodeSpecialCharacters) { XmlNodeConverter converter = new XmlNodeConverter(); converter.DeserializeRootElementName = deserializeRootElementName; converter.WriteArrayAttribute = writeArrayAttribute; + converter.EncodeSpecialCharacters = encodeSpecialCharacters; return (XmlDocument)DeserializeObject(value, typeof(XmlDocument), converter); } @@ -1020,15 +1043,38 @@ namespace Newtonsoft.Json /// The JSON string. /// The name of the root element to append when deserializing. /// - /// A flag to indicate whether to write the Json.NET array attribute. + /// A value to indicate whether to write the Json.NET array attribute. /// This attribute helps preserve arrays when converting the written XML back to JSON. /// /// The deserialized . public static XDocument DeserializeXNode(string value, string deserializeRootElementName, bool writeArrayAttribute) + { + return DeserializeXNode(value, deserializeRootElementName, writeArrayAttribute, false); + } + + /// + /// Deserializes the from a JSON string nested in a root element specified by , + /// writes a Json.NET array attribute for collections, and encodes special characters. + /// + /// The JSON string. + /// The name of the root element to append when deserializing. + /// + /// A value to indicate whether to write the Json.NET array attribute. + /// This attribute helps preserve arrays when converting the written XML back to JSON. + /// + /// + /// A value to indicate whether to encode special characters when converting JSON to XML. + /// If true, special characters like ':', '@', '?', '#' and '$' in JSON property names aren't used to specify + /// XML namespaces, attributes or processing directives. Instead special characters are encoded and written + /// as part of the XML element name. + /// + /// The deserialized . + public static XDocument DeserializeXNode(string value, string deserializeRootElementName, bool writeArrayAttribute, bool encodeSpecialCharacters) { XmlNodeConverter converter = new XmlNodeConverter(); converter.DeserializeRootElementName = deserializeRootElementName; converter.WriteArrayAttribute = writeArrayAttribute; + converter.EncodeSpecialCharacters = encodeSpecialCharacters; return (XDocument)DeserializeObject(value, typeof(XDocument), converter); }