| File: FileModifiers\XmlTransformer.cs | Web Access |
| Project: src\nuget-client\src\NuGet.Core\NuGet.PackageManagement\NuGet.PackageManagement.csproj (NuGet.PackageManagement) |
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #nullable disable using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using NuGet.Common; namespace NuGet.ProjectManagement { /// <summary> /// An XML project file transformer. /// </summary> public class XmlTransformer : IPackageFileTransformer { private readonly IDictionary<XName, Action<XElement, XElement>> _nodeActions; /// <summary> /// Initializes a new <see cref="XmlTransformer" /> class. /// </summary> /// <param name="nodeActions">A dictionary of XML node names to node actions.</param> /// <exception cref="ArgumentNullException">Thrown if <paramref name="nodeActions" /> /// is <see langword="null" />.</exception> public XmlTransformer(IDictionary<XName, Action<XElement, XElement>> nodeActions) { if (nodeActions == null) { throw new ArgumentNullException(nameof(nodeActions)); } _nodeActions = nodeActions; } /// <summary> /// Asynchronously transforms a file. /// </summary> /// <param name="streamTaskFactory">A stream task factory.</param> /// <param name="targetPath">A path to the file to be transformed.</param> /// <param name="projectSystem">The project where this change is taking place.</param> /// <param name="cancellationToken">A cancellation token.</param> /// <returns>A task that represents the asynchronous operation.</returns> /// <exception cref="ArgumentNullException">Thrown if <paramref name="streamTaskFactory" /> /// is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="projectSystem" /> /// is <see langword="null" />.</exception> /// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" /> /// is cancelled.</exception> public async Task TransformFileAsync( Func<Task<Stream>> streamTaskFactory, string targetPath, IMSBuildProjectSystem projectSystem, CancellationToken cancellationToken) { if (streamTaskFactory == null) { throw new ArgumentNullException(nameof(streamTaskFactory)); } if (projectSystem == null) { throw new ArgumentNullException(nameof(projectSystem)); } cancellationToken.ThrowIfCancellationRequested(); // Get the xml fragment var xmlFragment = await GetXmlAsync(streamTaskFactory, projectSystem, cancellationToken); var transformDocument = MSBuildNuGetProjectSystemUtility.GetOrCreateDocument(xmlFragment.Name, targetPath, projectSystem); // Do a merge transformDocument.Root.MergeWith(xmlFragment, _nodeActions); MSBuildNuGetProjectSystemUtility.AddFile(projectSystem, targetPath, transformDocument.Save); } /// <summary> /// Asynchronously reverses the transform on the targetPath, using all the potential source of change. /// </summary> /// <param name="streamTaskFactory">A factory for accessing the file to be reverted from the nupkg being uninstalled.</param> /// <param name="targetPath">A path to the file to be reverted.</param> /// <param name="matchingFiles">Other files in other packages that may have changed the <paramref name="targetPath" />.</param> /// <param name="projectSystem">The project where this change is taking place.</param> /// <param name="cancellationToken">A cancellation token.</param> /// <returns>A task that represents the asynchronous operation.</returns> /// <exception cref="ArgumentNullException">Thrown if <paramref name="streamTaskFactory" /> /// is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException">Thrown if <paramref name="projectSystem" /> /// is <see langword="null" />.</exception> /// <exception cref="OperationCanceledException">Thrown if <paramref name="cancellationToken" /> /// is cancelled.</exception> public async Task RevertFileAsync( Func<Task<Stream>> streamTaskFactory, string targetPath, IEnumerable<InternalZipFileInfo> matchingFiles, IMSBuildProjectSystem projectSystem, CancellationToken cancellationToken) { if (streamTaskFactory == null) { throw new ArgumentNullException(nameof(streamTaskFactory)); } if (projectSystem == null) { throw new ArgumentNullException(nameof(projectSystem)); } cancellationToken.ThrowIfCancellationRequested(); // Get the xml snippet var xmlFragment = await GetXmlAsync(streamTaskFactory, projectSystem, cancellationToken); var document = XmlUtility.GetOrCreateDocument(xmlFragment.Name, projectSystem.ProjectFullPath, targetPath, projectSystem.NuGetProjectContext); // Merge the other xml elements into one element within this xml hierarchy (matching the config file path) var elements = new List<XElement>(); foreach (var matchingFile in matchingFiles) { elements.Add(await GetXmlAsync(matchingFile, projectSystem, cancellationToken)); } var mergedFragments = elements.Aggregate( new XElement(xmlFragment.Name), (left, right) => left.MergeWith(right, _nodeActions)); // Take the difference of the xml and remove it from the main xml file document.Root.Except(xmlFragment.Except(mergedFragments)); // Save the new content to the file system using (var fileStream = FileSystemUtility.CreateFile( projectSystem.ProjectFullPath, targetPath, projectSystem.NuGetProjectContext)) { document.Save(fileStream); } } private static async Task<XElement> GetXmlAsync( InternalZipFileInfo packageFileInfo, IMSBuildProjectSystem projectSystem, CancellationToken cancellationToken) { string content; using var packageStream = File.OpenRead(packageFileInfo.ZipArchivePath); using var zipArchive = new ZipArchive(packageStream); var zipArchivePackageEntry = PathUtility.GetEntry(zipArchive, packageFileInfo.ZipArchiveEntryFullName); if (zipArchivePackageEntry == null) { throw new ArgumentException("internalZipFileInfo"); } content = await Preprocessor.ProcessAsync( () => Task.FromResult(zipArchivePackageEntry.Open()), projectSystem, cancellationToken); return XElement.Parse(content, LoadOptions.PreserveWhitespace); } private static async Task<XElement> GetXmlAsync( Func<Task<Stream>> streamTaskFactory, IMSBuildProjectSystem projectSystem, CancellationToken cancellationToken) { var content = await Preprocessor.ProcessAsync(streamTaskFactory, projectSystem, cancellationToken); return XElement.Parse(content, LoadOptions.PreserveWhitespace); } } }