Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.WordProcessor/WordReader.cs

285 lines
8.0 KiB
C#
Raw Normal View History

namespace QWERTYkez.WordProcessor;
/// <summary>
/// Предоставляет потокобезопасный процессор только для чтения документов Word (DOCX) формата.
/// <para>Не поддерживает операции изменения документа.</para>
/// </summary>
internal class WordReader : IDisposable, IWordReader
{
protected MemoryStream _ms = null!;
protected WordprocessingDocument _doc = null!;
protected Body _body = null!;
protected bool _disposed;
protected readonly object _syncLock = new();
protected string? _originalSourcePath;
public Body Body => _body;
internal WordReader() { }
#region Factory Methods
internal static WordReader? CreateInternal(FileInfo sourceFile)
{
if (sourceFile is null || !sourceFile.Exists)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
#endif
return null;
}
MemoryStream? ms = null;
WordprocessingDocument? doc = null;
try
{
ms = new MemoryStream();
using (var file = new FileStream(sourceFile.FullName,
FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
file.CopyTo(ms);
}
ms.Position = 0;
doc = WordprocessingDocument.Open(ms, isEditable: false,
new OpenSettings { AutoSave = false });
if (doc.MainDocumentPart?.Document?.Body is not { } body)
{
#if DEBUG
Debug.WriteLine("[DEBUG] Document body is null or empty");
#endif
doc.Dispose();
ms.Dispose();
return null;
}
var processor = new WordReader
{
_ms = ms,
_doc = doc,
_body = body,
_originalSourcePath = sourceFile.FullName,
FilePath = sourceFile.FullName
};
return processor;
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Error creating read-only processor: {ex.GetType().Name}: {ex.Message}");
#endif
doc?.Dispose();
ms?.Dispose();
return null;
}
}
internal static WordReader? CreateFromData(ReadOnlyMemory<byte> data)
{
if (data.IsEmpty)
return null;
var ms = new MemoryStream();
try
{
ms.Write(data.ToArray(), 0, data.Length);
ms.Position = 0;
var doc = WordprocessingDocument.Open(ms, false, new OpenSettings { AutoSave = false });
if (doc.MainDocumentPart?.Document?.Body is not { } body)
{
doc.Dispose();
ms.Dispose();
return null;
}
return new WordReader
{
_ms = ms,
_doc = doc,
_body = body,
FilePath = null // из памяти нет файла
};
}
catch
{
ms?.Dispose();
return null;
}
}
#endregion
#region Writers
public bool TryWrite(string destinationPath, Action<IWordWriter> action)
{
ThrowIfDisposed();
if (string.IsNullOrEmpty(destinationPath))
throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
if (action is null)
throw new ArgumentNullException(nameof(action));
// Копируем данные из текущего потока
byte[] data;
lock (_syncLock)
{
_ms.Position = 0;
data = _ms.ToArray();
}
using var writable = WordWriter.CreateFromData(new ReadOnlyMemory<byte>(data), destinationPath);
if (writable is null)
return false;
try
{
action(writable);
return true;
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Error in TryWrite action: {ex.Message}");
#endif
return false;
}
}
public bool TryWrite(Action<IWordWriter> write, Action<IWordReader> read)
{
ThrowIfDisposed();
if (write is null) throw new ArgumentNullException(nameof(write));
if (read is null) throw new ArgumentNullException(nameof(read));
// Копируем текущие данные
byte[] data;
lock (_syncLock)
{
_ms.Position = 0;
data = _ms.ToArray();
}
WordWriter? writable = null;
WordReader? resultReader = null;
try
{
// Создаём редактируемую копию (без привязки к файлу)
writable = WordWriter.CreateFromData(data);
if (writable is null)
return false;
// Применяем изменения
write(writable);
// Сохраняем изменения в поток и создаём read-only процессор
resultReader = writable.ToReader();
if (resultReader is null)
return false;
// Работаем с изменённой копией
read(resultReader);
return true;
}
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Error in TryWrite(write, read): {ex.Message}");
#endif
return false;
}
finally
{
// Гарантированно освобождаем созданные процессоры
resultReader?.Dispose();
writable?.Dispose();
}
}
#endregion
#region Properties
/// <summary>
/// Получает путь к исходному файлу.
/// </summary>
public string? FilePath { get; protected set; }
/// <summary>
/// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
/// </summary>
public bool IsValid => !_disposed && _doc is not null && _body is not null && _ms is not null;
/// <summary>
/// Получает текущий размер документа в байтах.
/// </summary>
public long DocumentSize => _ms.Length;
#endregion
#region Read Operations
/// <summary>
/// Находит все уникальные плейсхолдеры в формате $...$ в документе.
/// <para>Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.</para>
/// </summary>
/// <param name="comparisonType">Способ сравнения строк при поиске (по умолчанию: без учета регистра)</param>
/// <returns>Коллекция уникальных найденных плейсхолдеров</returns>
public ISet<string> FindPlaceholders()
{
ThrowIfDisposed();
lock (_syncLock)
{
return PlaceholderFinder.FindInDocument(_doc, _body);
}
}
#endregion
#region Dispose Pattern
protected void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(GetType().Name);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
lock (_syncLock)
{
if (disposing)
{
_doc.Dispose();
_ms.Dispose();
}
_doc = null!;
_body = null!;
_ms = null!;
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
~WordReader()
{
Dispose(disposing: false);
}
#endregion
}