using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AggregateConfigBuildTask.FileHandlers; using Microsoft.Build.Framework; namespace AggregateConfigBuildTask { internal static class ObjectManager { /// /// Combines all files of a specified type from a given directory into a single entity, while applying optional /// user-defined modifications, such as adding a source property. /// /// The root directory to search for input files. /// The type of files to search for, specified as a . /// A boolean indicating whether the source filename should be included as a property in each merged file object. /// The interface to interact with the file system. /// The interface used for logging operations. /// A single containing the combined content of all matched input files. public static async Task MergeFileObjects(string fileObjectDirectoryPath, FileType inputType, bool addSourceProperty, IFileSystem fileSystem, ITaskLogger log) { var finalResults = new ConcurrentBag(); JsonElement? finalResult = null; bool hasError = false; var expectedExtensions = FileHandlerFactory.GetExpectedFileExtensions(inputType); var fileGroups = fileSystem.GetFiles(fileObjectDirectoryPath, "*.*") .Where(file => expectedExtensions.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase)) .ToList() .Chunk(100); await fileGroups.ForEachAsync(Environment.ProcessorCount, async (files) => { JsonElement? intermediateResult = null; foreach (var file in files) { log.LogMessage(MessageImportance.High, "- Found file {0}", file); IFileHandler outputWriter; try { outputWriter = FileHandlerFactory.GetFileHandlerForType(fileSystem, inputType); } catch (ArgumentException ex) { hasError = true; log.LogError(message: "No reader found for file {0}: {1} Stacktrace: {2}", file, ex.Message, ex.StackTrace); continue; } JsonElement fileData; try { fileData = await outputWriter.ReadInput(file).ConfigureAwait(false); } catch (Exception ex) { hasError = true; log.LogError(message: "Could not parse {0}: {1}", file, ex.Message); log.LogErrorFromException(ex, true, true, file); continue; } // Merge the deserialized object into the final result finalResults.Add(await MergeAndUpdateObjects(intermediateResult, fileData, file, addSourceProperty).ConfigureAwait(false)); } }).ConfigureAwait(false); if (hasError) { return null; } foreach (var result in finalResults) { finalResult = await MergeAndUpdateObjects(finalResult, result, null, false).ConfigureAwait(false); } return finalResult; } /// /// Attempts to inject a source property into the given object, if specified. /// /// The object to be modified, which can be null. /// The source string to be injected as a property. /// A boolean indicating whether the source property should be injected, provided the object is not null. /// A modified containing the injected source property, or null if the was null. private static async Task InjectSourceProperty(JsonElement? sourceObject, string source, bool injectSourceProperty) { // If injectSourceProperty is true, inject the source property into the second object if (sourceObject != null && injectSourceProperty && sourceObject.HasValue && sourceObject.Value.ValueKind == JsonValueKind.Object) { var obj2Dict = sourceObject.Value; var jsonObject = obj2Dict.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal); foreach (var kvp in jsonObject.ToList()) { var key = kvp.Key; var value = kvp.Value; if (value.ValueKind == JsonValueKind.Array) { var firstObj2Value = value; var obj2NestedList = firstObj2Value.EnumerateArray().ToList(); for (int index = 0; index < obj2NestedList.Count; index++) { var currentObj2Nested = obj2NestedList[index]; if (currentObj2Nested.ValueKind == JsonValueKind.Object) { var nestedObj = currentObj2Nested; var nestedDict = nestedObj.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal); // Inject the "source" property nestedDict["source"] = JsonDocument.Parse($"\"{Path.GetFileNameWithoutExtension(source)}\"").RootElement; // Update the list at the correct index obj2NestedList[index] = await JsonHelper.ConvertToJsonElement(nestedDict).ConfigureAwait(false); } } jsonObject[key] = await JsonHelper.ConvertToJsonElement(obj2NestedList).ConfigureAwait(false); } } sourceObject = await JsonHelper.ConvertObjectToJsonElement(jsonObject).ConfigureAwait(false); } return sourceObject; } /// /// Merges two objects into a single , with optional /// user-defined modifications such as injecting a source file name property. This method will recursively /// merge nested objects and lists. /// /// The first to merge. /// The second to merge. /// The source identifier for the second object, which can be injected as a property if specified. /// A boolean indicating whether to inject the source identifier into the second object (). /// A merged containing the combined content of both input objects, including nested objects and lists. public static async Task MergeAndUpdateObjects(JsonElement? obj1, JsonElement? obj2, string source2, bool injectSourceProperty) { obj1 = await InjectSourceProperty(obj1, source2, injectSourceProperty).ConfigureAwait(false); obj2 = await InjectSourceProperty(obj2, source2, injectSourceProperty).ConfigureAwait(false); if (obj1 == null) return obj2 ?? default; if (obj2 == null) return obj1.Value; // Handle merging of objects if (obj1.Value.ValueKind == JsonValueKind.Object && obj2.Value.ValueKind == JsonValueKind.Object) { var dict1 = obj1.Value.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal); var dict2 = obj2.Value.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal); foreach (var key in dict2.Keys) { if (dict1.TryGetValue(key, out JsonElement dict1Value)) { dict1[key] = await MergeAndUpdateObjects(dict1Value, dict2[key], source2, injectSourceProperty).ConfigureAwait(false); } else { dict1[key] = dict2[key]; } } return await JsonHelper.ConvertToJsonElement(dict1).ConfigureAwait(false); } // Handle merging of arrays else if (obj1.Value.ValueKind == JsonValueKind.Array && obj2.Value.ValueKind == JsonValueKind.Array) { var list1 = obj1.Value.EnumerateArray().ToList(); list1.AddRange(obj2.Value.EnumerateArray().ToList()); return await JsonHelper.ConvertToJsonElement(list1).ConfigureAwait(false); } // For scalar values, sourceObject overwrites obj1 else { return obj2.Value; } } /// /// Injects additional properties into a JSON object if possible. The additional properties are provided as a dictionary and are added to the top-level JSON object. /// /// The object that is expected to be a JSON object (JsonElement) where additional properties will be injected. /// A dictionary of additional properties to inject. /// Logger reference. /// True if the properties were successfully injected, false otherwise. public static async Task InjectAdditionalProperties(JsonElement? finalResult, Dictionary additionalPropertiesDictionary, ITaskLogger log) { if (additionalPropertiesDictionary?.Count > 0) { if (finalResult is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) { var jsonDictionary = JsonHelper.JsonElementToDictionary(jsonElement); // Add the properties from additionalPropertiesDictionary, converting values to JsonElement foreach (var property in additionalPropertiesDictionary) { jsonDictionary[property.Key] = await JsonHelper.ConvertObjectToJsonElement(property.Value).ConfigureAwait(false); } return await JsonHelper.ConvertToJsonElement(jsonDictionary).ConfigureAwait(false); } log.LogWarning("Additional properties could not be injected since the top-level is not a JSON object."); return finalResult; } return finalResult; } } }