namespace QWERTYkez.WordProcessor; /// /// Предоставляет потокобезопасный процессор только для чтения документов Word (DOCX) формата. /// Не поддерживает операции изменения документа. /// 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 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 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(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 write, Action 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 /// /// Получает путь к исходному файлу. /// public string? FilePath { get; protected set; } /// /// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций. /// public bool IsValid => !_disposed && _doc is not null && _body is not null && _ms is not null; /// /// Получает текущий размер документа в байтах. /// public long DocumentSize => _ms.Length; #endregion #region Read Operations /// /// Находит все уникальные плейсхолдеры в формате $...$ в документе. /// Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов. /// /// Способ сравнения строк при поиске (по умолчанию: без учета регистра) /// Коллекция уникальных найденных плейсхолдеров public ISet 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 }