HashCalculator.GUI/ModXsd.cs
2022-01-25 19:05:37 +01:00

165 lines
5.4 KiB
C#

using Mvp.Xml.XInclude;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Xml.Linq;
namespace HashCalculator.GUI
{
public class ModXsd : IModXml
{
protected enum DocumentOption
{
Normal,
IsEntryPoint
}
public static readonly IEnumerable<string> NotSupportedAttributes = new[]
{
"fragid", "set-xml-id", "encoding", "accept", "accept-language"
};
public static XNamespace Schema = "http://www.w3.org/2001/XMLSchema";
public int TotalFilesProcessed => _processed.Count;
public int TotalAssets => _assets.Count;
private readonly ConcurrentDictionary<string, byte> _processed = new();
private readonly ConcurrentDictionary<AssetEntry, string> _assets = new();
private readonly BufferBlock<AssetEntry> _entries = new();
private readonly string _xmlFullPath;
private readonly CancellationToken _token;
public ModXsd(string xmlPath, CancellationToken token)
{
_xmlFullPath = Path.GetFullPath(xmlPath);
_token = token;
}
public async IAsyncEnumerable<AssetEntry> ProcessDocument()
{
if (_processed.Any() || _assets.Any())
{
throw new InvalidOperationException();
}
var task = Task.Run(async () =>
{
try
{
await ProcessDocumentInternal(_xmlFullPath);
}
catch
{
if (!_token.IsCancellationRequested)
{
throw;
}
}
finally
{
_entries.Complete();
}
});
while (await _entries.OutputAvailableAsync())
{
yield return _entries.Receive();
}
await task;
}
private async Task ProcessDocumentInternal(string fullPath)
{
_token.ThrowIfCancellationRequested();
if (!_processed.TryAdd(fullPath.ToUpperInvariant(), default))
{
return;
}
var document = await GetFileAsync(fullPath);
var rootName = document.Root?.Name
?? throw new InvalidDataException("Document doesn't have a root");
if (rootName != Schema + "schema")
{
throw new NotSupportedException();
}
var includes = from include in document.Root.Elements(Schema + "include")
let includePath = GetIncludePath(include, fullPath)
where includePath != null
select Task.Run(() => ProcessDocumentInternal(includePath), _token);
var includesTasks = includes.ToArray();
var items = from element in document.Root.Elements()
let name = element.Name
where name == Schema + "simpleType" || name == Schema + "complexType"
let entry = AssetEntry.TryParseXsd(element)
where entry != null
select entry;
foreach (var item in items)
{
if (_assets.TryAdd(item, fullPath))
{
_entries.Post(item);
}
else
{
var previousPath = _assets[item];
TracerListener.WriteLine($"[ModXsd] `{fullPath}`: Attempted to add item `{item.Type}:{item.Name}` multiple times - already processed in `{previousPath}`");
}
}
await Task.WhenAll(includesTasks);
}
private string? GetIncludePath(XElement include, string includerPath)
{
if (Path.GetDirectoryName(includerPath) is not { } currentDirectory)
{
throw new InvalidOperationException();
}
if (include.Name != Schema + "include")
{
throw new InvalidDataException();
}
var source = include.Attribute("schemaLocation")?.Value;
if (source is null)
{
throw new InvalidDataException();
}
var includedSource = Path.GetFullPath(source, currentDirectory);
if (!File.Exists(includedSource))
{
TracerListener.WriteLine($"[ModXsd]: Warning, include path does not exist! It is: {includedSource}");
return null;
}
return includedSource;
}
private async Task<XDocument> GetFileAsync(string normalizedPath)
{
try
{
var xml = await File.ReadAllBytesAsync(normalizedPath, _token);
using var xmlStream = new MemoryStream(xml);
using var reader = new XIncludingReader(normalizedPath, xmlStream);
reader.MoveToContent();
return XDocument.Load(reader);
}
catch (Exception e)
{
throw new InvalidDataException($"Failed to process XML file: {normalizedPath} - {e.Message}", e);
}
}
}
}