// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using IdentityModel.Client; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using UnrealBuildBase; #nullable enable namespace AutomationTool.Tasks { using JsonObject = System.Text.Json.Nodes.JsonObject; /// /// Parameters for task /// public class WriteJsonValueTaskParameters { /// /// Json file(s) which will be modified /// [TaskParameter(ValidationType = TaskParameterValidationType.FileSpec)] public string File = null!; /// /// Json element to set in each file. Syntax for this string is a limited subset of JsonPath notation, and may support object properties and /// array indices. Any array indices which are omitted or out of range will add a new element to the array (eg. '$.foo.bar[]' will add /// an element to the 'bar' array in the 'foo' object). /// [TaskParameter(ValidationType = TaskParameterValidationType.Default)] public string Key = null!; /// /// New value to set. May be any value JSON value (string, array, object, number, boolean or null). /// [TaskParameter(ValidationType = TaskParameterValidationType.Default)] public string Value = null!; } /// /// Modifies json files by setting a value specified in the key path /// [TaskElement("WriteJsonValue", typeof(WriteJsonValueTaskParameters))] public class WriteJsonValueTask : BgTaskImpl { WriteJsonValueTaskParameters Parameters; /// /// Create a new ModifyJsonValue. /// /// Parameters for this task. public WriteJsonValueTask(WriteJsonValueTaskParameters InParameters) { Parameters = InParameters; } /// /// Placeholder comment /// public override async Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet) { HashSet Files = ResolveFilespec(Unreal.RootDirectory, Parameters.File, TagNameToFileSet); JsonNode? ValueNode; try { ValueNode = String.IsNullOrEmpty(Parameters.Value) ? null : JsonNode.Parse(Parameters.Value); } catch (Exception ex) { throw new AutomationException(ex, $"Unable to parse '{Parameters.Value}': {ex.Message}"); } foreach (FileReference JsonFile in Files) { string JsonText = FileReference.Exists(JsonFile) ? await FileReference.ReadAllTextAsync(JsonFile) : "{}"; if (!Parameters.Key.StartsWith("$", StringComparison.Ordinal)) { throw new AutomationException("Key must be in JsonPath format (eg. $.Foo.Bar[123])"); } JsonNode? RootNode; try { RootNode = JsonNode.Parse(JsonText, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }); } catch (Exception ex) { throw new AutomationException($"Error parsing {JsonFile}: {ex.Message}"); } RootNode = MergeValue(Parameters.Key, 1, RootNode, ValueNode); string NewJsonText = RootNode?.ToJsonString(new JsonSerializerOptions { WriteIndented = true }) ?? String.Empty; DirectoryReference.CreateDirectory(JsonFile.Directory); await FileReference.WriteAllTextAsync(JsonFile, NewJsonText); } } static JsonNode? MergeValue(string Key, int MinIdx, JsonNode? PrevValue, JsonNode? Value) { if (MinIdx == Key.Length) { return Value; } // Find the length of the next token int MaxIdx = MinIdx + 1; while (MaxIdx < Key.Length && Key[MaxIdx] != '[' && Key[MaxIdx] != '.') { MaxIdx++; } // Handle different types of element if (Key[MinIdx] == '.') { JsonObject? Obj = PrevValue as JsonObject; if (Obj != null) { Obj = Obj.Deserialize(); // Clone so we can reattach } Obj ??= new JsonObject(); string PropertyName = Key.Substring(MinIdx + 1, MaxIdx - (MinIdx + 1)); JsonNode? NextNode; Obj.TryGetPropertyValue(PropertyName, out NextNode); Obj[PropertyName] = MergeValue(Key, MaxIdx, NextNode, Value); return Obj; } else if (Key[MinIdx] == '[') { if (Key[MaxIdx - 1] != ']') { throw new AutomationException("Missing ']' in array subscript in Json path expression '{Key}'"); } string IndexStr = Key.Substring(MinIdx + 1, (MaxIdx - 1) - (MinIdx + 1)).Trim(); int Index = int.MaxValue; if (IndexStr.Length > 0) { Index = int.Parse(IndexStr); } JsonArray? Array = PrevValue as JsonArray; if (Array != null) { Array = Array.Deserialize(); } Array ??= new JsonArray(); if (Index < Array.Count) { Array[Index] = MergeValue(Key, MaxIdx, Array[Index], Value); } else { Array.Add(MergeValue(Key, MaxIdx, null, Value)); } return Array; } else { throw new AutomationException($"Unable to parse JSON path after '{Key}'"); } } /// /// Placeholder comment /// public override void Write(XmlWriter Writer) { Write(Writer, Parameters); } /// /// Placeholder comment /// public override IEnumerable FindConsumedTagNames() { foreach (string TagName in FindTagNamesFromFilespec(Parameters.File)) { yield return TagName; } } /// /// Placeholder comment /// public override IEnumerable FindProducedTagNames() { yield break; } } }