Json的数据映射(基于LitJson)

  最近用到 Echarts 之类的图表显示插件, 它基于不同的 Json 结构可以得到不同的图表, 我们从 http 请求来的数据一般就是 Json 的, 肯定就想通过图形式的数据映射来完成图表的显示了, 不过首先C#它不是JS这种对 Json 结构友好的语言, 要对一个Json节点进行更新的时候, 需要把Json转换成数据结构, 然后找到相应点, 再用新的Json进行节点读取, 生成新的结构, 然后再从根节点读取Json的字符串出来, 才能完成数据映射......

  然后就是复杂结构映射, 比如一个Object和Array进行反复嵌套的情况, 要筛选出来某些数据或者结构, 再映射到其它结构中去的话, 会非常困难, 如果考虑用UE的蓝图的话, 有可能可以做到, Unity这个光是自己做可视化界面以及无限展开的映射选项, 就已经要命了......

 

  先说一个数据映射的过程, 因为使用的是LitJson, 它的扩展性比较强, 数据映射的函数正是用到了 JsonData 的 ReadValue 的方式, 不过我们稍微改动了一下 : 

    /// <summary>
    /// 核心数据映射逻辑, 跟LitJson中的有差别, 简单数据通过强制转换得到, 并保留原有数据类型不变
    /// </summary>
    /// <param name="jsonData"></param>
    /// <param name="reader"></param>
    /// <param name="json"></param>
    /// <returns></returns>
    private static IJsonWrapper ReadValue(JsonData jsonData, JsonReader reader, string json = null)
    {
        try
        {
            reader.Read();      // the json maybe error with read while read prime data

            if(reader.Token == JsonToken.ArrayEnd ||
                reader.Token == JsonToken.Null)
                return null;

            IJsonWrapper instance = jsonData;

            if(reader.Token == JsonToken.String)
            {
                instance.SetString((string)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Double)
            {
                instance.SetDouble((double)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Int)
            {
                instance.SetInt((int)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Long)
            {
                instance.SetLong((long)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.Boolean)
            {
                instance.SetBoolean((bool)reader.Value);
                return instance;
            }

            if(reader.Token == JsonToken.ArrayStart)
            {
                instance.SetJsonType(JsonType.Array);

                while(true)
                {
                    IJsonWrapper item = ReadValue(new JsonData(), reader);
                    if(item == null && reader.Token == JsonToken.ArrayEnd)
                        break;

                    ((IList)instance).Add(item);
                }
            }
            else if(reader.Token == JsonToken.ObjectStart)
            {
                instance.SetJsonType(JsonType.Object);

                while(true)
                {
                    reader.Read();

                    if(reader.Token == JsonToken.ObjectEnd)
                        break;

                    string property = (string)reader.Value;

                    ((IDictionary)instance)[property] = ReadValue(new JsonData(), reader);
                }
            }
        }
        catch { }

        if(reader.Token == JsonToken.None)
        {
            JsonDataSetPrime(jsonData, json);
        }

        return jsonData;
    }
    
    public static string ToRawString(string str)
    {
        if(string.IsNullOrEmpty(str))
        {
            return string.Empty;
        }
        var rawStr = str;
        if(rawStr.StartsWith("\"") && rawStr.EndsWith("\""))
        {
            rawStr = rawStr.Substring(1, rawStr.Length - 2);
        }
        return rawStr;
    }

    public static void JsonDataSetPrime(JsonData jsonData, string json)
    {
        var rawType = jsonData.GetJsonType();
        DataTable dataTable = json;
        DataTable rawData = ToRawString((string)dataTable);
        switch(rawType)
        {
            case JsonType.Int: { ((IJsonWrapper)jsonData).SetInt((int)rawData); } break;
            case JsonType.Long: { ((IJsonWrapper)jsonData).SetLong((long)rawData); } break;
            case JsonType.Double: { ((IJsonWrapper)jsonData).SetDouble((double)rawData); } break;
            case JsonType.Boolean: { ((IJsonWrapper)jsonData).SetBoolean((bool)rawData); } break;
            case JsonType.String:
                {
                    ((IJsonWrapper)jsonData).SetString((string)rawData);
                }
                break;
        }
    }

  这个 ReadValue 就是置换 JsonData 里的内容的, 使用 try catch 是因为它转换的逻辑是从上往下的, 节点的类型是由前置读取的类型给出的, 所以如果是一个Json对象, 它的字符串是合法的就能正确读取, 比如:

{"aa":100}

  可是如果修改对象是普通值类型, 比如要修改的是 100 这个对应的JsonData, 传入的Json可能是这样的:

"200"

  它就不是一个合法的Json, 是无法正常读取的, 所以会抛出异常, 并且导致 JsonReader 的类型是 None, 所以 JsonDataSetPrime() 方法就强制给JsonData赋值了. 这样就封装了节点数据置换的方法了.

  然后做一个数据映射的路径记录, 就能把数据映射到模板里面去了, 比如下图, 左边是数据, 右边是一个 Echarts 图表的模板 : 

  获取它的路径就很简单, 得到 From:xAxis/type To:title/text 这样的结果, 那么只需要获取到左边的节点, 然后ToJson, 用右边相应节点的JsonData来读取一遍即可.

  然后就是有些复杂的映射, 比如 Object(Dictionary) 到 Array 的映射, Array 中嵌套的 Object/Array 对应的结构的映射, 可以从下面的截图看出来 : 

  想要把 yAxis 这个 Object 映射到 data 这个 Array 里面去, 可以映射 Key 或者 Value 的方式 : 

 

  这里是把 yAxis 的 Keys 映射成 Array了. 或者也可以用 Values 映射, 只要结构能够对的上就行了. 代码逻辑跟下面相同, 只是把对应的数值取出来罢了:

    JsonData fromTag;
    var list = new List<string>(fromTag.Keys);
    var json = LitJson.JsonMapper.ToJson(list);

  因为 Object 的 Key 一定是 string 类型的, 所以可以简单的创建 List 对象, 可是如果是 Value 这种复杂对象, 就不能简单构建了!!! 这些映射可以通过简单的数据结构就能保存, 比如上面的我的结构是这样的 : 

    public enum MappingLogic
    {
        Replace,
        ObjectToArray_Keys,
        ObjectToArray_Values
    }
    public class MappingInfo
    {
        public string from;
        public string to;
        public MappingLogic mappingLogic = MappingLogic.Replace;
        public int order = 100;      // 数值越小越先执行
    }
    
    // Json 文件
    [{"from":"yAxis","to":"xAxis/data","mappingLogic":1,"order":100}]

  [ 从 yAxis 节点获取 Keys 映射到 xAxis/data 节点中去 ]

 

  嵌套式的映射 : 

   要将左边的 data (Array) 队列中的 Object 结构中的 xAxis 映射到右边的 data(Array)结构中去, 得到下图 : 

  其实可以看出, 复杂结构就复杂在 Array 的操作上, Object 的每个对象都是有 Key 的, 而 Array 需要通过自己获得 index 来进行映射(普通映射), 如果作为结构映射, 又需要通过 Foreach 进行结构厉遍, 看一下怎样通过代码完成映射结构的 : 

先看生成的映射文件 

[{
    "from": "series/[array:0]/markLine/data/[array:Foreach]/xAxis",
    "to": "series/[array:0]/data",
    "mappingLogic": 0,
    "order": 100
}]

  from 的结构已经不是简单的节点路径了, 到 data/[array:Foreach] 这个节点, 表示的就是对于 data 这个节点的 array 需要进行厉遍, 然后再获取每个厉遍的 xAxis 节点的值, 映射到右边的 data 节点去, 

  代码结构 : 

    public const string Array = "array";
    public const string Foreach = "Foreach";
    
    
    public static void DoMap(JsonData from, JsonData to, MappingInfo info)
    {
        var fromTag = GetHierarchy(from, info.from, true);
        var json = "{}";
        switch(info.mappingLogic)
        {
            case MappingLogic.ObjectToArray_Keys:
                {
                    var list = new List<string>(fromTag.Keys);
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            case MappingLogic.ObjectToArray_Values:
                {
                    var list = new List<string>();
                    fromTag.ForeachDictionary((_key, _data) =>
                    {
                        list.Add(_data.ToString());
                    });
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            default: { json = fromTag.ToJson(); } break;
        }
        WrapJsonData(to, info.to, json, true, false);
    }

    // it must be root json node
    public static void WrapJsonData(JsonData jsonData, string hierarchy, string json, bool clear = true, bool keepRawData = true)
    {
        jsonData = GetHierarchy(jsonData, hierarchy, keepRawData);
        WrapJsonData(jsonData, json, clear);
    }
    public static void WrapJsonData(JsonData jsonData, string json, bool clear = true)
    {
        JsonReader reader = new JsonReader(json);
        if(clear)
        {
            jsonData.Clear();
            jsonData.ClearJsonCache();
        }
        ReadValue(jsonData, reader, json);
    }
    public static void WrapJsonData(JsonData jsonData, string json, bool clear = true)
    {
        JsonReader reader = new JsonReader(json);
        if(clear)
        {
            jsonData.Clear();
            jsonData.ClearJsonCache();
        }
        ReadValue(jsonData, reader, json);
    }
    public static JsonData GetHierarchy(JsonData root, string hierarchy, bool keepRawData = true)
    {
        var node = root;
        if(string.IsNullOrEmpty(hierarchy) == false)
        {
            var layers = hierarchy.Split('/');
            if(layers != null && layers.Length > 0)
            {
                for(int i = 0; i < layers.Length; i++)
                {
                    var target = layers[i];
                    int index = -1;
                    if(node.IsArray && IsArrayIndex(target, ref index))
                    {
                        node = node[index];
                    }
                    else if(node.IsArray && IsArrayForeach(target, ref index))
                    {
                        var tempNode = new JsonData();
                        foreach(JsonData data in node)
                        {
                            var lastHierarchy = string.Join("/", layers, i + 1, layers.Length - i - 1);
                            tempNode.Add(GetHierarchy(data, lastHierarchy, true));
                        }
                        if(keepRawData)
                        {
                            node = tempNode;
                        }
                        else
                        {
                            WrapJsonData(node, tempNode.ToJson(), true);
                        }
                        break;
                    }
                    else
                    {
                        node = node[target];
                    }
                }
            }
        }
        return node;
    }
    public static bool IsArrayIndex(string pattern, ref int index)
    {
        string element = "";
        if(IsArrayElement(pattern, ref element))
        {
            if(int.TryParse(element, out index))
            {
                return true;
            }
        }
        return false;
    }
    public static bool IsArrayForeach(string pattern, ref int index)
    {
        string element = "";
        if(IsArrayElement(pattern, ref element))
        {
            if(string.Equals(element, Foreach, System.StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }
    public static bool IsArrayElement(string pattern, ref string element)
    {
        if(string.IsNullOrEmpty(pattern) == false)
        {
            if(pattern.StartsWith("[") && pattern.EndsWith("]"))
            {
                var sp = pattern.Substring(1, pattern.Length - 2).Split(':');
                if(sp != null && sp.Length > 1)
                {
                    if(string.Equals(sp[0], Array, System.StringComparison.OrdinalIgnoreCase))
                    {
                        element = sp[1];
                        return true;
                    }
                }
            }
        }
        return false;
    }

 

  现在主要的就是这些了, 有了半图形化的界面方式, 点击连线就能生成映射数据了, 并且从普通数据的类型保持, 到简单嵌套类型的数据映射, 它基本上能完成90%的需求了, 并且很贴心的添加了代码生成:

  减少了大量的工作量, 未来可期...

 

  对了, 在 EditorWindow 下怎样实现画线, 这里用了一个取巧的方法, 每个元素做成一个Toggle, 当左边的某个元素被点击之后, 就设定当前鼠标位置为起始位置(它们都在Scroll里, 需要偏移位置) : 

 leftPoint = (UnityEngine.Event.current.mousePosition) + new Vector2(0, _jsonStringScroll1.y);

  然后通过跟当前鼠标位置做连线(有个 Drawing 的脚本, wiki 可找到) : 

    if(_selectedLeft != null)
    {
        Drawing.DrawArrow(leftPoint - new Vector2(0, _jsonStringScroll1.y), UnityEngine.Event.current.mousePosition, Color.gray, 2, 10);
    }

  

  

---------------------------------------------------------------------

今天发现对于结构映射没有很好的方法, 比如下面这样的 : 

  右边给出了结构, 左边给出了数据, 因为它们的命名不同, 所以需要根据右边的结构来对左边的数据进行映射, 就不能简单地把左边替换到右边去了.

  只能额外添加一个映射功能了, 添加了一个 MappingLogic.StructureMapping 的映射逻辑, 这里 默认为模板中的第一条数据为结构, 从数据中对它进行映射:

    public static void DoMap(JsonData from, JsonData to, MappingInfo info)
    {
        var fromTag = info.mappingLogic != MappingLogic.Write ? GetHierarchy(from, info.from, true) : null;
        var json = "{}";
        switch(info.mappingLogic)
        {
            case MappingLogic.ObjectToArray_Keys:
                {
                    var list = new List<string>(fromTag.Keys);
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            case MappingLogic.ObjectToArray_Values:
                {
                    var list = new List<string>();
                    fromTag.ForeachDictionary((_key, _data) =>
                    {
                        list.Add(_data.ToString());
                    });
                    json = LitJson.JsonMapper.ToJson(list);
                }
                break;
            case MappingLogic.StructureMapping:
                {
                    var targetNode = GetHierarchy(to, info.to);
                    var structure = targetNode[0];

                    var fromVar = GetVarName(info.from);
                    var toVar = GetVarName(info.to);
                    for(int i = 0, imax = fromTag.Count; i < imax; i++)
                    {
                        JsonData sourceData = fromTag[i];
                        JsonData destData = targetNode.Count > i ? targetNode[i] : targetNode[targetNode.Add(Clone(structure))];
                        destData[toVar] = sourceData[fromVar];
                    }
                    return;
                }
                break;
            case MappingLogic.Write:
                {
                    var targetNode = GetHierarchy(to, info.to);
                    WrapJsonData(targetNode, info.writeData, true);
                    return;
                }
                break;
            default: { json = fromTag.ToJson(); } break;
        }
        WrapJsonData(to, info.to, json, true, false);
    }

  而在获取目标节点的时候直接对结构节点返回了:

    public static JsonData GetHierarchy(JsonData root, string hierarchy, bool keepRawData = true)
    {
        var node = root;
        if(string.IsNullOrEmpty(hierarchy) == false)
        {
            var layers = hierarchy.Split('/');
            if(layers != null && layers.Length > 0)
            {
                for(int i = 0; i < layers.Length; i++)
                {
                    var target = layers[i];
                    int index = -1;
                    if(node.IsArray && IsArrayIndex(target, ref index))
                    {
                        node = node[index];
                    }
                    else if(node.IsArray && IsArrayForeach(target, ref index))
                    {
                        if(IsStructureMapping(target))
                        {
                            return node;  // 返回了
                        }
                        var tempNode = new JsonData();
                        foreach(JsonData data in node)
                        {
                            var lastHierarchy = string.Join("/", layers, i + 1, layers.Length - i - 1);
                            tempNode.Add(GetHierarchy(data, lastHierarchy, true));
                        }
                        if(keepRawData)
                        {
                            node = tempNode;
                        }
                        else
                        {
                            WrapJsonData(node, tempNode.ToJson(), true);
                        }
                        break;
                    }
                    else
                    {
                        node = node[target];
                    }
                }
            }
        }
        return node;
    }

  当然这个必然是在 Array 节点下才需要的映射功能, 要不然直接映射就行了.

  映射的代码比较简单:

            case MappingLogic.StructureMapping:
                {
                    var targetNode = GetHierarchy(to, info.to);
                    var structure = targetNode[0];

                    var fromVar = GetVarName(info.from);
                    var toVar = GetVarName(info.to);
                    for(int i = 0, imax = fromTag.Count; i < imax; i++)
                    {
                        JsonData sourceData = fromTag[i];
                        JsonData destData = targetNode.Count > i ? targetNode[i] : targetNode[targetNode.Add(Clone(structure))];
                        destData[toVar] = sourceData[fromVar];
                    }
                    return;
                }
                break;

  这里只需要把两个相关根节点找出来, 然后把模板节点进行克隆, 然后根据映射关系设置即可.

  这样的映射没有改变原有结构, 所以即使进行多层映射也是没有问题的

 

 

 

posted @ 2021-08-25 15:13  tiancaiKG  阅读(1060)  评论(0编辑  收藏  举报