using System.Data; using System.Text.Json; using GB5Shared.DTO.Ice; using IceImportDAL.DTO.IceImportPipeline; namespace IceImportDAL.CustomCode.Parsers; // IceMapIceSourceType = 1 (Json). Legacy never implemented this format — the plan explicitly // leaves the exact shape to our judgment, keeping it simple: a top-level JSON array (or single // object) of flat objects becomes the root entity's DataTable, keyed by IceMapRootEntityId (the // DTO-side entity resolved via MENTITY.POCOTODTOLINKID — NOT the raw POCO-side IceMapEntityId, // which is a different MENTITY row whenever the entity has a distinct POCO/DTO split; see // IceMapDTO's doc comment), consistent with FlatTableEntitySplitter's Excel/CSV keying — // DestinationMemberEntityId, not IceMapDetails.EntityId, which for ValueType 4/5 rows means // something different: the entity being looked up. Any property whose value is a JSON array of // objects is flattened into a child entity's DataTable keyed by the raw property name — // IceImportPipeline's table-resolution falls back to matching by column overlap when a table isn't // found under the exact numeric entity-id key, so a freeform JSON property name still resolves // correctly. A synthetic "__ROWINDEX__" column links each child row back to the index of the // root-array row it came from — HierarchyMapper uses this to build the parent/child tree instead of // relying on a shared key column (which JSON, unlike Excel/CSV sheets, has no natural equivalent for). public class JsonSourceFileParser : ISourceFileParser { public const string RowIndexColumn = "__ROWINDEX__"; public int SourceType => 1; public async Task ParseAsync( Stream fileStream, string fileName, IceMapDTO mapDefinition, CancellationToken ct) { using var doc = await JsonDocument.ParseAsync(fileStream, cancellationToken: ct).ConfigureAwait(false); var rootKey = mapDefinition.IceMapRootEntityId.ToString(); var result = new ParsedImportDataSetDTO { SourceFileName = fileName }; var tables = new Dictionary(StringComparer.OrdinalIgnoreCase); IEnumerable rootElements = doc.RootElement.ValueKind switch { JsonValueKind.Array => doc.RootElement.EnumerateArray(), JsonValueKind.Object => new[] { doc.RootElement }, _ => throw new InvalidOperationException( "JSON source file must have a top-level array or object.") }; int rowIndex = 0; foreach (var element in rootElements) { AppendRow(tables, rootKey, element, rowIndex); rowIndex++; } foreach (var kvp in tables) result.Tables[kvp.Key] = kvp.Value; return result; } private static void AppendRow( Dictionary tables, string entityKey, JsonElement element, int rowIndex) { if (element.ValueKind != JsonValueKind.Object) return; if (!tables.TryGetValue(entityKey, out var table)) { table = new DataTable(entityKey); table.Columns.Add(RowIndexColumn, typeof(string)); tables[entityKey] = table; } // Pass 1 — recurse into child arrays and ensure every scalar column exists on the table // BEFORE creating this row, so DataRow's storage is never indexed against a column added // after the row object was allocated. var scalarProps = new List<(string ColName, JsonElement Value)>(); foreach (var prop in element.EnumerateObject()) { if (prop.Value.ValueKind == JsonValueKind.Array && prop.Value.EnumerateArray().Any(x => x.ValueKind == JsonValueKind.Object)) { var childKey = prop.Name.ToUpperInvariant(); foreach (var childElement in prop.Value.EnumerateArray()) AppendRow(tables, childKey, childElement, rowIndex); continue; } if (prop.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array) continue; // nested object / empty array — not flattened in this best-effort parser var colName = prop.Name.ToUpperInvariant(); if (!table.Columns.Contains(colName)) table.Columns.Add(colName, typeof(string)); scalarProps.Add((colName, prop.Value)); } // Pass 2 — now that every column this row needs exists, build and add the row. var row = table.NewRow(); row[RowIndexColumn] = rowIndex.ToString(); foreach (var (colName, value) in scalarProps) row[colName] = value.ValueKind == JsonValueKind.Null ? string.Empty : value.ToString(); table.Rows.Add(row); } }