Files
XRLib/Assets/Scripts/UVC/Data/DataMapper.cs

546 lines
22 KiB
C#

using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace UVC.Data
{
/// <summary>
/// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다.
/// </summary>
/// <remarks>
/// 이 클래스는 JSON 데이터를 DataObject와 DataArray 형식으로 변환하며, 중첩된 구조(nested structure)도 처리할 수 있습니다.
/// 소스 JSON 객체의 속성을 Mask 객체에 정의된 타입에 따라 적절히 변환합니다.
/// </remarks>
/// <example>
/// 기본 사용 예시:
/// <code>
/// // 소스 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에 따라 타입 변환됨
/// </code>
/// </example>
public class DataMapper
{
// 재귀 호출 제한하기 위한 설정 추가
private int maxRecursionDepth = 10;
private DataMask mask;
/// <summary>
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
/// </summary>
public bool SupportsStreamParsing { get; internal set; } = true;
/// <summary>
/// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
/// </summary>
public int SupportsStreamLength { get; internal set; } = 10000;
/// <summary>
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="mask">타입 변환을 위한 Mask JSON 객체</param>
/// <remarks>
/// Mask 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만,
/// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다.
/// </remarks>
public DataMapper(DataMask mask)
{
this.mask = mask;
}
/// <summary>
/// 소스 객체를 Mask 객체를 기반으로 매핑하여 새로운 DataObject를 생성합니다.
/// </summary>
/// <param name="source">매핑할 원본 JSON 객체</param>
/// <returns>매핑된 DataObject 객체</returns>
/// <example>
/// <code>
/// var mapper = new DataMapper(maskJson);
/// DataObject result = mapper.Map(sourceJson);
/// Debug.Log(result["name"].ToString()); // "김철수"
/// Debug.Log(result["age"].ToObject&lt;int&gt;()); // 30
/// </code>
/// </example>
public DataObject Mapping(JObject source)
{
return MapObject(source, mask);
}
/// <summary>
/// 소스 배열을 Mask 객체를 기반으로 매핑하여 새로운 DataArray를 생성합니다.
/// </summary>
/// <param name="source">매핑할 원본 JSON 배열</param>
/// <returns>매핑된 DataArray 객체</returns>
/// <remarks>
/// 이 메서드는 Mask 객체를 JArray로 변환하여 소스 배열의 각 항목을 매핑합니다.
/// Mask 배열이 비어있으면 원본 배열의 각 항목을 그대로 변환하고,
/// 그렇지 않으면 Mask 배열이 하나인 경우 첫 번째 항목을 템플릿으로 사용하고
/// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다.
/// </remarks>
/// <example>
/// <code>
/// 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
/// </code>
/// </example>
public DataArray Mapping(JArray source)
{
JArray arr = new JArray(mask);
return MapArray(source, arr);
}
/// <summary>
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
/// </summary>
/// <param name="jsonStream">JSON 데이터 스트림</param>
/// <returns>매핑된 DataObject</returns>
public DataObject MapObjectStream(System.IO.Stream jsonStream)
{
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceObject = serializer.Deserialize<JObject>(reader);
return Mapping(sourceObject);
}
}
/// <summary>
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
/// </summary>
/// <param name="jsonStream">JSON 데이터 스트림</param>
/// <returns>매핑된 DataObject</returns>
public DataArray MapArrayStream(System.IO.Stream jsonStream)
{
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceArray = serializer.Deserialize<JArray>(reader);
return Mapping(sourceArray);
}
}
/// <summary>
/// 객체를 재귀적으로 매핑합니다.
/// </summary>
/// <param name="sourceObject">원본 JSON 객체</param>
/// <param name="maskObject">Mask JSON 객체</param>
/// <returns>매핑된 DataObject 객체</returns>
/// <remarks>
/// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다.
/// Mask 객체에 포함되지 않은 속성은 원본 값을 그대로 사용합니다.
/// </remarks>
/// <example>
/// 중첩 객체 매핑 예시:
/// <code>
/// 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과 동일한 중첩 구조를 유지
/// </code>
/// </example>
private DataObject MapObject(JObject sourceObject, DataMask maskObject, int depth = 0)
{
if (depth >= maxRecursionDepth)
{
// 깊이 제한에 도달하면 간소화된 처리
return new DataObject(sourceObject) { IdKey = maskObject.ObjectIdKey, Name = maskObject.ObjectName };
}
DataObject target = new DataObject() { IdKey = maskObject.ObjectIdKey, Name = maskObject.ObjectName };
foreach (var property in sourceObject.Properties())
{
if (maskObject.ContainsKey(property.Name))
{
JToken maskValue = maskObject[property.Name];
JToken sourceValue = property.Value;
// 중첩된 객체 처리
if (sourceValue.Type == JTokenType.Object && maskValue.Type == JTokenType.Object)
{
target[property.Name] = MapObject((JObject)sourceValue, (DataMask)maskValue, depth + 1);
}
// 중첩된 배열 처리
else if (sourceValue.Type == JTokenType.Array && maskValue.Type == JTokenType.Array)
{
var arr = MapArray((JArray)sourceValue, (JArray)maskValue);
target[property.Name] = arr;
}
else
{
string propertyName = property.Name;
if(maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName))
{
propertyName = maskObject.NamesForReplace[propertyName];
}
MapProperty(propertyName, sourceValue, maskValue, target);
}
}
else
{
continue; // Mask에 없는 속성은 무시
}
}
return target;
}
/// <summary>
/// JToken을 실제 객체로 변환하는 헬퍼 메서드
/// </summary>
/// <param name="token">변환할 JToken 객체</param>
/// <returns>변환된 .NET 객체</returns>
/// <remarks>
/// 이 메서드는 JToken의 타입에 따라 적절한 .NET 타입의 객체로 변환합니다.
/// 객체는 DataObject로, 배열은 DataArray로 변환되며, 기본 타입은 해당하는 .NET 타입으로 변환됩니다.
/// </remarks>
private object ConvertJTokenToObject(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
return new DataObject((JObject)token);
case JTokenType.Array:
JArray array = (JArray)token;
if (array.All(item => item.Type == JTokenType.Object))
{
return new DataArray((JArray)token);
}
return array.ToObject<object>();
case JTokenType.Integer:
return token.ToObject<int>();
case JTokenType.Float:
return token.ToObject<double>();
case JTokenType.String:
return token.ToObject<string>();
case JTokenType.Boolean:
return token.ToObject<bool>();
case JTokenType.Date:
return token.ToObject<DateTime>();
case JTokenType.Null:
return null;
default:
return token.ToObject<object>();
}
}
/// <summary>
/// 배열을 재귀적으로 매핑합니다.
/// </summary>
/// <param name="sourceArray">원본 JSON 배열</param>
/// <param name="maskArray">Mask JSON 배열</param>
/// <returns>매핑된 DataArray 객체</returns>
/// <remarks>
/// Mask 배열이 비어있으면, 원본 배열의 각 항목을 DataObject로 변환하여 복사합니다.
/// 단순 값(문자열, 숫자 등)은 "value" 키를 가진 DataObject로 래핑됩니다.
///
/// Mask 배열이 비어있지 않으면, Mask 배열의 첫 번째 항목을 템플릿으로 사용하고
/// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다.
/// 원본 배열의 각 항목을 매핑합니다. 중첩 배열은 "items" 키를 가진 DataObject로 래핑됩니다.
/// </remarks>
/// <example>
/// 배열 매핑 예시:
/// <code>
/// var sourceJson = JObject.Parse(@"{
/// ""contacts"": [
/// { ""type"": ""mobile"", ""number"": ""010-1234-5678"" },
/// { ""type"": ""home"", ""number"": ""02-123-4567"" }
/// ]
/// }");
///
/// var maskJson = DataMask.Parse(@"{
/// ""contacts"": [
/// { ""type"": """", ""number"": """" }
/// ]
/// }");
/// maskJson.ObjectIdKey = "contacts"; // DataObject의 Id로 사용할 속성 지정
///
/// var mapper = new DataMapper(maskJson);
/// var result = mapper.Map(sourceJson);
/// // result.contacts는 원본 배열과 동일한 구조의 DataArray
/// </code>
/// </example>
private DataArray MapArray(JArray sourceArray, JArray maskArray)
{
// 빠른 초기 크기 할당으로 재할당 방지
DataArray targetArray = new DataArray(sourceArray.Count);
// 특정 크기 이상일 경우 병렬 처리 적용
if (sourceArray.Count > 1000 && maskArray.Count <= 1)
{
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
// 배열을 병렬로 처리하되, 순서 유지
var results = new DataObject[sourceArray.Count];
Parallel.For(0, sourceArray.Count, parallelOptions, i =>
{
var sourceItem = sourceArray[i];
var maskTemplate = maskArray.Count > 0 ? maskArray[0] : null;
if (sourceItem.Type == JTokenType.Object && maskTemplate != null && maskTemplate.Type == JTokenType.Object)
{
results[i] = MapObject((JObject)sourceItem, (DataMask)maskTemplate);
}
else
{
// 단순 값 처리...
var dataObject = new DataObject();
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
results[i] = dataObject;
}
});
// 결과를 순서대로 추가
targetArray.AddRange(results);
return targetArray;
}
// Mask 배열이 비어있으면 원본 배열을 그대로 사용
if (maskArray.Count == 0)
{
foreach (JToken sourceItem in sourceArray)
{
if (sourceItem.Type == JTokenType.Object)
{
targetArray.Add(new DataObject((JObject)sourceItem));
}
else
{
// DataObject가 아닌 경우, 새 DataObject를 만들고 값을 넣어줍니다
var dataObject = new DataObject();
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
targetArray.Add(dataObject);
}
}
return targetArray;
}
// Mask 배열 개수가 1개면 1개만 템플릿으로 사용
JToken maskTemplate = maskArray.First;
int idx = 0;
foreach (JToken sourceItem in sourceArray)
{
if (maskArray.Count > 1)
{
if (idx > 0 && idx < maskArray.Count)
{
maskTemplate = maskArray[idx];
}
else if (idx >= maskArray.Count)
{
continue; // Mask 배열의 범위를 벗어나면 무시
}
}
if (sourceItem.Type == JTokenType.Object && maskTemplate.Type == JTokenType.Object)
{
targetArray.Add(MapObject((JObject)sourceItem, (DataMask)maskTemplate));
}
else if (sourceItem.Type == JTokenType.Array && maskTemplate.Type == JTokenType.Array)
{
// DataArray는 DataObject만 담을 수 있으므로, 배열을 처리하는 방식을 변경해야 함
var nestedArray = MapArray((JArray)sourceItem, (JArray)maskTemplate);
var container = new DataObject();
container.Add("items", nestedArray);
targetArray.Add(container);
}
else
{
// 단순 값을 DataObject로 래핑
var dataObject = new DataObject();
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
targetArray.Add(dataObject);
}
idx++;
}
return targetArray;
}
/// <summary>
/// 개별 속성을 매핑합니다.
/// </summary>
/// <param name="propertyName">속성 이름</param>
/// <param name="sourceValue">매핑할 원본 값</param>
/// <param name="maskValue">타입을 결정하는 Mask 값</param>
/// <param name="target">값을 추가할 대상 DataObject</param>
/// <remarks>
/// 이 메서드는 Mask 값의 타입에 따라 원본 값을 적절한 타입으로 변환합니다.
///
/// 지원되는 타입:
/// - 문자열 (JTokenType.String)
/// - 정수 (JTokenType.Integer)
/// - 실수 (JTokenType.Float)
/// - 불리언 (JTokenType.Boolean)
/// - 날짜/시간 (JTokenType.Date)
/// - 열거형 (Enum)
/// - DataMap (문자열 매핑 딕셔너리)
///
/// 타입 변환이 불가능한 경우 원본 값이 그대로 사용됩니다.
/// </remarks>
/// <example>
/// 다양한 타입 매핑 예시:
/// <code>
/// var sourceJson = JObject.Parse(@"{
/// ""name"": ""김철수"",
/// ""age"": 30,
/// ""height"": 175.5,
/// ""isActive"": true,
/// ""birthDate"": ""1990-01-01T00:00:00"",
/// ""status"": ""Active""
/// }");
///
/// // Mask 객체 설정 (열거형 포함)
/// var maskJson = new DataMask();
/// maskJson["name"] = "";
/// maskJson["age"] = 0;
/// maskJson["height"] = 0.0;
/// maskJson["isActive"] = false;
/// maskJson["birthDate"] = JToken.FromObject(DateTime.Now);
/// maskJson["status"] = JToken.FromObject(UserStatus.Inactive);
///
/// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정
///
/// var mapper = new DataMapper(maskJson);
/// var result = mapper.Map(sourceJson);
/// // result에는 모든 속성이 적절한 타입으로 변환됨
/// </code>
/// </example>
private void MapProperty(string propertyName, JToken sourceValue, JToken maskValue, DataObject target)
{
// 소스 값이 널이면 바로 널 설정
if (sourceValue.Type == JTokenType.Null)
{
target[propertyName] = null;
return;
}
if (sourceValue.Type == maskValue.Type)
{
switch (sourceValue.Type)
{
case JTokenType.String:
target[propertyName] = sourceValue.ToObject<string>();
return;
case JTokenType.Integer:
target[propertyName] = sourceValue.ToObject<int>();
return;
case JTokenType.Float:
target[propertyName] = sourceValue.ToObject<double>();
return;
case JTokenType.Boolean:
target[propertyName] = sourceValue.ToObject<bool>();
return;
}
}
if (maskValue.Type == JTokenType.Date && sourceValue.Type == JTokenType.String)
{
string dateStr = sourceValue.ToObject<string>();
if (DateTime.TryParse(dateStr, out DateTime dateValue))
{
target[propertyName] = dateValue;
}
else
{
target[propertyName] = null;
}
}
else if (maskValue.ToObject<object>()?.GetType()?.IsEnum == true && sourceValue.Type == JTokenType.String)
{
Type enumType = maskValue.ToObject<object>().GetType();
target[propertyName] = Enum.Parse(enumType, sourceValue.ToObject<string>(), true);
}
else if (maskValue.Type == JTokenType.Object && sourceValue.Type == JTokenType.String)
{
try
{
// 먼저 DataMap로 변환 시도
var DataMap = maskValue.ToObject<DataMap>();
if (DataMap != null)
{
string strValue = sourceValue.ToObject<string>();
if (DataMap.ContainsKey(strValue))
{
target[propertyName] = DataMap[strValue];
}
else
{
target[propertyName] = strValue;
}
}
else
{
// DataMap가 아니면 소스 값 그대로 사용
target[propertyName] = sourceValue.ToObject<object>();
}
}
catch
{
// 변환 실패 시 소스 값 그대로 사용
target[propertyName] = sourceValue.ToObject<object>();
}
}
else
{
// 기타 타입은 그대로 객체로 변환
target[propertyName] = ConvertJTokenToObject(sourceValue);
}
}
}
}