388 lines
20 KiB
C#
388 lines
20 KiB
C#
#nullable enable
|
|
|
|
using Cysharp.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using UnityEngine;
|
|
using UVC.Data.Core;
|
|
using UVC.Log;
|
|
|
|
namespace UVC.Data.Http
|
|
{
|
|
/// <summary>
|
|
/// HTTP 응답 데이터를 처리하는 정적 클래스입니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 클래스는 `HttpDataFetcher`로부터 데이터 처리 로직을 분리하여 재사용성을 높입니다.
|
|
/// HTTP 응답(문자열)을 입력받아, 설정된 규칙(`HttpRequestConfig`)에 따라 파싱, 유효성 검사,
|
|
/// 데이터 변환(`IDataObject`로 매핑) 및 결과 핸들러 호출까지의 과정을 담당합니다.
|
|
/// 모든 메서드는 정적(static)이므로 인스턴스 생성 없이 사용할 수 있습니다.
|
|
/// </remarks>
|
|
public static class HttpDataProcessor
|
|
{
|
|
/// <summary>
|
|
/// HTTP 응답 문자열을 비동기적으로 처리하고, 결과를 IDataObject로 변환한 후 적절한 핸들러를 호출합니다.
|
|
/// </summary>
|
|
/// <param name="key">요청을 식별하는 고유 키. 로그 및 데이터 저장소에서 사용됩니다.</param>
|
|
/// <param name="info">HTTP 요청에 대한 모든 설정(URL, 매퍼, 핸들러 등)을 담고 있는 객체입니다.</param>
|
|
/// <param name="responseContent">HTTP 요청으로부터 받은 원시 응답 문자열입니다.</param>
|
|
/// <param name="cancellationToken">비동기 작업을 취소하기 위한 토큰입니다.</param>
|
|
/// <remarks>
|
|
/// 이 메서드는 다음과 같은 순서로 응답을 처리합니다.
|
|
/// 1. 응답 문자열이 비어 있는지 확인합니다.
|
|
/// 2. `HttpResponseMask`를 적용하여 응답의 성공 여부를 판단하고 실제 데이터 부분을 추출합니다.
|
|
/// 3. 데이터가 분할 처리(Split) 대상인지, 단일 객체(Object)인지, 배열(Array)인지 확인하고 각각의 처리 메서드를 호출합니다.
|
|
/// 4. 처리된 데이터(`IDataObject`)를 `DataRepository`에 추가하거나 업데이트합니다.
|
|
/// 5. 설정에 따라 성공(`SuccessHandler`) 또는 실패(`FailHandler`) 핸들러를 메인 스레드에서 호출하여 UI 업데이트 등을 안전하게 처리할 수 있도록 합니다.
|
|
/// 6. 모든 과정에서 발생하는 예외(JSON 파싱, 작업 취소 등)를 처리하고 로그를 남깁니다.
|
|
/// </remarks>
|
|
public static void ProcessResponse(string key, HttpRequestConfig info, string responseContent, CancellationToken cancellationToken)
|
|
{
|
|
// 매핑된 최종 데이터 객체를 담을 변수입니다. try-finally 블록에서 자원 해제를 위해 외부에 선언합니다.
|
|
IDataObject? mappedObject = null;
|
|
try
|
|
{
|
|
// 앞뒤 공백 제거
|
|
responseContent = responseContent.Trim();
|
|
|
|
// 응답이 비어있으면 실패로 간주하고 FailHandler를 호출합니다.
|
|
if (string.IsNullOrEmpty(responseContent))
|
|
{
|
|
// 핸들러는 UI와 상호작용할 수 있으므로 메인 스레드에서 호출합니다.
|
|
if (info.FailHandler != null)
|
|
{
|
|
UniTask.Post(() => info.FailHandler.Invoke("Response content is null or empty."));
|
|
}
|
|
return;
|
|
}
|
|
// ResponseMask를 사용해 응답이 성공적인지 확인하고, 실제 데이터 부분을 추출합니다.
|
|
// 예를 들어, 응답이 {"status":"OK", "data":{...}} 형태일 때, "status"가 "OK"인지 확인하고 "data" 부분만 가져옵니다.
|
|
HttpResponseResult responseResult = info.ResponseMask.Apply(responseContent);
|
|
if (!responseResult.IsSuccess)
|
|
{
|
|
if (info.FailHandler != null)
|
|
{
|
|
string errorMessage = responseResult.Message!;
|
|
UniTask.Post(() => info.FailHandler.Invoke(errorMessage));
|
|
}
|
|
return;
|
|
}
|
|
// 마스크를 통해 추출된 실제 데이터
|
|
responseContent = responseResult.Data!.Trim();
|
|
// 응답을 최상위 키를 기준으로 분할 처리해야 하는 경우
|
|
if (info.SplitResponseByKey && responseContent.StartsWith("{"))
|
|
{
|
|
ProcessSplitResponse(info, responseContent, cancellationToken);
|
|
return; // 분할 처리가 완료되면 이 메서드의 나머지 로직은 실행하지 않습니다.
|
|
}
|
|
// 응답이 JSON 객체 형태인 경우 (e.g., {"id": 1, "name": "item"})
|
|
else if (responseContent.StartsWith("{"))
|
|
{
|
|
mappedObject = ProcessObjectResponse(info, responseContent, cancellationToken);
|
|
if (mappedObject == null) return;
|
|
}
|
|
// 응답이 JSON 배열 형태인 경우 (e.g., [{"id": 1}, {"id": 2}])
|
|
else if (responseContent.StartsWith("["))
|
|
{
|
|
mappedObject = ProcessArrayResponse(info, responseContent, cancellationToken);
|
|
if (mappedObject == null) return;
|
|
}
|
|
// 핸들러 호출 전 작업 취소 요청이 있었는지 다시 확인합니다.
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// 매핑된 데이터를 DataRepository에 저장하거나 업데이트합니다.
|
|
var repoObject = mappedObject;
|
|
if (mappedObject != null)
|
|
{
|
|
// DataRepository는 모든 데이터를 중앙에서 관리하는 저장소입니다.
|
|
repoObject = DataRepository.Instance.AddOrUpdateData(key, mappedObject, info.UpdatedDataOnly);
|
|
// 만약 반환된 객체가 원본과 같다면, 핸들러에 전달하기 위해 복제본을 만듭니다.
|
|
if (repoObject == mappedObject) repoObject = mappedObject.Clone(fromPool: false);
|
|
}
|
|
// 'UpdatedDataOnly' 옵션이 켜져 있고, 실제로 데이터가 업데이트되었을 때만 핸들러를 호출합니다.
|
|
if (info.UpdatedDataOnly)
|
|
{
|
|
if (repoObject != null && repoObject.UpdatedCount > 0)
|
|
{
|
|
if (info.SuccessHandler != null)
|
|
{
|
|
var handlerData = repoObject;
|
|
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// 최종적으로 처리된 데이터가 있는 경우 성공 핸들러를 호출합니다.
|
|
if (repoObject != null)
|
|
{
|
|
if (info.SuccessHandler != null)
|
|
{
|
|
var handlerData = repoObject;
|
|
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
|
|
}
|
|
}
|
|
// 처리된 데이터가 없는 경우 실패 핸들러를 호출합니다.
|
|
else
|
|
{
|
|
if (info.FailHandler != null)
|
|
{
|
|
UniTask.Post(() => info.FailHandler.Invoke("Data is Null"));
|
|
}
|
|
}
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
// JSON 파싱 중 오류가 발생한 경우
|
|
ULog.Error($"JSON parsing error for {key}: {ex.Message}\nResponse: {responseContent}", ex);
|
|
if (info.FailHandler != null)
|
|
{
|
|
UniTask.Post(() => info.FailHandler.Invoke($"JSON parsing error: {ex.Message}"));
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 작업이 외부에서 취소된 경우, 예외를 다시 던져 상위 호출자에게 알립니다.
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 그 외 모든 예외 처리
|
|
ULog.Error($"Error processing response for {key}: {ex.Message}", ex);
|
|
if (info.FailHandler != null)
|
|
{
|
|
UniTask.Post(() => info.FailHandler.Invoke($"Error processing response: {ex.Message}"));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// 매핑 과정에서 생성된 IDataObject 객체는 내부적으로 풀링될 수 있습니다.
|
|
// 처리가 끝난 객체는 다시 풀에 반환하여 메모리 할당을 줄입니다.
|
|
mappedObject?.ReturnToPool();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 키-값 형태의 JSON 응답을 각 키별로 분할하여 처리합니다.
|
|
/// </summary>
|
|
/// <param name="info">HTTP 요청 설정 객체.</param>
|
|
/// <param name="jsonResponse">분할할 JSON 객체 문자열.</param>
|
|
/// <param name="cancellationToken">비동기 작업을 위한 취소 토큰.</param>
|
|
/// <remarks>
|
|
/// 예를 들어, `{"AGV": [...], "ALARM": [...]}` 같은 응답을 받으면,
|
|
/// "AGV"와 "ALARM"을 각각 별개의 데이터로 간주하고 처리합니다.
|
|
/// 각 키에 대해 별도의 DataMapper나 Validator를 `HttpRequestConfig`에 설정할 수 있습니다.
|
|
/// </remarks>
|
|
public static void ProcessSplitResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken = null)
|
|
{
|
|
JObject responseObject = JObject.Parse(jsonResponse);
|
|
// JSON 객체의 모든 프로퍼티(키-값 쌍)를 순회합니다.
|
|
foreach (var property in responseObject.Properties())
|
|
{
|
|
cancellationToken?.ThrowIfCancellationRequested();
|
|
|
|
string subKey = property.Name; // "AGV", "ALARM" 등
|
|
|
|
JToken subToken = property.Value; // `[...]` 또는 `{...}`
|
|
IDataObject? subMappedObject = null;
|
|
|
|
// 현재 키(subKey)에 대한 분할 설정을 가져옵니다.
|
|
DataMapperValidator? splitConfig = null;
|
|
if (info.SplitConfigs != null) info.SplitConfigs.TryGetValue(subKey, out splitConfig);
|
|
|
|
// 분할 설정에 지정된 매퍼와 유효성 검사기를 사용합니다. 없으면 null입니다.
|
|
var subKeyDataMapper = splitConfig?.DataMapper;
|
|
var subKeyValidator = splitConfig?.Validator;
|
|
|
|
//매퍼가 null인 경우, 해당 키는 처리하지 않습니다.
|
|
if (subKeyDataMapper == null) continue;
|
|
|
|
try
|
|
{
|
|
// 하위 데이터가 배열 또는 객체 형태인지에 따라 적절한 처리 메서드를 호출합니다.
|
|
if (subToken is JArray && subToken.Count() > 0)
|
|
{
|
|
// 분할 처리 중 개별 항목의 실패가 전체 실패로 이어지지 않도록 `invokeFailHandler`를 false로 설정합니다.
|
|
subMappedObject = ProcessArrayResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
|
|
}
|
|
else if (subToken is JObject && subToken.Count() > 0)
|
|
{
|
|
subMappedObject = ProcessObjectResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
|
|
}
|
|
|
|
//Debug.Log($"Processing split response for key: {subKey}, {subToken.Count()}. {subToken.GetType().Name} subMappedObject == null:{subMappedObject == null}");
|
|
|
|
// 매핑된 결과가 없으면 (예: 유효성 검사 실패) 다음 키로 넘어갑니다.
|
|
if (subMappedObject == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// 분할된 데이터를 `subKey`를 키로 사용하여 DataRepository에 저장합니다.
|
|
var repoObject = DataRepository.Instance.AddOrUpdateData(subKey, subMappedObject, info.UpdatedDataOnly);
|
|
|
|
// 핸들러 호출 조건(데이터가 있거나, 업데이트된 경우)을 확인합니다.
|
|
bool shouldInvokeHandler = repoObject != null && (!info.UpdatedDataOnly || repoObject.UpdatedCount > 0);
|
|
|
|
if (shouldInvokeHandler && info.SuccessHandler != null)
|
|
{
|
|
var handlerData = repoObject.Clone(fromPool: false);
|
|
//await UniTask.SwitchToMainThread();
|
|
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
|
|
// 다음 키 처리를 위해 다시 스레드 풀로 전환합니다.
|
|
//await UniTask.SwitchToThreadPool();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
// 사용이 끝난 객체는 풀에 반환합니다.
|
|
subMappedObject?.ReturnToPool();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IDataObject? ProcessObjectResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
|
|
{
|
|
Action<string>? failHandler = invokeFailHandler ? info.FailHandler : null;
|
|
return ProcessObjectResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 단일 JSON 객체 응답을 검증하고 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="jsonResponse">처리할 JSON 객체 문자열.</param>
|
|
/// <param name="cancellationToken">취소 토큰.</param>
|
|
/// <param name="dataMapper">사용할 데이터 매퍼. null이면 원시 데이터를 반환합니다.</param>
|
|
/// <param name="validator">사용할 유효성 검사기. null이면 검사를 생략합니다.</param>
|
|
/// <param name="failHandler">실패 시 호출할 핸들러.</param>
|
|
/// <returns>매핑된 IDataObject 또는 null(실패 시).</returns>
|
|
/// <remarks>
|
|
/// 이 메서드는 먼저 `DataValidator`를 통해 데이터의 유효성을 검사하고,
|
|
/// 통과하면 `DataMapper`를 사용해 JSON을 `IDataObject`로 변환합니다.
|
|
/// 대용량 JSON의 경우 메모리 효율을 위해 스트림 기반 파싱을 지원합니다.
|
|
/// </remarks>
|
|
public static IDataObject? ProcessObjectResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
|
|
{
|
|
// 유효성 검사기가 설정된 경우
|
|
if (validator != null)
|
|
{
|
|
bool isValid;
|
|
// 스트림 파싱을 지원하고 데이터가 충분히 큰 경우, 스트림으로 유효성을 검사합니다.
|
|
if (validator.SupportsStreamParsing && jsonResponse.Length > validator.SupportsStreamLength)
|
|
{
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse));
|
|
isValid = validator.IsValid(stream);
|
|
}
|
|
else
|
|
{
|
|
JObject source = JObject.Parse(jsonResponse);
|
|
isValid = validator.IsValid(source);
|
|
}
|
|
|
|
// 유효성 검사에 실패하면 실패 핸들러를 호출하고 null을 반환합니다.
|
|
if (!isValid)
|
|
{
|
|
if (failHandler != null)
|
|
{
|
|
UniTask.Post(() => failHandler.Invoke("Data is not Valid"));
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
cancellationToken?.ThrowIfCancellationRequested();
|
|
|
|
// 데이터 매퍼가 설정된 경우
|
|
if (dataMapper != null)
|
|
{
|
|
// 스트림 파싱 지원 여부에 따라 매핑 방식을 선택합니다.
|
|
if (dataMapper.SupportsStreamParsing && jsonResponse.Length > dataMapper.SupportsStreamLength)
|
|
{
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse));
|
|
return dataMapper.MapObjectStream(stream);
|
|
}
|
|
else
|
|
{
|
|
JObject source = JObject.Parse(jsonResponse);
|
|
return dataMapper.Map(source);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 매퍼가 없으면 원시 JSON 문자열을 그대로 `DataObject`로 감싸 반환합니다.
|
|
return new DataObject(jsonResponse);
|
|
}
|
|
}
|
|
|
|
private static IDataObject? ProcessArrayResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
|
|
{
|
|
Action<string>? failHandler = invokeFailHandler ? info.FailHandler : null;
|
|
return ProcessArrayResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
|
|
}
|
|
|
|
/// <summary>
|
|
/// JSON 배열 응답을 검증하고 매핑합니다.
|
|
/// </summary>
|
|
/// <param name="jsonResponse">처리할 JSON 배열 문자열.</param>
|
|
/// <param name="cancellationToken">취소 토큰.</param>
|
|
/// <param name="dataMapper">사용할 데이터 매퍼. null이면 원시 데이터를 반환합니다.</param>
|
|
/// <param name="validator">사용할 유효성 검사기. null이면 검사를 생략합니다.</param>
|
|
/// <param name="failHandler">실패 시 호출할 핸들러.</param>
|
|
/// <returns>매핑된 IDataObject(보통 DataArray) 또는 null(실패 시).</returns>
|
|
/// <remarks>
|
|
/// `DataValidator`를 통해 배열의 각 항목을 필터링하여 유효한 데이터만 포함하는 새 배열을 만들 수 있습니다.
|
|
/// 그 후 `DataMapper`를 통해 배열 전체를 `DataArray` 객체로 변환합니다.
|
|
/// </remarks>
|
|
public static IDataObject? ProcessArrayResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
|
|
{
|
|
JArray? sourceArray;
|
|
|
|
// 유효성 검사기가 설정된 경우
|
|
if (validator != null)
|
|
{
|
|
// 스트림 파싱 지원 여부에 따라 유효한 데이터만 필터링합니다.
|
|
if (validator.SupportsStreamParsing && jsonResponse.Length > validator.SupportsStreamLength)
|
|
{
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse));
|
|
sourceArray = validator.GetValidData(stream);
|
|
}
|
|
else
|
|
{
|
|
sourceArray = validator.GetValidData(JArray.Parse(jsonResponse));
|
|
}
|
|
|
|
// 유효한 데이터가 하나도 없는 경우
|
|
if (sourceArray == null || sourceArray.Count == 0)
|
|
{
|
|
if (failHandler != null)
|
|
{
|
|
UniTask.Post(() => failHandler.Invoke("Data is not Valid or empty after validation"));
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 검사기가 없으면 원본 배열을 그대로 사용합니다.
|
|
sourceArray = JArray.Parse(jsonResponse);
|
|
}
|
|
|
|
cancellationToken?.ThrowIfCancellationRequested();
|
|
|
|
// 데이터 매퍼가 설정된 경우, 필터링된 배열을 매핑합니다.
|
|
if (dataMapper != null)
|
|
{
|
|
return dataMapper.Map(sourceArray);
|
|
}
|
|
else
|
|
{
|
|
// 매퍼가 없으면 필터링된 배열을 `DataArray`로 감싸 반환합니다.
|
|
return new DataArray(sourceArray.ToString());
|
|
}
|
|
}
|
|
}
|
|
} |