using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Text.RegularExpressions; using Newtonsoft.Json; using XP.Common.Localization.Enums; using XP.Common.Localization.Interfaces; using XP.Common.Logging.Interfaces; using XP.ReportEngine.Interfaces; using XP.ReportEngine.Models; namespace XP.ReportEngine.Services { /// /// 表达式数据绑定器实现 | Expression data binder implementation /// 支持 ${} 语法的数据绑定、格式化函数和本地化键解析 /// Supports ${} syntax data binding, format functions and localization key resolution /// public class ExpressionDataBinder : IDataBinder { private readonly ILoggerService _logger; private readonly ILocalizationService _localizationService; private static readonly Regex ExpressionPattern = new(@"\$\{([^}]+)\}", RegexOptions.Compiled); private static readonly Regex LocalizationPattern = new(@"^loc:(.+)$", RegexOptions.Compiled); private static readonly Regex FunctionPattern = new(@"^(\w+)\((.+)\)$", RegexOptions.Compiled); private static readonly Regex IndexPattern = new(@"^([^\[]+)\[(\d+)\]$", RegexOptions.Compiled); public ExpressionDataBinder(ILoggerService logger, ILocalizationService localizationService) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); } public ReportTemplate Bind(ReportTemplate template, ReportContext context) { if (template == null) throw new ArgumentNullException(nameof(template)); if (context == null) throw new ArgumentNullException(nameof(context)); _logger.Info("开始数据绑定 | Starting data binding"); var clonedTemplate = DeepClone(template); if (clonedTemplate.Pages != null) { foreach (var page in clonedTemplate.Pages) { if (page.Elements == null) continue; foreach (var element in page.Elements) { BindElement(element, context); } } } _logger.Info("数据绑定完成 | Data binding completed"); return clonedTemplate; } private void BindElement(TemplateElement element, ReportContext context) { // 文本内容绑定 | Text content binding if (!string.IsNullOrEmpty(element.Content)) { element.Content = ResolveAllExpressions(element.Content, context); } // 图像元素绑定:通过 DataKey 从 ReportContext.Images 获取 ImageData // Image element binding: get ImageData from ReportContext.Images via DataKey if (string.Equals(element.Type, "image", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(element.DataKey) && context.Images != null && context.Images.TryGetValue(element.DataKey, out var imageData)) { element.ImageData = imageData; } // 表格数据绑定:通过 DataKey 从 ReportContext.Properties 获取表格行数据 // Table data binding: get table row data from ReportContext.Properties via DataKey if (string.Equals(element.Type, "table", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(element.DataKey) && context.Properties != null && context.Properties.TryGetValue(element.DataKey, out var tableValue) && tableValue is List> tableRows) { element.TableData = tableRows; } // 表格列头绑定 | Table column header binding if (element.Columns != null) { foreach (var column in element.Columns) { if (!string.IsNullOrEmpty(column.Header)) { column.Header = ResolveAllExpressions(column.Header, context); } } } } private string ResolveAllExpressions(string input, ReportContext context) { return ExpressionPattern.Replace(input, match => { var expression = match.Groups[1].Value.Trim(); return ResolveExpression(expression, context); }); } private string ResolveExpression(string expression, ReportContext context) { // 1. 本地化键 loc:ResourceKey | Localization key var locMatch = LocalizationPattern.Match(expression); if (locMatch.Success) { var resourceKey = locMatch.Groups[1].Value.Trim(); return ResolveLocalizationKey(resourceKey); } // 2. 格式化函数 functionName(params) | Format function var funcMatch = FunctionPattern.Match(expression); if (funcMatch.Success) { var functionName = funcMatch.Groups[1].Value; var paramExpression = funcMatch.Groups[2].Value.Trim(); return ResolveFormatFunction(functionName, paramExpression, context); } // 3. 属性路径 | Property path return ResolvePropertyPath(expression, context); } private string ResolveLocalizationKey(string resourceKey) { try { var value = _localizationService.GetString(resourceKey); if (value == null) { _logger.Warn("本地化键未找到: {Key} | Localization key not found: {Key}", resourceKey); return string.Empty; } return value; } catch (Exception ex) { _logger.Warn("解析本地化键失败: {Key}, 错误: {Message} | Failed to resolve localization key: {Key}, error: {Message}", resourceKey, ex.Message); return string.Empty; } } private string ResolveFormatFunction(string functionName, string paramExpression, ReportContext context) { switch (functionName.ToLowerInvariant()) { case "formatdate": { var value = ResolvePropertyValue(paramExpression, context); return FormatDate(value); } case "formatnumber": { var parts = paramExpression.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); var value = ResolvePropertyValue(parts[0].Trim(), context); var decimals = parts.Length > 1 && int.TryParse(parts[1].Trim(), out var d) ? d : 2; return FormatNumber(value, decimals); } case "formatpercent": { var value = ResolvePropertyValue(paramExpression, context); return FormatPercent(value); } default: { _logger.Warn("未知的格式化函数: {FunctionName} | Unknown format function: {FunctionName}", functionName); return string.Empty; } } } private string ResolvePropertyPath(string path, ReportContext context) { var value = ResolvePropertyValue(path, context); if (value == null) { _logger.Warn("绑定属性未找到: {Path},替换为空字符串 | Binding property not found: {Path}, replacing with empty string", path); return string.Empty; } return ConvertToString(value); } private object ResolvePropertyValue(string path, ReportContext context) { if (string.IsNullOrWhiteSpace(path)) return null; path = path.Trim(); // 优先从 Properties 字典查找 | First look up in Properties dictionary if (context.Properties != null && context.Properties.TryGetValue(path, out var directValue)) { return directValue; } // 尝试从 context 对象解析嵌套路径 | Try nested path from context object var resolved = ResolveNestedPath(path, context); return resolved; } private object ResolveNestedPath(string path, object root) { if (root == null || string.IsNullOrWhiteSpace(path)) return null; var segments = path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); var current = root; foreach (var segment in segments) { if (current == null) return null; var indexMatch = IndexPattern.Match(segment); if (indexMatch.Success) { var propertyName = indexMatch.Groups[1].Value; var index = int.Parse(indexMatch.Groups[2].Value); current = GetPropertyValue(current, propertyName); if (current == null) return null; current = GetIndexedValue(current, index); } else { current = GetPropertyValue(current, segment); } } return current; } private object GetPropertyValue(object obj, string propertyName) { if (obj == null || string.IsNullOrWhiteSpace(propertyName)) return null; // 字典访问 | Dictionary access if (obj is IDictionary dict) { if (dict.TryGetValue(propertyName, out var dictValue)) return dictValue; foreach (var kvp in dict) { if (string.Equals(kvp.Key, propertyName, StringComparison.OrdinalIgnoreCase)) return kvp.Value; } return null; } // 反射获取属性 | Reflection property access var type = obj.GetType(); var propInfo = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (propInfo != null) { try { return propInfo.GetValue(obj); } catch { return null; } } return null; } private object GetIndexedValue(object obj, int index) { if (obj == null || index < 0) return null; if (obj is IList list) { return index < list.Count ? list[index] : null; } if (obj is IEnumerable enumerable) { var i = 0; foreach (var item in enumerable) { if (i == index) return item; i++; } } return null; } private string FormatDate(object value) { if (value == null) return string.Empty; DateTime dateTime; if (value is DateTime dt) { dateTime = dt; } else if (DateTime.TryParse(value.ToString(), out var parsed)) { dateTime = parsed; } else { _logger.Warn("无法将值转换为日期: {Value} | Cannot convert value to date: {Value}", value); return value.ToString(); } var format = _localizationService.CurrentLanguage switch { SupportedLanguage.ZhCN => "yyyy年MM月dd日", SupportedLanguage.ZhTW => "yyyy年MM月dd日", SupportedLanguage.EnUS => "MM/dd/yyyy", _ => "yyyy-MM-dd" }; return dateTime.ToString(format); } private string FormatNumber(object value, int decimals) { if (value == null) return string.Empty; if (!TryConvertToDouble(value, out var number)) { _logger.Warn("无法将值转换为数字: {Value} | Cannot convert value to number: {Value}", value); return value.ToString(); } var culture = GetCultureInfo(); return number.ToString($"N{decimals}", culture); } private string FormatPercent(object value) { if (value == null) return string.Empty; if (!TryConvertToDouble(value, out var number)) { _logger.Warn("无法将值转换为百分比: {Value} | Cannot convert value to percentage: {Value}", value); return value.ToString(); } // 值在 0-1 范围内视为小数百分比 | Values in 0-1 range treated as decimal percentage if (number >= 0 && number <= 1) { number *= 100; } var culture = GetCultureInfo(); return number.ToString("F2", culture) + "%"; } private bool TryConvertToDouble(object value, out double result) { result = 0; if (value == null) return false; switch (value) { case double d: result = d; return true; case float f: result = f; return true; case int i: result = i; return true; case long l: result = l; return true; case decimal dec: result = (double)dec; return true; default: return double.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out result); } } private CultureInfo GetCultureInfo() { return _localizationService.CurrentLanguage switch { SupportedLanguage.ZhCN => new CultureInfo("zh-CN"), SupportedLanguage.ZhTW => new CultureInfo("zh-TW"), SupportedLanguage.EnUS => new CultureInfo("en-US"), _ => CultureInfo.InvariantCulture }; } private string ConvertToString(object value) { if (value == null) return string.Empty; if (value is DateTime dt) return FormatDate(dt); return value.ToString(); } private ReportTemplate DeepClone(ReportTemplate template) { var json = JsonConvert.SerializeObject(template); return JsonConvert.DeserializeObject(json); } } }