Skip to content

Instantly share code, notes, and snippets.

@alastairtree
Created June 12, 2016 12:16
Show Gist options
  • Save alastairtree/8dac89d554ad8f8b9ef3accaa8cc2127 to your computer and use it in GitHub Desktop.
Save alastairtree/8dac89d554ad8f8b9ef3accaa8cc2127 to your computer and use it in GitHub Desktop.
xml and json conversion formatters
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Newtonsoft.Json.Linq;
namespace Formatters
{
public class XmlDocumentCleanJsonFormatter : XmlDocumentRawJsonFormatter
{
Dictionary<string,string> arrayNameMapping = new Dictionary<string, string>();
private readonly string arrayPrefix = "ArrayOf";
protected override string TransformXmlDocument(XmlDocument doc)
{
var jsonString = base.TransformXmlDocument(doc);
var tokenisedJsonDocument = JToken.Parse(jsonString);
RecursivelyWalkDown(tokenisedJsonDocument, CleanEachNodeIfItHasRedundantArraySerialisationFormatting);
var unwrappedRootToken = UnwrapOuterToken(tokenisedJsonDocument);
var cleanedUpArrayProperties = TidyUpArrayOfNamedProperties(unwrappedRootToken);
var camelCasedTokenisedDocument = ToCamelCasedProperJsonDocument(cleanedUpArrayProperties);
var text = camelCasedTokenisedDocument.ToString(JsonFormatting);
return text;
}
private JToken UnwrapOuterToken(JToken jToken)
{
var jobject = jToken as JObject;
if (jobject != null && NodeHasASinglePropertyThatIsAnObject(jobject))
{
return jobject.Properties().Single(n => n.Name != "?xml").Value;
}
return jToken;
}
/// <summary>
/// camel case all properties and remove any illegal @ suffixed properties
/// </summary>
/// <param name="tokenisedJsonDocument"></param>
/// <returns></returns>
private static JToken ToCamelCasedProperJsonDocument(JToken tokenisedJsonDocument)
{
Func<string, string> camelCaser = name =>
{
name = name.TrimStart("@".ToArray());
return name[0].ToString().ToLower() +
name.Substring(1, name.Length - 1)
;
};
var camelCasedTokenisedDocument = Rename(tokenisedJsonDocument, camelCaser);
return camelCasedTokenisedDocument;
}
public static JToken Rename(JToken json, Func<string, string> map)
{
var property = json as JProperty;
if (property != null)
{
return new JProperty(map(property.Name), Rename(property.Value, map));
}
var array = json as JArray;
if (array != null)
{
var cont = array.Select(el => Rename(el, map));
return new JArray(cont);
}
var jObject = json as JObject;
if (jObject != null)
{
var cont = jObject.Properties().Select(el => Rename(el, map));
return new JObject(cont);
}
return json;
}
public JToken TidyUpArrayOfNamedProperties(JToken json)
{
var property = json as JProperty;
if (property != null)
{
if(property.Name.StartsWith(arrayPrefix) && property.Value is JArray && arrayNameMapping.ContainsKey(property.Name))
return new JProperty(
arrayNameMapping[property.Name], TidyUpArrayOfNamedProperties(property.Value));
else
return new JProperty(property.Name, TidyUpArrayOfNamedProperties(property.Value));
}
var array = json as JArray;
if (array != null)
{
var cont = array.Select(TidyUpArrayOfNamedProperties);
return new JArray(cont);
}
var jObject = json as JObject;
if (jObject != null)
{
var cont = jObject.Properties().Select(TidyUpArrayOfNamedProperties);
return new JObject(cont);
}
return json;
}
private bool CleanEachNodeIfItHasRedundantArraySerialisationFormatting(JObject node)
{
if (NodesParentIsProperty(node) &&
NodeHasASinglePropertyThatIsAnArray(node))
{
var parentProperty = ((JProperty) node.Parent);
var singleChildProperty = node.Properties().Single(WhereNotIgnoredNode);
if (PropertiesAreNamedPluralAndSingularParentAndChild(parentProperty, singleChildProperty)
|| PropertiesAreNamedArrayOfParentAndChildPair(parentProperty, singleChildProperty))
{
ThenRemoveTheRedundantChildNodeAndReplaceWithArrayContents(singleChildProperty, parentProperty);
ReattachIgnoredProperties(node, parentProperty);
return true;
}
}
if (NodesParentIsProperty(node) &&
NodeHasASinglePropertyThatIsAnObject(node))
{
var parentProperty = ((JProperty) node.Parent);
var singleChildProperty = node.Properties().Single(WhereNotIgnoredNode);
if (PropertiesAreNamedPluralAndSingularParentAndChild(parentProperty, singleChildProperty)
|| PropertiesAreNamedArrayOfParentAndChildPair(parentProperty, singleChildProperty))
{
ThenRemoveTheRedundantChildNodeAndReplaceWithNewArrayContainingTheSoloChild(singleChildProperty,
parentProperty);
ReattachIgnoredProperties(node, parentProperty);
return true;
}
}
return false;
}
private static void ReattachIgnoredProperties(JObject node, JProperty parentProperty)
{
//we may have ignored xml attributes which become @attributeName json properties so recreate them
foreach (var xmlAttributeNodes in node.Properties().Where(WhereIgnoredNode))
{
parentProperty.AddBeforeSelf(xmlAttributeNodes.DeepClone());
}
}
private static bool WhereNotIgnoredNode(JProperty node)
{
return !WhereIgnoredNode(node);
}
private static bool WhereIgnoredNode(JProperty node)
{
return node.Name.StartsWith("@") || node.Name == "?xml";
}
private static void ThenRemoveTheRedundantChildNodeAndReplaceWithArrayContents(JProperty singleChildProperty,
JProperty parentProperty)
{
parentProperty.Value = singleChildProperty.Value.DeepClone();
}
private static void ThenRemoveTheRedundantChildNodeAndReplaceWithNewArrayContainingTheSoloChild(
JProperty singleChildProperty,
JProperty parentProperty)
{
parentProperty.Value = new JArray(singleChildProperty.Value.DeepClone());
}
private static bool NodesParentIsProperty(JObject node)
{
return (node.Parent as JProperty) != null;
}
private static bool NodeHasASinglePropertyThatIsAnArray(JObject node)
{
var props = node.Properties().Where(WhereNotIgnoredNode).ToArray();
return props.Count() == 1 &&
props.Single().Value.Type == JTokenType.Array;
}
private static bool NodeHasASinglePropertyThatIsAnObject(JObject node)
{
var props = node.Properties().Where(WhereNotIgnoredNode).ToArray();
return props.Count() == 1 && props.Single().Value.Type == JTokenType.Object;
}
private static bool PropertiesAreNamedPluralAndSingularParentAndChild(JProperty parent, JProperty child)
{
//this is not a perfect algorithm at spotting plurals but works well in practice
return parent.Name.Substring(0, 4) == child.Name.Substring(0, 4) &&
parent.Name.EndsWith("s");
}
private bool PropertiesAreNamedArrayOfParentAndChildPair(JProperty parent, JProperty child)
{
var parentName = parent.Name.ToLower();
if (parentName.StartsWith(arrayPrefix.ToLower()))
{
var childName = child.Name.ToLower();
var childCutoffPoint = childName.Length - 2; // handles s/es/is/ies pluralisation
childName = childName.Substring(0, childCutoffPoint);
//this is not a perfect algorithm at spotting plurals but works well in practice
var match = parentName.Contains(arrayPrefix.ToLower() + childName);
if (match)
{
// save the naming of the child for later cleanup
var pluralisedChild = child.Name.EndsWith("s") ? child.Name : (child.Name + "s");
if(!arrayNameMapping.ContainsKey(parent.Name))
arrayNameMapping.Add(parent.Name, pluralisedChild);
return true;
}
}
return false;
}
private static void RecursivelyWalkDown(JToken node,
Func<JObject, bool> objectAction)
{
if (node.Type == JTokenType.Object)
{
foreach (var jProperty in node.Children<JProperty>().ToList())
{
RecursivelyWalkDown(jProperty.Value, objectAction);
}
objectAction((JObject) node);
}
else if (node.Type == JTokenType.Array)
{
foreach (var child in node.Children().ToList())
{
RecursivelyWalkDown(child, objectAction);
}
}
}
}
}
using System;
using System.Configuration;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Xml;
using Newtonsoft.Json;
using Formatting = Newtonsoft.Json.Formatting;
namespace Formatters
{
public class XmlDocumentRawJsonFormatter : BufferedMediaTypeFormatter
{
protected static readonly Formatting JsonFormatting =
ConfigurationManager.AppSettings["json-indented"]?.Equals("true") ?? false ? Formatting.Indented : Formatting.None;
public XmlDocumentRawJsonFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
public override bool CanWriteType(Type type)
{
if (type == typeof (XmlDocument))
{
return true;
}
return false;
}
public override bool CanReadType(Type type)
{
return false;
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
using (var writer = new StreamWriter(writeStream))
{
var doc = ((XmlDocument) value);
var jsonString = TransformXmlDocument(doc);
writer.Write(jsonString);
}
}
protected virtual string TransformXmlDocument(XmlDocument doc)
{
return ConvertXmlToJson(doc);
}
private static string ConvertXmlToJson(XmlDocument doc)
{
JsonConvert.DefaultSettings = () => WebApiConfig.DefaultJsonSerializerSettings;
var jsonString = JsonConvert.SerializeObject(doc, JsonFormatting,
WebApiConfig.DefaultJsonSerializerSettings);
return jsonString;
}
}
}
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Xml;
using System.Xml.Linq;
namespace Formatters
{
public class XmlDocumentXmlFormatter : BufferedMediaTypeFormatter
{
public XmlDocumentXmlFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
}
public override bool CanWriteType(Type type)
{
if (type == typeof (XmlDocument))
{
return true;
}
return false;
}
public override bool CanReadType(Type type)
{
return false;
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
using (var writer = new StreamWriter(writeStream))
{
((XmlDocument) value).Save(writer);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment