using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UVC.Log;
namespace UVC.Data.Core
{
///
/// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다.
///
///
/// 이 클래스는 JSON 데이터를 DataObject와 DataArray 형식으로 변환하며, 중첩된 구조(nested structure)도 처리할 수 있습니다.
/// 소스 JSON 객체의 속성을 Mask 객체에 정의된 타입에 따라 적절히 변환합니다.
///
///
/// 기본 사용 예시:
///
/// // 소스 JSON 데이터
/// var sourceJson = JObject.Parse(@"{
/// ""name"": ""김철수"",
/// ""age"": 30,
/// ""isActive"": true
/// }");
///
/// // Mask 객체 (타입 지정용)
/// var maskJson = DataMask.Parse(@"{
/// ""name"": """",
/// ""age"": 0,
/// ""isActive"": false
/// }");
/// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정
///
/// var mapper = new DataMapper(maskJson);
/// DataObject result = mapper.Map(sourceJson);
///
/// // result는 원본과 동일한 구조이며 각 속성이 Mask에 따라 타입 변환됨
///
///
public class DataMapper
{
// 재귀 호출 제한하기 위한 설정 추가
private int maxRecursionDepth = 10;
///
/// 재귀 최대 깊이를 설정하거나 가져옵니다. 이 값을 초과하는 중첩 객체는 간소화된 처리가 적용됩니다.
///
public int MaxRecursionDepth
{
get => maxRecursionDepth;
set => maxRecursionDepth = Math.Max(1, value); // 최소 1 이상 보장
}
///
/// 타입 변환을 위한 마스크 객체
///
private DataMask mask;
public DataMask Mask => mask;
///
/// 병렬 처리를 적용할 배열의 최소 크기
///
private int parallelProcessingThreshold = 1000;
///
/// 병렬 처리를 적용할 배열의 최소 크기를 설정하거나 가져옵니다.
///
public int ParallelProcessingThreshold
{
get => parallelProcessingThreshold;
set => parallelProcessingThreshold = Math.Max(10, value); // 최소 10 이상 보장
}
///
/// 매핑 중 발생한 변환 오류를 추적하는 딕셔너리
///
private Dictionary conversionErrors;
///
/// 매핑 중 발생한 모든 변환 오류에 접근할 수 있는 읽기 전용 속성
///
public IReadOnlyDictionary ConversionErrors => conversionErrors;
///
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
///
public bool SupportsStreamParsing { get; internal set; } = true;
///
/// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
///
public int SupportsStreamLength { get; internal set; } = 10000;
///
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
///
/// 타입 변환을 위한 Mask JSON 객체
///
/// Mask 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만,
/// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다.
///
public DataMapper(DataMask mask)
{
this.mask = mask ?? throw new ArgumentNullException(nameof(mask));
this.conversionErrors = new Dictionary();
}
///
/// 소스 객체를 Mask 객체를 기반으로 매핑하여 새로운 DataObject를 생성합니다.
///
/// 매핑할 원본 JSON 객체
/// 매핑된 DataObject 객체
///
///
/// var mapper = new DataMapper(maskJson);
/// DataObject result = mapper.Map(sourceJson);
/// ULog.Debug(result["name"].ToString()); // "김철수"
/// ULog.Debug(result["age"].ToObject()); // 30
///
///
public DataObject Map(JObject source)
{
return MapObject(source, mask);
}
///
/// 소스 배열을 Mask 객체를 기반으로 매핑하여 새로운 DataArray를 생성합니다.
///
/// 매핑할 원본 JSON 배열
/// 매핑된 DataArray 객체
///
/// 이 메서드는 Mask 객체를 JArray로 변환하여 소스 배열의 각 항목을 매핑합니다.
/// Mask 배열이 비어있으면 원본 배열의 각 항목을 그대로 변환하고,
/// 그렇지 않으면 Mask 배열이 하나인 경우 첫 번째 항목을 템플릿으로 사용하고
/// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다.
///
///
///
/// var sourceArray = JArray.Parse(@"[
/// { ""name"": ""김철수"", ""age"": 30 },
/// { ""name"": ""이영희"", ""age"": 25 }
/// ]");
///
/// var maskJson = DataMask.Parse(@"{
/// ""name"": """",
/// ""age"": 0
/// }");
/// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정
///
/// var mapper = new DataMapper(maskJson);
/// DataArray result = mapper.Map(sourceArray);
/// // result는 원본 배열과 동일한 구조의 DataArray
///
///
public DataArray Map(JArray source)
{
List arr = new List() { mask };
return MapArray(source, arr);
}
///
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
///
/// JSON 데이터 스트림
/// 매핑된 DataObject
public DataObject MapObjectStream(System.IO.Stream jsonStream)
{
if (jsonStream == null)
throw new ArgumentNullException(nameof(jsonStream));
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
// 현재는 Newtonsoft.Json의 기본 역직렬화 사용
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
// 청크 읽기 설정 - 메모리 사용량 최적화
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceObject = serializer.Deserialize(reader);
return Map(sourceObject);
}
}
///
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
///
/// JSON 데이터 스트림
/// 매핑된 DataObject
public DataArray MapArrayStream(System.IO.Stream jsonStream)
{
if (jsonStream == null)
throw new ArgumentNullException(nameof(jsonStream));
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
// 청크 읽기 설정 - 메모리 사용량 최적화
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceArray = serializer.Deserialize(reader);
return Map(sourceArray);
}
}
///
/// 객체를 재귀적으로 매핑합니다.
///
/// 원본 JSON 객체
/// Mask JSON 객체
/// 현재 재귀 깊이
/// 현재 속성 경로(오류 추적용)
/// 매핑된 DataObject 객체
///
/// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다.
/// Mask 객체에 포함되지 않은 속성은 원본 값을 그대로 사용합니다.
///
///
/// 중첩 객체 매핑 예시:
///
/// var sourceJson = JObject.Parse(@"{
/// ""user"": {
/// ""name"": ""김철수"",
/// ""address"": {
/// ""city"": ""서울"",
/// ""zipcode"": ""12345""
/// }
/// }
/// }");
///
/// var maskJson = DataMask.Parse(@"{
/// ""user"": {
/// ""name"": """",
/// ""address"": {
/// ""city"": """",
/// ""zipcode"": """"
/// }
/// }
/// }");
/// maskJson.ObjectIdKey = "user"; // DataObject의 Id로 사용할 속성 지정
///
/// var mapper = new DataMapper(maskJson);
/// var result = mapper.Map(sourceJson);
/// // result는 sourceJson과 동일한 중첩 구조를 유지
///
///
private DataObject MapObject(JObject sourceObject, DataMask maskObject, int depth = 0, string path = "")
{
if (maskObject == null)
{
var dataObj = DataObjectPool.Get();
dataObj.FromJObject(sourceObject);
return dataObj;
}
// Mask가 비어있으면 원본 객체를 그대로 사용, 깊이 제한에 도달하면 간소화된 처리
if (maskObject.Count == 0 || depth >= maxRecursionDepth)
{
DataObject dObj = DataObjectPool.Get();
dObj.IdKey = maskObject.ObjectIdKey;
dObj.Name = maskObject.ObjectName;
// 속성 이름 변환 처리
foreach (var property in sourceObject.Properties())
{
string propertyName = property.Name;
if (maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName))
{
propertyName = maskObject.NamesForReplace[propertyName];
}
dObj[propertyName] = ConvertJTokenToObject(property.Value);
}
return dObj;
}
DataObject target = DataObjectPool.Get();
target.IdKey = maskObject.ObjectIdKey;
target.Name = maskObject.ObjectName;
foreach (var property in sourceObject.Properties())
{
string propertyName = property.Name;
string currentPath = string.IsNullOrEmpty(path) ? propertyName : $"{path}.{propertyName}";
if (maskObject.ContainsKey(propertyName))
{
object maskValue = maskObject[propertyName];
JToken sourceValue = property.Value;
// 속성 이름 변환 처리
if (maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName))
{
propertyName = maskObject.NamesForReplace[propertyName];
}
try
{
// 중첩된 객체 처리
if (sourceValue.Type == JTokenType.Object && maskValue is DataMask maskSubObject)
{
target[propertyName] = MapObject((JObject)sourceValue, maskSubObject, depth + 1, currentPath);
}
// 중첩된 배열 처리
else if (sourceValue.Type == JTokenType.Array && maskValue is List maskArrayTemplate)
{
var arr = MapArray((JArray)sourceValue, maskArrayTemplate, currentPath);
target[propertyName] = arr;
}
// 일반 속성 처리
else
{
MapProperty(propertyName, sourceValue, maskValue, target, currentPath);
}
}
catch (Exception ex)
{
// 예외 정보 저장 후 기본값 설정
conversionErrors[currentPath] = ex;
ULog.Warning($"매핑 중 오류 발생 - 경로: {currentPath}", ex);
// 마스크 값 타입에 맞는 기본값 설정
SetDefaultValueForProperty(propertyName, maskValue, target);
}
}
else
{
// Mask에 없는 속성은 무시 (필요시 설정을 통해 포함 가능)
continue;
}
}
return target;
}
///
/// 예외 발생 시 마스크 값 타입에 맞는 기본값을 설정합니다.
///
private void SetDefaultValueForProperty(string propertyName, object maskValue, DataObject target)
{
if (maskValue is string)
{
target[propertyName] = string.Empty;
}
else if (maskValue is int || maskValue is long || maskValue is short || maskValue is byte)
{
target[propertyName] = 0;
}
else if (maskValue is double || maskValue is float || maskValue is decimal)
{
target[propertyName] = 0.0;
}
else if (maskValue is bool)
{
target[propertyName] = false;
}
else if (maskValue is DateTime)
{
target[propertyName] = null;
}
else if (maskValue is DataMask)
{
target[propertyName] = DataObjectPool.Get();
}
else if (maskValue is List)
{
target[propertyName] = DataArrayPool.Get();
}
else
{
target[propertyName] = null;
}
}
///
/// JToken을 실제 객체로 변환하는 헬퍼 메서드
///
/// 변환할 JToken 객체
/// 변환된 .NET 객체
///
/// 이 메서드는 JToken의 타입에 따라 적절한 .NET 타입의 객체로 변환합니다.
/// 객체는 DataObject로, 배열은 DataArray로 변환되며, 기본 타입은 해당하는 .NET 타입으로 변환됩니다.
///
private object ConvertJTokenToObject(JToken token)
{
if (token == null) return null;
switch (token.Type)
{
case JTokenType.Object:
var dataObject = DataObjectPool.Get();
dataObject.FromJObject((JObject)token);
return dataObject;
case JTokenType.Array:
JArray array = (JArray)token;
if (array.All(item => item.Type == JTokenType.Object))
{
return DataArrayPool.Get().FromJArray(array);
}
return array.ToObject