// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using System.Xml; using EpicGames.Core; 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 { get; set; } = 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 { get; set; } = 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 { get; set; } = null!; } /// /// Modifies json files by setting a value specified in the key path /// [TaskElement("WriteJsonValue", typeof(WriteJsonValueTaskParameters))] public class WriteJsonValueTask : BgTaskImpl { readonly WriteJsonValueTaskParameters _parameters; /// /// Create a new ModifyJsonValue. /// /// Parameters for this task. public WriteJsonValueTask(WriteJsonValueTaskParameters parameters) { _parameters = parameters; } /// /// 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 = Int32.MaxValue; if (indexStr.Length > 0) { index = Int32.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; } } }