Added EncodeSpecialCharacters setting to XmlNodeConverter (#1866)

This commit is contained in:
James Newton-King
2018-10-08 14:42:49 +13:00
committed by GitHub
parent 5633e17fc1
commit 5ed873f44b
3 changed files with 396 additions and 69 deletions
@@ -159,6 +159,116 @@ namespace Newtonsoft.Json.Tests.Converters
Assert.AreEqual("<Time>0001-01-01T00:00:00</Time>", 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("<Be_x003A_fore_x003A_After_x0021_>Value!</Be_x003A_fore_x003A_After_x0021_>", 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("<BeforeAfter_x0021_>Value!</BeforeAfter_x0021_>", 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(@"<root>
<_x003F_xml>
<_x0040_version>1.0</_x0040_version>
<_x0040_standalone>no</_x0040_standalone>
</_x003F_xml>
<_x003F_xml-stylesheet>href=""classic.xsl"" type=""text/xml""</_x003F_xml-stylesheet>
<span>
<_x0040_class>vevent</_x0040_class>
<a>
<_x0040_class>url</_x0040_class>
<_x0040_href>http://www.web2con.com/</_x0040_href>
<span>
<_x0040_class>summary</_x0040_class>
<_x0023_text>Web 2.0 Conference</_x0023_text>
<_x0023_cdata-section>my escaped text</_x0023_cdata-section>
</span>
<span>
<_x0040_class>location</_x0040_class>
<_x0023_text>Argent Hotel, San Francisco, CA</_x0023_text>
</span>
<abbr>
<_x0040_class>dtstart</_x0040_class>
<_x0040_title>2005-10-05</_x0040_title>
<_x0023_text>October 5</_x0023_text>
</abbr>
<abbr>
<_x0040_class>dtend</_x0040_class>
<_x0040_title>2005-10-08</_x0040_title>
<_x0023_text>7</_x0023_text>
</abbr>
</a>
</span>
</root>", 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("<Be_x003A_fore_x003A_After_x0021_>Value!</Be_x003A_fore_x003A_After_x0021_>", 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("<BeforeAfter_x0021_>Value!</BeforeAfter_x0021_>", 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(@"<root>
<_x003F_xml>
<_x0040_version>1.0</_x0040_version>
<_x0040_standalone>no</_x0040_standalone>
</_x003F_xml>
<_x003F_xml-stylesheet>href=""classic.xsl"" type=""text/xml""</_x003F_xml-stylesheet>
<span>
<_x0040_class>vevent</_x0040_class>
<a>
<_x0040_class>url</_x0040_class>
<_x0040_href>http://www.web2con.com/</_x0040_href>
<span>
<_x0040_class>summary</_x0040_class>
<_x0023_text>Web 2.0 Conference</_x0023_text>
<_x0023_cdata-section>my escaped text</_x0023_cdata-section>
</span>
<span>
<_x0040_class>location</_x0040_class>
<_x0023_text>Argent Hotel, San Francisco, CA</_x0023_text>
</span>
<abbr>
<_x0040_class>dtstart</_x0040_class>
<_x0040_title>2005-10-05</_x0040_title>
<_x0023_text>October 5</_x0023_text>
</abbr>
<abbr>
<_x0040_class>dtend</_x0040_class>
<_x0040_title>2005-10-08</_x0040_title>
<_x0023_text>7</_x0023_text>
</abbr>
</a>
</span>
</root>", 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(@"<root>
<_x0024_id>1</_x0024_id>
<_x0024_values>1</_x0024_values>
<_x0024_values>2</_x0024_values>
<_x0024_values>3</_x0024_values>
<_x0024_values>4</_x0024_values>
<_x0024_values>5</_x0024_values>
</root>", xmlNode.ToString());
string json = JsonConvert.SerializeXNode(xmlNode, Formatting.Indented, true);
Assert.AreEqual(initialJson, json);
}
[Test]
public void SerializeDollarProperty()
{
@@ -957,7 +957,7 @@ namespace Newtonsoft.Json.Converters
public string DeserializeRootElementName { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <value><c>true</c> if the array attribute is written to the XML; otherwise, <c>false</c>.</value>
@@ -969,7 +969,16 @@ namespace Newtonsoft.Json.Converters
/// <value><c>true</c> if the JSON root object is omitted; otherwise, <c>false</c>.</value>
public bool OmitRootObject { get; set; }
#region Writing
/// <summary>
/// Gets or sets a value indicating whether to encode special characters when converting JSON to XML.
/// If <c>true</c>, 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.
/// </summary>
/// <value><c>true</c> if special characters are encoded; otherwise, <c>false</c>.</value>
public bool EncodeSpecialCharacters { get; set; }
#region Writing
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
@@ -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<string, string> attributeNameValues = ReadAttributeElements(reader, manager);
Dictionary<string, string> 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<string, string> 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<string, string> ReadAttributeElements(JsonReader reader, XmlNamespaceManager manager)
{
Dictionary<string, string> 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);
+48 -2
View File
@@ -942,15 +942,38 @@ namespace Newtonsoft.Json
/// <param name="value">The JSON string.</param>
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <param name="writeArrayAttribute">
/// 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.
/// </param>
/// <returns>The deserialized <see cref="XmlNode"/>.</returns>
public static XmlDocument DeserializeXmlNode(string value, string deserializeRootElementName, bool writeArrayAttribute)
{
return DeserializeXmlNode(value, deserializeRootElementName, writeArrayAttribute, false);
}
/// <summary>
/// Deserializes the <see cref="XmlNode"/> from a JSON string nested in a root element specified by <paramref name="deserializeRootElementName"/>,
/// writes a Json.NET array attribute for collections, and encodes special characters.
/// </summary>
/// <param name="value">The JSON string.</param>
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <param name="writeArrayAttribute">
/// 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.
/// </param>
/// <param name="encodeSpecialCharacters">
/// A value to indicate whether to encode special characters when converting JSON to XML.
/// If <c>true</c>, 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.
/// </param>
/// <returns>The deserialized <see cref="XmlNode"/>.</returns>
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
/// <param name="value">The JSON string.</param>
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <param name="writeArrayAttribute">
/// 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.
/// </param>
/// <returns>The deserialized <see cref="XNode"/>.</returns>
public static XDocument DeserializeXNode(string value, string deserializeRootElementName, bool writeArrayAttribute)
{
return DeserializeXNode(value, deserializeRootElementName, writeArrayAttribute, false);
}
/// <summary>
/// Deserializes the <see cref="XNode"/> from a JSON string nested in a root element specified by <paramref name="deserializeRootElementName"/>,
/// writes a Json.NET array attribute for collections, and encodes special characters.
/// </summary>
/// <param name="value">The JSON string.</param>
/// <param name="deserializeRootElementName">The name of the root element to append when deserializing.</param>
/// <param name="writeArrayAttribute">
/// 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.
/// </param>
/// <param name="encodeSpecialCharacters">
/// A value to indicate whether to encode special characters when converting JSON to XML.
/// If <c>true</c>, 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.
/// </param>
/// <returns>The deserialized <see cref="XNode"/>.</returns>
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);
}