using Implab.Formats.Json; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml; namespace Implab.Xml { public class JsonXmlReader : XmlReader { struct JsonContext { public string localName; public bool skip; } JsonReader m_parser; JsonXmlReaderOptions m_options; JsonXmlReaderPosition m_position = JsonXmlReaderPosition.Initial; XmlNameTable m_nameTable; readonly string m_jsonRootName; readonly string m_jsonNamespace; readonly string m_jsonPrefix; readonly bool m_jsonFlattenArrays; readonly string m_jsonArrayItemName; string m_jsonLocalName; string m_jsonValueName; bool m_jsonSkip; // indicates wheather to generate closing tag for objects or arrays readonly Stack m_jsonNameStack = new Stack(); XmlQualifiedName m_elementQName; string m_elementPrefix; int m_elementDepth; bool m_elementIsEmpty; XmlQualifiedName m_qName; string m_prefix; int m_xmlDepth; XmlSimpleAttribute[] m_attributes; object m_value; bool m_isEmpty; XmlNodeType m_nodeType = XmlNodeType.None; bool m_isAttribute; // indicates that we are reading attribute nodes int m_currentAttribute; bool m_currentAttributeRead; XmlNameContext m_context; readonly string m_xmlnsPrefix; readonly string m_xmlnsNamespace; readonly string m_xsiPrefix; readonly string m_xsiNamespace; public JsonXmlReader(JsonReader parser, JsonXmlReaderOptions options) { Safe.ArgumentNotNull(parser, nameof(parser)); m_parser = parser; m_options = options ?? new JsonXmlReaderOptions(); m_jsonFlattenArrays = m_options.FlattenArrays; m_nameTable = m_options.NameTable ?? new NameTable(); m_jsonRootName = m_nameTable.Add(string.IsNullOrEmpty(m_options.RootName) ? "data" : m_options.RootName); m_jsonArrayItemName = m_nameTable.Add(string.IsNullOrEmpty(m_options.ArrayItemName) ? "item" : m_options.ArrayItemName); m_jsonNamespace = m_nameTable.Add(m_options.NamespaceUri ?? string.Empty); m_jsonPrefix = m_nameTable.Add(m_options.NodesPrefix ?? string.Empty); m_xmlnsPrefix = m_nameTable.Add(XmlNameContext.XmlnsPrefix); m_xmlnsNamespace = m_nameTable.Add(XmlNameContext.XmlnsNamespace); m_xsiPrefix = m_nameTable.Add(XmlNameContext.XsiPrefix); m_xsiNamespace = m_nameTable.Add(XmlNameContext.XsiNamespace); // TODO validate m_jsonRootName, m_jsonArrayItemName m_context = new XmlNameContext(null, 0); } public override int AttributeCount { get { return m_attributes == null ? 0 : m_attributes.Length; } } public override string BaseURI { get { return string.Empty; } } public override int Depth { get { return m_xmlDepth; } } public override bool EOF { get { return m_position == JsonXmlReaderPosition.Eof; } } public override bool IsEmptyElement { get { return m_isEmpty; } } public override string LocalName { get { return m_qName.Name; } } public override string NamespaceURI { get { return m_qName.Namespace; } } public override XmlNameTable NameTable { get { return m_nameTable; } } public override XmlNodeType NodeType { get { return m_nodeType; } } public override string Prefix { get { return m_prefix; } } public override ReadState ReadState { get { switch (m_position) { case JsonXmlReaderPosition.Initial: return ReadState.Initial; case JsonXmlReaderPosition.Eof: return ReadState.EndOfFile; case JsonXmlReaderPosition.Closed: return ReadState.Closed; case JsonXmlReaderPosition.Error: return ReadState.Error; default: return ReadState.Interactive; }; } } public override string Value { get { return ConvertValueToString(m_value); } } static string ConvertValueToString(object value) { if (value == null) return string.Empty; switch (Convert.GetTypeCode(value)) { case TypeCode.Double: return ((double)value).ToString(CultureInfo.InvariantCulture); case TypeCode.String: return (string)value; case TypeCode.Boolean: return (bool)value ? "true" : "false"; default: return value.ToString(); } } public override string GetAttribute(int i) { Safe.ArgumentInRange(i, 0, AttributeCount - 1, nameof(i)); return ConvertValueToString(m_attributes[i].Value); } public override string GetAttribute(string name) { if (m_attributes == null) return null; var qName = m_context.Resolve(name); var attr = Array.Find(m_attributes, x => x.QName == qName); var value = ConvertValueToString(attr?.Value); return value == string.Empty ? null : value; } public override string GetAttribute(string name, string namespaceURI) { if (m_attributes == null) return null; var qName = new XmlQualifiedName(name, namespaceURI); var attr = Array.Find(m_attributes, x => x.QName == qName); var value = ConvertValueToString(attr?.Value); return value == string.Empty ? null : value; } public override string LookupNamespace(string prefix) { return m_context.ResolvePrefix(prefix); } public override bool MoveToAttribute(string name) { if (m_attributes == null || m_attributes.Length == 0) return false; var qName = m_context.Resolve(name); var index = Array.FindIndex(m_attributes, x => x.QName == qName); if (index >= 0) { MoveToAttributeImpl(index); return true; } return false; } public override bool MoveToAttribute(string name, string ns) { if (m_attributes == null || m_attributes.Length == 0) return false; var qName = m_context.Resolve(name); var index = Array.FindIndex(m_attributes, x => x.QName == qName); if (index >= 0) { MoveToAttributeImpl(index); return true; } return false; } void MoveToAttributeImpl(int i) { if (!m_isAttribute) { m_elementQName = m_qName; m_elementDepth = m_xmlDepth; m_elementPrefix = m_prefix; m_elementIsEmpty = m_isEmpty; m_isAttribute = true; } var attr = m_attributes[i]; m_currentAttribute = i; m_currentAttributeRead = false; m_nodeType = XmlNodeType.Attribute; m_xmlDepth = m_elementDepth + 1; m_qName = attr.QName; m_value = attr.Value; m_prefix = attr.Prefix; } public override bool MoveToElement() { if (m_isAttribute) { m_value = null; m_nodeType = XmlNodeType.Element; m_xmlDepth = m_elementDepth; m_prefix = m_elementPrefix; m_qName = m_elementQName; m_isEmpty = m_elementIsEmpty; m_isAttribute = false; return true; } return false; } public override bool MoveToFirstAttribute() { if (m_attributes != null && m_attributes.Length > 0) { MoveToAttributeImpl(0); return true; } return false; } public override bool MoveToNextAttribute() { if (m_isAttribute) { var next = m_currentAttribute + 1; if (next < AttributeCount) { MoveToAttributeImpl(next); return true; } return false; } else { return MoveToFirstAttribute(); } } public override bool ReadAttributeValue() { if (!m_isAttribute || m_currentAttributeRead) return false; ValueNode(m_attributes[m_currentAttribute].Value); m_currentAttributeRead = true; return true; } public override void ResolveEntity() { /* do nothing */ } /// /// Determines do we need to increase depth after the current node /// /// public bool IsSibling() { switch (m_nodeType) { case XmlNodeType.None: // start document case XmlNodeType.Attribute: // after attribute only it's content can be iterated with ReadAttributeValue method return false; case XmlNodeType.Element: // if the elemnt is empty the next element will be it's sibling return m_isEmpty; default: return true; } } void ValueNode(object value) { if (!IsSibling()) // the node is nested m_xmlDepth++; m_qName = XmlQualifiedName.Empty; m_nodeType = XmlNodeType.Text; m_prefix = string.Empty; m_value = value; m_isEmpty = false; m_attributes = null; } void ElementNode(string name, string ns, XmlSimpleAttribute[] attrs, bool empty) { if (!IsSibling()) // the node is nested m_xmlDepth++; var context = m_context; List definedAttrs = null; // define new namespaces if (attrs != null) { foreach (var attr in attrs) { if (attr.QName.Name == "xmlns") { if (context == m_context) context = new XmlNameContext(m_context, m_xmlDepth); context.DefinePrefix(ConvertValueToString(attr.Value), string.Empty); } else if (attr.Prefix == m_xmlnsPrefix) { if (context == m_context) context = new XmlNameContext(m_context, m_xmlDepth); context.DefinePrefix(ConvertValueToString(attr.Value), attr.QName.Name); } else { string attrPrefix; if (string.IsNullOrEmpty(attr.QName.Namespace)) continue; // auto-define prefixes if (!context.LookupNamespacePrefix(attr.QName.Namespace, out attrPrefix) || string.IsNullOrEmpty(attrPrefix)) { // new namespace prefix added attrPrefix = context.CreateNamespacePrefix(attr.QName.Namespace); attr.Prefix = attrPrefix; if (definedAttrs == null) definedAttrs = new List(); definedAttrs.Add(new XmlSimpleAttribute(attrPrefix, m_xmlnsNamespace, m_xmlnsPrefix, attr.QName.Namespace)); } } } } string p; // auto-define prefixes if (!context.LookupNamespacePrefix(ns, out p)) { if (context == m_context) context = new XmlNameContext(m_context, m_xmlDepth); p = context.CreateNamespacePrefix(ns); if (definedAttrs == null) definedAttrs = new List(); definedAttrs.Add(new XmlSimpleAttribute(p, m_xmlnsNamespace, m_xmlnsPrefix, ns)); } if (definedAttrs != null) { if (attrs != null) definedAttrs.AddRange(attrs); attrs = definedAttrs.ToArray(); } if (!empty) m_context = context; m_nodeType = XmlNodeType.Element; m_qName = new XmlQualifiedName(name, ns); m_prefix = p; m_value = null; m_isEmpty = empty; m_attributes = attrs; } void EndElementNode(string name, string ns) { if (IsSibling()) { // closing the element which has children m_xmlDepth--; } string p; if (!m_context.LookupNamespacePrefix(ns, out p)) throw new Exception($"Failed to lookup namespace '{ns}'"); if (m_context.Depth == m_xmlDepth) m_context = m_context.ParentContext; m_nodeType = XmlNodeType.EndElement; m_prefix = p; m_qName = new XmlQualifiedName(name, ns); m_value = null; m_attributes = null; m_isEmpty = false; } void XmlDeclaration() { if (!IsSibling()) // the node is nested m_xmlDepth++; m_nodeType = XmlNodeType.XmlDeclaration; m_qName = new XmlQualifiedName("xml"); m_value = "version='1.0'"; m_prefix = string.Empty; m_attributes = null; m_isEmpty = false; } public override bool Read() { try { string elementName; XmlSimpleAttribute[] elementAttrs = null; MoveToElement(); switch (m_position) { case JsonXmlReaderPosition.Initial: m_jsonLocalName = m_jsonRootName; m_jsonSkip = false; XmlDeclaration(); m_position = JsonXmlReaderPosition.Declaration; return true; case JsonXmlReaderPosition.Declaration: elementAttrs = new[] { new XmlSimpleAttribute(m_xsiPrefix, m_xmlnsNamespace, m_xmlnsPrefix, m_xsiNamespace), string.IsNullOrEmpty(m_jsonPrefix) ? new XmlSimpleAttribute(m_xmlnsPrefix, string.Empty, string.Empty, m_jsonNamespace) : new XmlSimpleAttribute(m_jsonPrefix, m_xmlnsNamespace, m_xmlnsPrefix, m_jsonNamespace) }; break; case JsonXmlReaderPosition.ValueElement: if (!m_isEmpty) { if (m_parser.ElementValue != null && !m_parser.ElementValue.Equals(string.Empty)) ValueNode(m_parser.ElementValue); else goto case JsonXmlReaderPosition.ValueContent; m_position = JsonXmlReaderPosition.ValueContent; return true; } else { m_position = JsonXmlReaderPosition.ValueEndElement; break; } case JsonXmlReaderPosition.ValueContent: EndElementNode(m_jsonValueName, m_jsonNamespace); m_position = JsonXmlReaderPosition.ValueEndElement; return true; case JsonXmlReaderPosition.Eof: case JsonXmlReaderPosition.Closed: case JsonXmlReaderPosition.Error: return false; } while (m_parser.Read()) { var jsonName = m_nameTable.Add(m_parser.ElementName); switch (m_parser.ElementType) { case JsonElementType.BeginObject: if (!EnterJsonObject(jsonName, out elementName)) continue; m_position = JsonXmlReaderPosition.BeginObject; ElementNode(elementName, m_jsonNamespace, elementAttrs, false); break; case JsonElementType.EndObject: if (!LeaveJsonScope(out elementName)) continue; m_position = JsonXmlReaderPosition.EndObject; EndElementNode(elementName, m_jsonNamespace); break; case JsonElementType.BeginArray: if (!EnterJsonArray(jsonName, out elementName)) continue; m_position = JsonXmlReaderPosition.BeginArray; ElementNode(elementName, m_jsonNamespace, elementAttrs, false); break; case JsonElementType.EndArray: if (!LeaveJsonScope(out elementName)) continue; m_position = JsonXmlReaderPosition.EndArray; EndElementNode(elementName, m_jsonNamespace); break; case JsonElementType.Value: if (!VisitJsonValue(jsonName, out m_jsonValueName)) continue; m_position = JsonXmlReaderPosition.ValueElement; if (m_parser.ElementValue == null) // generate empty element with xsi:nil="true" attribute ElementNode( m_jsonValueName, m_jsonNamespace, new[] { new XmlSimpleAttribute("nil", m_xsiNamespace, m_xsiPrefix, true) }, true ); else ElementNode(m_jsonValueName, m_jsonNamespace, elementAttrs, m_parser.ElementValue.Equals(string.Empty)); break; default: throw new Exception($"Unexpected JSON element {m_parser.ElementType}: {m_parser.ElementName}"); } return true; } m_position = JsonXmlReaderPosition.Eof; return false; } catch { m_position = JsonXmlReaderPosition.Error; throw; } } void SaveJsonName() { m_jsonNameStack.Push(new JsonContext { skip = m_jsonSkip, localName = m_jsonLocalName }); } bool EnterJsonObject(string name, out string elementName) { SaveJsonName(); m_jsonSkip = false; if (string.IsNullOrEmpty(name)) { if (m_jsonNameStack.Count != 1 && !m_jsonFlattenArrays) m_jsonLocalName = m_jsonArrayItemName; } else { m_jsonLocalName = name; } elementName = m_jsonLocalName; return true; } /// /// Called when JSON parser visits BeginArray ('[') element. /// /// Optional property name if the array is the member of an object /// true if element should be emited, false otherwise bool EnterJsonArray(string name, out string elementName) { SaveJsonName(); if (string.IsNullOrEmpty(name)) { // m_jsonNameStack.Count == 1 means the root node if (m_jsonNameStack.Count != 1 && !m_jsonFlattenArrays) m_jsonLocalName = m_jsonArrayItemName; m_jsonSkip = false; // we should not flatten arrays inside arrays or in the document root } else { m_jsonLocalName = name; m_jsonSkip = m_jsonFlattenArrays; } elementName = m_jsonLocalName; return !m_jsonSkip; } bool VisitJsonValue(string name, out string elementName) { if (string.IsNullOrEmpty(name)) { // m_jsonNameStack.Count == 0 means that JSON document consists from simple value elementName = (m_jsonNameStack.Count == 0 || m_jsonFlattenArrays) ? m_jsonLocalName : m_jsonArrayItemName; } else { elementName = name; } return true; } bool LeaveJsonScope(out string elementName) { elementName = m_jsonLocalName; var skip = m_jsonSkip; var prev = m_jsonNameStack.Pop(); m_jsonLocalName = prev.localName; m_jsonSkip = prev.skip; return !skip; } public override string ToString() { switch (NodeType) { case XmlNodeType.Element: return $"<{Name} {string.Join(" ", (m_attributes ?? new XmlSimpleAttribute[0]).Select(x => $"{x.Prefix}{(string.IsNullOrEmpty(x.Prefix) ? "" : ":")}{x.QName.Name}='{ConvertValueToString(x.Value)}'"))} {(IsEmptyElement ? "/" : "")}>"; case XmlNodeType.Attribute: return $"@{Name}"; case XmlNodeType.Text: return $"{Value}"; case XmlNodeType.CDATA: return $""; case XmlNodeType.EntityReference: return $"&{Name};"; case XmlNodeType.EndElement: return $""; default: return $".{NodeType} {Name} {Value}"; } } } }