193 lines
6.5 KiB
C#
193 lines
6.5 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Runtime.Serialization;
|
|
using System.Runtime.Serialization.Json;
|
|
using System.Text;
|
|
|
|
namespace BizTalkPlatformManagementTool.Services
|
|
{
|
|
/// <summary>
|
|
/// Persists DataContract models as JSON with repository-defined encoding rules.
|
|
/// </summary>
|
|
public static class JsonFileStore
|
|
{
|
|
/// <summary>
|
|
/// UTF-8 encoding instance that writes JSON without a byte order mark.
|
|
/// </summary>
|
|
private static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
|
|
|
|
/// <summary>
|
|
/// Serializes a value to a UTF-8 JSON file without a byte order mark.
|
|
/// </summary>
|
|
/// <typeparam name="T">The model type to serialize.</typeparam>
|
|
/// <param name="path">The target JSON file path.</param>
|
|
/// <param name="value">The value to serialize.</param>
|
|
public static void Save<T>(string path, T value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
{
|
|
throw new ArgumentException("A JSON target path is required.", "path");
|
|
}
|
|
|
|
var fullPath = Path.GetFullPath(path);
|
|
var directory = Path.GetDirectoryName(fullPath);
|
|
Directory.CreateDirectory(directory);
|
|
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
|
{
|
|
UseSimpleDictionaryFormat = true
|
|
});
|
|
|
|
using (var stream = new MemoryStream())
|
|
{
|
|
serializer.WriteObject(stream, value);
|
|
var json = Utf8NoBom.GetString(stream.ToArray());
|
|
WriteAtomically(fullPath, json);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a JSON file into the requested DataContract model type.
|
|
/// </summary>
|
|
/// <typeparam name="T">The model type to deserialize.</typeparam>
|
|
/// <param name="path">The JSON file path to load.</param>
|
|
/// <returns>The deserialized model.</returns>
|
|
public static T Load<T>(string path)
|
|
{
|
|
var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings
|
|
{
|
|
UseSimpleDictionaryFormat = true
|
|
});
|
|
|
|
var json = NormalizeJson(File.ReadAllText(path, Encoding.UTF8));
|
|
using (var stream = new MemoryStream(Utf8NoBom.GetBytes(json)))
|
|
{
|
|
try
|
|
{
|
|
return (T)serializer.ReadObject(stream);
|
|
}
|
|
catch (SerializationException ex)
|
|
{
|
|
throw new InvalidDataException("Failed to load JSON file '" + path + "'. " + DescribeJsonStart(json), ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes byte order mark variants that may exist in previously written files.
|
|
/// </summary>
|
|
/// <param name="json">The raw JSON text read from disk.</param>
|
|
/// <returns>The JSON text without a leading BOM marker.</returns>
|
|
private static string NormalizeJson(string json)
|
|
{
|
|
if (string.IsNullOrEmpty(json))
|
|
{
|
|
return json;
|
|
}
|
|
|
|
if (json[0] == '\uFEFF')
|
|
{
|
|
json = json.Substring(1);
|
|
}
|
|
|
|
if (json.StartsWith("\u00EF\u00BB\u00BF"))
|
|
{
|
|
json = json.Substring(3);
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds a short diagnostic message for a JSON deserialization failure.
|
|
/// </summary>
|
|
/// <param name="json">The normalized JSON text that failed to deserialize.</param>
|
|
/// <returns>A diagnostic suffix describing the beginning of the JSON content.</returns>
|
|
private static string DescribeJsonStart(string json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
return "The file is empty or contains only whitespace.";
|
|
}
|
|
|
|
var trimmed = json.TrimStart();
|
|
var first = trimmed.Length == 0 ? '\0' : trimmed[0];
|
|
if (first != '{' && first != '[')
|
|
{
|
|
return "The first JSON character is '" + first + "' instead of '{' or '['.";
|
|
}
|
|
|
|
return "The file starts with valid JSON syntax but could not be deserialized into the expected model.";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a file through a same-directory temporary file so an interrupted
|
|
/// save cannot leave a truncated snapshot or operation plan behind.
|
|
/// </summary>
|
|
private static void WriteAtomically(string path, string content)
|
|
{
|
|
var temporaryPath = path + ".tmp." + Guid.NewGuid().ToString("N");
|
|
var backupPath = path + ".bak." + Guid.NewGuid().ToString("N");
|
|
try
|
|
{
|
|
File.WriteAllText(temporaryPath, content, Utf8NoBom);
|
|
if (!File.Exists(path))
|
|
{
|
|
File.Move(temporaryPath, path);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
File.Replace(temporaryPath, path, backupPath, true);
|
|
TryDelete(backupPath);
|
|
}
|
|
catch (PlatformNotSupportedException)
|
|
{
|
|
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
ReplaceWithRenameFallback(path, temporaryPath, backupPath);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
TryDelete(temporaryPath);
|
|
}
|
|
}
|
|
|
|
private static void ReplaceWithRenameFallback(string path, string temporaryPath, string backupPath)
|
|
{
|
|
File.Move(path, backupPath);
|
|
try
|
|
{
|
|
File.Move(temporaryPath, path);
|
|
TryDelete(backupPath);
|
|
}
|
|
catch
|
|
{
|
|
if (!File.Exists(path) && File.Exists(backupPath))
|
|
{
|
|
File.Move(backupPath, path);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private static void TryDelete(string path)
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Temporary cleanup is best-effort and must not hide the save result.
|
|
}
|
|
}
|
|
}
|
|
}
|