| File: Model\Document.cs | Web Access |
| Project: src\arcade\src\Microsoft.DotNet.XliffTasks\Microsoft.DotNet.XliffTasks.csproj (Microsoft.DotNet.XliffTasks) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; using System.Text; namespace XliffTasks.Model { internal abstract class Document { /// <summary> /// Indicates if content has been loaded in to the document. /// </summary> public abstract bool HasContent { get; } private static readonly Encoding s_utf8WithBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); private static readonly Encoding s_utf8WithoutBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); /// <summary> /// The encoding to use when saving the document to a stream. /// This is the encoding that was detected on Load, or default value of UTF8-with-BOM for new documents. /// </summary> public Encoding Encoding { get; set; } = s_utf8WithBom; /// <summary> /// Loads (or reloads) the document content from the given file path. /// </summary> public void Load(string path) { using FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read); Load(stream); } /// <summary> /// Loads (or reloads) the document content from the given stream. /// </summary> public void Load(Stream stream) { // NOTE: It is important to pass UTF8-without-BOM here and not UTF8-with-BOM (aka Encoding.UTF8 and also same // as default when not passing encoding). The reason is that CurrentEncoding is only different from the encoding // provided when the StreamReader encounters a BOM. As such, if we start off with UTF8-with-BOM, we'll end up with // UTF8-with-BOM even if the document has no BOM, which would defeat our purpose of preserving the encoding and BOM // of the original document in a Load/Modify/Save cycle. using StreamReader reader = new(stream, s_utf8WithoutBom, detectEncodingFromByteOrderMarks: true); Load(reader); Encoding = reader.CurrentEncoding; } /// <summary> /// Loads (or reloads) the document content from the given reader. /// </summary> public abstract void Load(TextReader reader); /// <summary> /// Saves the document's content to the given file path. /// </summary> public void Save(string path) { //On Windows: // Readers will prevent the file from being overwritten due to FileShare.Read. // Readers can read in parallel, but when there's contention with a writer, the retrying will kick in to resolve it. //On Unix: // FileShare.Read does nothing, but... // File.Replace is implemented with rename system call that will mean that even though writers can overwrite while // reading is happening, each reader will see file before or after overwrite, not in between EnsureContent(); string tempPath = Path.Combine(Path.GetDirectoryName(path), Path.GetRandomFileName()); using (FileStream stream = File.Open(tempPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) { Save(stream); } ExponentialRetry.ExecuteWithRetryOnIOException(() => { if (File.Exists(path)) { File.Replace( sourceFileName: tempPath, destinationFileName: path, destinationBackupFileName: null, ignoreMetadataErrors: true); } else { File.Move(sourceFileName: tempPath, destFileName: path); } }, maxRetryCount: 3); } /// <summary> /// Saves the document's content to the given stream. /// </summary> public void Save(Stream stream) { EnsureContent(); using StreamWriter writer = new(stream, Encoding); Save(writer); } /// <summary> /// Saves the document's content to the given writer. /// </summary> public abstract void Save(TextWriter writer); /// <summary> /// Throws if this document has no content. /// </summary> /// <exception cref="InvalidOperationException"><see cref="HasContent"/> is false.</exception> protected void EnsureContent() { if (!HasContent) { throw new InvalidOperationException("Document has no content loaded."); } } } }