diff --git a/src/LogExpert.Configuration/ConfigManager.cs b/src/LogExpert.Configuration/ConfigManager.cs index e1437a7b..5b8e716a 100644 --- a/src/LogExpert.Configuration/ConfigManager.cs +++ b/src/LogExpert.Configuration/ConfigManager.cs @@ -46,6 +46,7 @@ public class ConfigManager : IConfigManager }; private const string SETTINGS_FILE_NAME = "settings.json"; + private const int MAX_FILE_HISTORY = 10; #endregion @@ -251,6 +252,43 @@ public void ImportHighlightSettings (FileInfo fileInfo, ExportImportFlags import Save(SettingsFlags.All); } + /// + /// Adds the specified file name to the file history list, moving it to the top if it already exists. + /// + /// If the file name already exists in the history, it is moved to the top of the list. The file + /// history list is limited to a maximum number of entries; the oldest entries are removed if the limit is exceeded. + /// This method is supported only on Windows platforms. + /// The name of the file to add to the file history list. Comparison is case-insensitive. + [SupportedOSPlatform("windows")] + public void AddToFileHistory (string fileName) + { + bool findName (string s) => s.ToUpperInvariant().Equals(fileName.ToUpperInvariant(), StringComparison.Ordinal); + + var index = Instance.Settings.FileHistoryList.FindIndex(findName); + + if (index != -1) + { + Instance.Settings.FileHistoryList.RemoveAt(index); + } + + Instance.Settings.FileHistoryList.Insert(0, fileName); + + while (Instance.Settings.FileHistoryList.Count > MAX_FILE_HISTORY) + { + Instance.Settings.FileHistoryList.RemoveAt(Instance.Settings.FileHistoryList.Count - 1); + } + + Save(SettingsFlags.FileHistory); + } + + public void ClearLastOpenFilesList () + { + lock (_loadSaveLock) + { + Instance.Settings.LastOpenFilesList.Clear(); + } + } + #endregion #region Private Methods @@ -807,7 +845,8 @@ IOException or NotSupportedException or PathTooLongException or UnauthorizedAccessException or - SecurityException) + SecurityException or + JsonSerializationException) { _logger.Error($"Error while deserializing config data: {e}"); newGroups = []; @@ -1060,6 +1099,7 @@ private bool ValidateSettings (Settings settings) return true; } + #endregion /// diff --git a/src/LogExpert.Core/Classes/Persister/PersisterHelpers.cs b/src/LogExpert.Core/Classes/Persister/PersisterHelpers.cs new file mode 100644 index 00000000..9e7b9942 --- /dev/null +++ b/src/LogExpert.Core/Classes/Persister/PersisterHelpers.cs @@ -0,0 +1,71 @@ +using System.Collections.ObjectModel; + +using LogExpert.Core.Interface; + +namespace LogExpert.Core.Classes.Persister; + +public static class PersisterHelpers +{ + + private const string LOCAL_FILE_SYSTEM_NAME = "LocalFileSystem"; + + /// + /// Checks if the file name is a settings file (.lxp). If so, the contained logfile name + /// is returned. If not, the given file name is returned unchanged. + /// + /// The file name to resolve + /// Plugin registry for file system resolution (optional) + /// The resolved log file path + public static string FindFilenameForSettings (string fileName, IPluginRegistry pluginRegistry) + { + ArgumentException.ThrowIfNullOrWhiteSpace(fileName, nameof(fileName)); + + if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase)) + { + var persistenceData = Persister.Load(fileName); + if (persistenceData == null) + { + return fileName; + } + + if (!string.IsNullOrEmpty(persistenceData.FileName)) + { + if (pluginRegistry != null) + { + var fs = pluginRegistry.FindFileSystemForUri(persistenceData.FileName); + // Use file system plugin for non-local files (network, SFTP, etc.) + if (fs != null && fs.GetType().Name != LOCAL_FILE_SYSTEM_NAME) + { + return persistenceData.FileName; + } + } + + // Handle rooted paths (absolute paths) + if (Path.IsPathRooted(persistenceData.FileName)) + { + return persistenceData.FileName; + } + + // Handle relative paths in .lxp files + var dir = Path.GetDirectoryName(fileName); + return Path.Join(dir, persistenceData.FileName); + } + } + + return fileName; + } + + public static ReadOnlyCollection FindFilenameForSettings (ReadOnlyCollection fileNames, IPluginRegistry pluginRegistry) + { + ArgumentNullException.ThrowIfNull(fileNames); + + var foundFiles = new List(fileNames.Count); + + foreach (var fileName in fileNames) + { + foundFiles.Add(FindFilenameForSettings(fileName, pluginRegistry)); + } + + return foundFiles.AsReadOnly(); + } +} diff --git a/src/LogExpert.Core/Classes/Persister/PersisterXML.cs b/src/LogExpert.Core/Classes/Persister/PersisterXML.cs index 1a015dbc..c570cd5b 100644 --- a/src/LogExpert.Core/Classes/Persister/PersisterXML.cs +++ b/src/LogExpert.Core/Classes/Persister/PersisterXML.cs @@ -601,7 +601,8 @@ public static PersistenceData Load (string fileName) } catch (Exception xmlParsingException) when (xmlParsingException is XmlException or UnauthorizedAccessException or - IOException) + IOException or + FileNotFoundException) { _logger.Error(xmlParsingException, $"Error loading persistence data from {fileName}, unknown format, parsing xml or json was not possible"); return null; diff --git a/src/LogExpert.Core/Classes/Persister/ProjectData.cs b/src/LogExpert.Core/Classes/Persister/ProjectData.cs index 47f64a7d..5a5abd3d 100644 --- a/src/LogExpert.Core/Classes/Persister/ProjectData.cs +++ b/src/LogExpert.Core/Classes/Persister/ProjectData.cs @@ -15,5 +15,10 @@ public class ProjectData /// public string TabLayoutXml { get; set; } + /// + /// Gets or sets the full file path to the project file. + /// + public string ProjectFilePath { get; set; } + #endregion } \ No newline at end of file diff --git a/src/LogExpert.Core/Classes/Persister/ProjectFileResolver.cs b/src/LogExpert.Core/Classes/Persister/ProjectFileResolver.cs new file mode 100644 index 00000000..d089cfe8 --- /dev/null +++ b/src/LogExpert.Core/Classes/Persister/ProjectFileResolver.cs @@ -0,0 +1,34 @@ +using System.Collections.ObjectModel; + +using LogExpert.Core.Interface; + +namespace LogExpert.Core.Classes.Persister; + +/// +/// Helper class to resolve project file references to actual log files. +/// Handles .lxp (persistence) files by extracting the actual log file path. +/// +public static class ProjectFileResolver +{ + /// + /// Resolves project file names to actual log files. + /// If a file is a .lxp persistence file, extracts the log file path from it. + /// + /// The project data containing file references + /// Plugin registry for file system resolution (optional) + /// List of tuples containing (logFilePath, originalFilePath) + public static ReadOnlyCollection<(string LogFile, string OriginalFile)> ResolveProjectFiles (ProjectData projectData, IPluginRegistry pluginRegistry = null) + { + ArgumentNullException.ThrowIfNull(projectData); + + var resolved = new List<(string LogFile, string OriginalFile)>(); + + foreach (var fileName in projectData.FileNames) + { + var logFile = PersisterHelpers.FindFilenameForSettings(fileName, pluginRegistry); + resolved.Add((logFile, fileName)); + } + + return resolved.AsReadOnly(); + } +} \ No newline at end of file diff --git a/src/LogExpert.Core/Classes/Persister/ProjectFileValidator.cs b/src/LogExpert.Core/Classes/Persister/ProjectFileValidator.cs new file mode 100644 index 00000000..aa560354 --- /dev/null +++ b/src/LogExpert.Core/Classes/Persister/ProjectFileValidator.cs @@ -0,0 +1,251 @@ +using LogExpert.Core.Interface; + +namespace LogExpert.Core.Classes.Persister; + +/// +/// Provides static methods for validating project file references, identifying missing or accessible files, and +/// suggesting alternative file paths using available file system plugins. +/// +/// This class is intended for use with project data that includes file references and a project file +/// path. It supports validation of both local file paths and URI-based files through plugin resolution. All methods are +/// thread-safe and do not modify input data. Use the provided methods to check file existence, resolve canonical file +/// paths, and locate possible alternatives for missing files. +public static class ProjectFileValidator +{ + /// + /// Validates the files referenced by the specified project and identifies missing or accessible files using + /// available file system plugins. + /// + /// Files are considered valid if they exist on disk or if a suitable file system plugin is + /// available for URI-based files. For missing files, possible alternative paths are suggested based on the project + /// file location. + /// The project data containing the list of file names to validate and the project file path. Cannot be null. + /// The plugin registry used to resolve file system plugins for URI-based files. Cannot be null. + /// A ProjectValidationResult containing lists of valid files, missing files, and possible alternative file paths + /// for missing files. + public static ProjectValidationResult ValidateProject (ProjectData projectData, IPluginRegistry pluginRegistry) + { + ArgumentNullException.ThrowIfNull(projectData); + ArgumentNullException.ThrowIfNull(pluginRegistry); + + var result = new ProjectValidationResult(); + + foreach (var fileName in projectData.FileNames) + { + var normalizedPath = NormalizeFilePath(fileName); + + if (File.Exists(normalizedPath)) + { + result.ValidFiles.Add(fileName); + } + else if (IsUri(fileName)) + { + // Check if URI-based file system plugin is available + var fs = pluginRegistry.FindFileSystemForUri(fileName); + if (fs != null) + { + result.ValidFiles.Add(fileName); + } + else + { + result.MissingFiles.Add(fileName); + } + } + else + { + result.MissingFiles.Add(fileName); + + var alternativePaths = FindAlternativePaths(fileName, projectData.ProjectFilePath); + result.PossibleAlternatives[fileName] = alternativePaths; + } + } + + return result; + } + + /// + /// Normalizes the specified file path by resolving the actual file name if the file is a persistence file. + /// + /// Use this method to obtain the canonical file path for files that may be persisted under a + /// different name. For files that do not have a ".lxp" extension, the input path is returned unchanged. + /// The path of the file to normalize. If the file has a ".lxp" extension, its persisted file name will be resolved; + /// otherwise, the original path is returned. + /// The normalized file path. If the input is a persistence file, returns the resolved file name; otherwise, returns + /// the original file path. + private static string NormalizeFilePath (string fileName) + { + if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase)) + { + var persistenceData = Persister.Load(fileName); + return persistenceData?.FileName ?? fileName; + } + + return fileName; + } + + /// + /// Determines whether the specified string represents an absolute URI with a scheme other than "file". + /// + /// This method returns false for local file paths and URIs with the "file" scheme, treating them + /// as regular files rather than remote resources. Common URI schemes include "http", "https", "ftp", and + /// "sftp". + /// The string to evaluate as a potential URI. Cannot be null, empty, or consist only of white-space characters. + /// true if the string is a valid absolute URI with a non-file scheme; otherwise, false. + private static bool IsUri (string fileName) + { + return !string.IsNullOrWhiteSpace(fileName) && + Uri.TryCreate(fileName, UriKind.Absolute, out var uri) && + !string.IsNullOrEmpty(uri.Scheme) && + !uri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Searches for alternative file paths that may correspond to the specified file name, considering common locations + /// such as the project directory, its subdirectories, the user's Documents/LogExpert folder, alternate drive + /// letters, and relative paths from the project directory. + /// + /// This method attempts to locate files that may have been moved, renamed, or exist in typical + /// user or project directories. It ignores errors encountered during directory or file access and does not + /// guarantee that all possible alternative locations are checked. Duplicate paths are excluded from the + /// result. + /// The name or path of the file to search for. Can be an absolute or relative path. Cannot be null, empty, or + /// whitespace. + /// The full path to the project file used as a reference for searching related directories. Can be null or empty if + /// project context is not available. + /// A list of strings containing the full paths of files found that match the specified file name in alternative + /// locations. The list will be empty if no matching files are found. + private static List FindAlternativePaths (string fileName, string projectFilePath) + { + var alternatives = new List(); + + if (string.IsNullOrWhiteSpace(fileName)) + { + return alternatives; + } + + var baseName = Path.GetFileName(fileName); + + if (string.IsNullOrWhiteSpace(baseName)) + { + return alternatives; + } + + // Search in directory of .lxj project file + if (!string.IsNullOrWhiteSpace(projectFilePath)) + { + try + { + var projectDir = Path.GetDirectoryName(projectFilePath); + if (!string.IsNullOrEmpty(projectDir) && Directory.Exists(projectDir)) + { + var candidatePath = Path.Join(projectDir, baseName); + if (File.Exists(candidatePath)) + { + alternatives.Add(candidatePath); + } + + // Also check subdirectories (one level deep) + var subdirs = Directory.GetDirectories(projectDir); + alternatives.AddRange( + subdirs + .Select(subdir => Path.Join(subdir, baseName)) + .Where(File.Exists)); + } + } + catch (Exception ex) when (ex is ArgumentException or + ArgumentNullException or + PathTooLongException or + UnauthorizedAccessException or + IOException) + { + // Ignore errors when searching in project directory + } + } + + // Search in Documents/LogExpert folder + try + { + var documentsPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LogExpert"); + + if (Directory.Exists(documentsPath)) + { + var docCandidate = Path.Join(documentsPath, baseName); + if (File.Exists(docCandidate) && !alternatives.Contains(docCandidate)) + { + alternatives.Add(docCandidate); + } + } + } + catch (Exception ex) when (ex is ArgumentException or + ArgumentNullException or + PathTooLongException or + UnauthorizedAccessException or + IOException) + { + // Ignore errors when searching in Documents folder + } + + // If the original path is absolute, try to find the file in the same directory structure + // but on a different drive (useful when drive letters change) + if (Path.IsPathRooted(fileName)) + { + try + { + var driveLetters = DriveInfo.GetDrives() + .Where(d => d.IsReady && d.DriveType == DriveType.Fixed) + .Select(d => d.Name[0]) + .ToList(); + + var originalDrive = Path.GetPathRoot(fileName)?[0]; + var pathWithoutDrive = fileName.Length > 3 ? fileName[3..] : string.Empty; + + foreach (var drive in driveLetters.Where(drive => drive != originalDrive && !string.IsNullOrEmpty(pathWithoutDrive))) + { + var alternatePath = $"{drive}:\\{pathWithoutDrive}"; + if (File.Exists(alternatePath) && !alternatives.Contains(alternatePath)) + { + alternatives.Add(alternatePath); + } + } + } + catch (Exception ex) when (ex is ArgumentException or + ArgumentNullException or + PathTooLongException or + UnauthorizedAccessException or + IOException) + { + // Ignore errors when searching on different drives + } + } + + // Try relative path resolution from project directory + if (!Path.IsPathRooted(fileName) && !string.IsNullOrWhiteSpace(projectFilePath)) + { + try + { + var projectDir = Path.GetDirectoryName(projectFilePath); + if (!string.IsNullOrEmpty(projectDir)) + { + var relativePath = Path.Join(projectDir, fileName); + var normalizedPath = Path.GetFullPath(relativePath); + + if (File.Exists(normalizedPath) && !alternatives.Contains(normalizedPath)) + { + alternatives.Add(normalizedPath); + } + } + } + catch (Exception ex) when (ex is ArgumentException or + ArgumentNullException or + PathTooLongException or + UnauthorizedAccessException or + IOException or + NotSupportedException) + { + // Ignore errors with relative path resolution + } + } + + return alternatives; + } +} \ No newline at end of file diff --git a/src/LogExpert.Core/Classes/Persister/ProjectLoadResult.cs b/src/LogExpert.Core/Classes/Persister/ProjectLoadResult.cs new file mode 100644 index 00000000..83b15ceb --- /dev/null +++ b/src/LogExpert.Core/Classes/Persister/ProjectLoadResult.cs @@ -0,0 +1,35 @@ +namespace LogExpert.Core.Classes.Persister; + +/// +/// Represents the result of loading a project file, including validation information. +/// +public class ProjectLoadResult +{ + /// + /// The loaded project data (contains resolved log file paths). + /// + public ProjectData ProjectData { get; set; } + + /// + /// Validation result containing valid, missing, and alternative file paths. + /// + public ProjectValidationResult ValidationResult { get; set; } + + /// + /// Mapping of original file references to resolved log files. + /// Key: resolved log file path (.log) + /// Value: original file reference (.lxp or .log) + /// Used to update persistence files when user selects alternatives. + /// + public Dictionary LogToOriginalFileMapping { get; set; } = []; + + /// + /// Indicates whether the project has at least one valid file to load. + /// + public bool HasValidFiles => ValidationResult?.ValidFiles.Count > 0; + + /// + /// Indicates whether user intervention is needed due to missing files. + /// + public bool RequiresUserIntervention => ValidationResult?.HasMissingFiles ?? false; +} \ No newline at end of file diff --git a/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs b/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs index b2ae8f17..8a0c3da5 100644 --- a/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs +++ b/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs @@ -1,5 +1,7 @@ using System.Text; +using LogExpert.Core.Interface; + using Newtonsoft.Json; using NLog; @@ -13,11 +15,13 @@ public static class ProjectPersister #region Public methods /// - /// Loads the project session data from a specified file. + /// Loads the project session data from a specified file, including validation of referenced files. + /// Resolves .lxp persistence files to actual .log files before validation. /// - /// - /// - public static ProjectData LoadProjectData (string projectFileName) + /// The path to the project file (.lxj) + /// The plugin registry for file system validation + /// A containing the project data and validation results + public static ProjectLoadResult LoadProjectData (string projectFileName, IPluginRegistry pluginRegistry) { try { @@ -27,15 +31,74 @@ public static ProjectData LoadProjectData (string projectFileName) }; var json = File.ReadAllText(projectFileName, Encoding.UTF8); - return JsonConvert.DeserializeObject(json, settings); + var projectData = JsonConvert.DeserializeObject(json, settings); + + // Set project file path for alternative file search + projectData.ProjectFilePath = projectFileName; + + // Resolve .lxp files to actual .log files + var resolvedFiles = ProjectFileResolver.ResolveProjectFiles(projectData, pluginRegistry); + + // Create mapping: logFile → originalFile + var logToOriginalMapping = new Dictionary(); + foreach (var (logFile, originalFile) in resolvedFiles) + { + logToOriginalMapping[logFile] = originalFile; + } + + // Create new ProjectData with resolved log file paths + var resolvedProjectData = new ProjectData + { + FileNames = [.. resolvedFiles.Select(r => r.LogFile)], + TabLayoutXml = projectData.TabLayoutXml, + ProjectFilePath = projectData.ProjectFilePath + }; + + // Validate the actual log files (not .lxp files) + var validationResult = ProjectFileValidator.ValidateProject(resolvedProjectData, pluginRegistry); + + return new ProjectLoadResult + { + ProjectData = resolvedProjectData, + ValidationResult = validationResult, + LogToOriginalFileMapping = logToOriginalMapping + }; } catch (Exception ex) when (ex is UnauthorizedAccessException or IOException or JsonSerializationException) { - _logger.Warn($"Error loading persistence data from {projectFileName}, trying old xml version"); - return ProjectPersisterXML.LoadProjectData(projectFileName); + + var projectData = ProjectPersisterXML.LoadProjectData(projectFileName); + + // Set project file path for alternative file search + projectData.ProjectFilePath = projectFileName; + + // Resolve .lxp files for XML fallback as well + var resolvedFiles = ProjectFileResolver.ResolveProjectFiles(projectData, pluginRegistry); + + var logToOriginalMapping = new Dictionary(); + foreach (var (logFile, originalFile) in resolvedFiles) + { + logToOriginalMapping[logFile] = originalFile; + } + + var resolvedProjectData = new ProjectData + { + FileNames = [.. resolvedFiles.Select(r => r.LogFile)], + TabLayoutXml = projectData.TabLayoutXml, + ProjectFilePath = projectData.ProjectFilePath + }; + + var validationResult = ProjectFileValidator.ValidateProject(resolvedProjectData, pluginRegistry); + + return new ProjectLoadResult + { + ProjectData = resolvedProjectData, + ValidationResult = validationResult, + LogToOriginalFileMapping = logToOriginalMapping + }; } } diff --git a/src/LogExpert.Core/Classes/Persister/ProjectValidationResult.cs b/src/LogExpert.Core/Classes/Persister/ProjectValidationResult.cs new file mode 100644 index 00000000..fed2d742 --- /dev/null +++ b/src/LogExpert.Core/Classes/Persister/ProjectValidationResult.cs @@ -0,0 +1,12 @@ +namespace LogExpert.Core.Classes.Persister; + +public class ProjectValidationResult +{ + public List ValidFiles { get; } = []; + + public List MissingFiles { get; } = []; + + public Dictionary> PossibleAlternatives { get; } = []; + + public bool HasMissingFiles => MissingFiles.Count > 0; +} diff --git a/src/LogExpert.Core/Interface/IConfigManager.cs b/src/LogExpert.Core/Interface/IConfigManager.cs index 7face12b..a436cb23 100644 --- a/src/LogExpert.Core/Interface/IConfigManager.cs +++ b/src/LogExpert.Core/Interface/IConfigManager.cs @@ -142,4 +142,22 @@ public interface IConfigManager /// Thrown if settings validation fails. /// Thrown if the file cannot be written. void Save (SettingsFlags flags); + + /// + /// Adds the specified file name to the file history list, moving it to the top if it already exists. + /// + /// If the file name already exists in the history, it is moved to the top of the list. The file + /// history list is limited to a maximum number of entries; the oldest entries are removed if the limit is exceeded. + /// This method is supported only on Windows platforms. + /// The name of the file to add to the file history list. Comparison is case-insensitive. + void AddToFileHistory (string fileName); + + /// + /// Clears the list of recently opened files. + /// + /// Call this method to remove all entries from the recent files list, typically to reset user + /// history or in response to a privacy-related action. After calling this method, the list of last open files will + /// be empty until new files are opened. + + void ClearLastOpenFilesList (); } \ No newline at end of file diff --git a/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj b/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj index ae88c488..7a56ee24 100644 --- a/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj +++ b/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/LogExpert.Persister.Tests/ProjectFileValidatorTests.cs b/src/LogExpert.Persister.Tests/ProjectFileValidatorTests.cs new file mode 100644 index 00000000..ad21d5ff --- /dev/null +++ b/src/LogExpert.Persister.Tests/ProjectFileValidatorTests.cs @@ -0,0 +1,980 @@ +using System.Globalization; + +using LogExpert.Core.Classes.Persister; + +namespace LogExpert.Persister.Tests; + +/// +/// Unit tests for the Project File Validator implementation (Issue #514). +/// Tests validation logic for missing files in project/session loading. +/// Includes tests for ProjectFileResolver, PersisterHelpers, and ProjectPersister updates. +/// +[TestFixture] +public class ProjectFileValidatorTests +{ + private string _testDirectory; + private string _projectFile; + private List _testLogFiles; + + [SetUp] + public void Setup () + { + // Create temporary test directory + _testDirectory = Path.Join(Path.GetTempPath(), "LogExpertTests", "ProjectValidator", Guid.NewGuid().ToString()); + _ = Directory.CreateDirectory(_testDirectory); + + // Initialize test log files list + _testLogFiles = []; + + // Create a project file path (will be created in individual tests) + _projectFile = Path.Join(_testDirectory, "test_project.lxj"); + + // Initialize PluginRegistry for tests + _ = PluginRegistry.PluginRegistry.Create(_testDirectory, 1000); + } + + [TearDown] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Unit Test")] + public void TearDown () + { + // Clean up test directory + if (Directory.Exists(_testDirectory)) + { + try + { + Directory.Delete(_testDirectory, true); + } + catch + { + // Ignore cleanup errors + } + } + } + + #region Helper Methods + + /// + /// Creates test log files with specified names. + /// + private void CreateTestLogFiles (params string[] fileNames) + { + foreach (var fileName in fileNames) + { + var filePath = Path.Join(_testDirectory, fileName); + File.WriteAllText(filePath, $"Test log content for {fileName}"); + _testLogFiles.Add(filePath); + } + } + + /// + /// Creates a test project file with specified log file references. + /// + private void CreateTestProjectFile (params string[] logFileNames) + { + var projectData = new ProjectData + { + FileNames = [.. logFileNames.Select(name => Path.Join(_testDirectory, name))], + TabLayoutXml = "test" + }; + + ProjectPersister.SaveProjectData(_projectFile, projectData); + } + + /// + /// Creates a .lxp persistence file pointing to a log file. + /// + private void CreatePersistenceFile (string lxpFileName, string logFileName) + { + var lxpPath = Path.Join(_testDirectory, lxpFileName); + var logPath = Path.Join(_testDirectory, logFileName); + + var persistenceData = new PersistenceData + { + FileName = logPath + }; + + // Use the correct namespace: LogExpert.Core.Classes.Persister.Persister + _ = Core.Classes.Persister.Persister.SavePersistenceDataWithFixedName(lxpPath, persistenceData); + } + + /// + /// Deletes specified log files to simulate missing files. + /// + private void DeleteLogFiles (params string[] fileNames) + { + foreach (var filePath in fileNames.Select(fileName => Path.Join(_testDirectory, fileName))) + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + } + } + + #endregion + + #region PersisterHelpers Tests + + [Test] + public void PersisterHelpers_FindFilenameForSettings_RegularLogFile_ReturnsUnchanged () + { + // Arrange + CreateTestLogFiles("test.log"); + var logPath = Path.Join(_testDirectory, "test.log"); + + // Act + var result = PersisterHelpers.FindFilenameForSettings(logPath, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.EqualTo(logPath), "Regular log file should be returned unchanged"); + } + + [Test] + public void PersisterHelpers_FindFilenameForSettings_LxpFile_ReturnsLogPath () + { + // Arrange + CreateTestLogFiles("actual.log"); + CreatePersistenceFile("settings.lxp", "actual.log"); + var lxpPath = Path.Join(_testDirectory, "settings.lxp"); + var expectedLogPath = Path.Join(_testDirectory, "actual.log"); + + // Act + var result = PersisterHelpers.FindFilenameForSettings(lxpPath, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.EqualTo(expectedLogPath), "Should resolve .lxp to actual log file"); + } + + [Test] + public void PersisterHelpers_FindFilenameForSettings_NullFileName_ThrowsArgumentNullException () + { + // Act & Assert - ThrowIfNullOrWhiteSpace throws ArgumentNullException for null + _ = Assert.Throws(() => + PersisterHelpers.FindFilenameForSettings((string)null, PluginRegistry.PluginRegistry.Instance)); + } + + [Test] + public void PersisterHelpers_FindFilenameForSettings_EmptyFileName_ThrowsArgumentException () + { + // Act & Assert + _ = Assert.Throws(() => + PersisterHelpers.FindFilenameForSettings(string.Empty, PluginRegistry.PluginRegistry.Instance)); + } + + [Test] + public void PersisterHelpers_FindFilenameForSettings_ListOfFiles_ResolvesAll () + { + // Arrange + CreateTestLogFiles("log1.log", "log2.log", "log3.log"); + var fileList = new List + { + Path.Join(_testDirectory, "log1.log"), + Path.Join(_testDirectory, "log2.log"), + Path.Join(_testDirectory, "log3.log") + }; + + // Act - call the List overload explicitly + var result = PersisterHelpers.FindFilenameForSettings(fileList.AsReadOnly(), PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Has.Count.EqualTo(3), "Should resolve all files"); + Assert.That(result[0], Does.EndWith("log1.log")); + Assert.That(result[1], Does.EndWith("log2.log")); + Assert.That(result[2], Does.EndWith("log3.log")); + } + + [Test] + public void PersisterHelpers_FindFilenameForSettings_MixedLxpAndLog_ResolvesBoth () + { + // Arrange + CreateTestLogFiles("direct.log", "referenced.log"); + CreatePersistenceFile("indirect.lxp", "referenced.log"); + + var fileList = new List + { + Path.Join(_testDirectory, "direct.log"), + Path.Join(_testDirectory, "indirect.lxp") + }; + + // Act - call the List overload explicitly + var result = PersisterHelpers.FindFilenameForSettings(fileList.AsReadOnly(), PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0], Does.EndWith("direct.log"), "Direct log should be unchanged"); + Assert.That(result[1], Does.EndWith("referenced.log"), ".lxp should resolve to referenced log"); + } + + [Test] + public void PersisterHelpers_FindFilenameForSettings_CorruptedLxp_ReturnsLxpPath () + { + // Arrange + var lxpPath = Path.Join(_testDirectory, "corrupted.lxp"); + File.WriteAllText(lxpPath, "This is not valid XML"); + + // Act + var result = PersisterHelpers.FindFilenameForSettings(lxpPath, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.EqualTo(lxpPath), "Corrupted .lxp should return original path"); + } + + #endregion + + #region ProjectFileResolver Tests + + [Test] + public void ProjectFileResolver_ResolveProjectFiles_AllLogFiles_ReturnsUnchanged () + { + // Arrange + CreateTestLogFiles("file1.log", "file2.log"); + var projectData = new ProjectData + { + FileNames = + [ + Path.Join(_testDirectory, "file1.log"), + Path.Join(_testDirectory, "file2.log") + ] + }; + + // Act + var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0].LogFile, Does.EndWith("file1.log")); + Assert.That(result[0].OriginalFile, Does.EndWith("file1.log")); + Assert.That(result[1].LogFile, Does.EndWith("file2.log")); + Assert.That(result[1].OriginalFile, Does.EndWith("file2.log")); + } + + [Test] + public void ProjectFileResolver_ResolveProjectFiles_WithLxpFiles_ResolvesToLogs () + { + // Arrange + CreateTestLogFiles("actual1.log", "actual2.log"); + CreatePersistenceFile("settings1.lxp", "actual1.log"); + CreatePersistenceFile("settings2.lxp", "actual2.log"); + + var projectData = new ProjectData + { + FileNames = + [ + Path.Join(_testDirectory, "settings1.lxp"), + Path.Join(_testDirectory, "settings2.lxp") + ] + }; + + // Act + var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0].LogFile, Does.EndWith("actual1.log"), "Should resolve to actual log"); + Assert.That(result[0].OriginalFile, Does.EndWith("settings1.lxp"), "Should preserve original .lxp"); + Assert.That(result[1].LogFile, Does.EndWith("actual2.log")); + Assert.That(result[1].OriginalFile, Does.EndWith("settings2.lxp")); + } + + [Test] + public void ProjectFileResolver_ResolveProjectFiles_MixedFiles_ResolvesProperly () + { + // Arrange + CreateTestLogFiles("direct.log", "referenced.log"); + CreatePersistenceFile("indirect.lxp", "referenced.log"); + + var projectData = new ProjectData + { + FileNames = + [ + Path.Join(_testDirectory, "direct.log"), + Path.Join(_testDirectory, "indirect.lxp") + ] + }; + + // Act + var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + Assert.That(result[0].LogFile, Does.EndWith("direct.log")); + Assert.That(result[0].OriginalFile, Does.EndWith("direct.log")); + Assert.That(result[1].LogFile, Does.EndWith("referenced.log")); + Assert.That(result[1].OriginalFile, Does.EndWith("indirect.lxp")); + } + + [Test] + public void ProjectFileResolver_ResolveProjectFiles_NullProjectData_ThrowsArgumentNullException () + { + // Act & Assert + _ = Assert.Throws(() => + ProjectFileResolver.ResolveProjectFiles(null, PluginRegistry.PluginRegistry.Instance)); + } + + [Test] + public void ProjectFileResolver_ResolveProjectFiles_EmptyProject_ReturnsEmptyList () + { + // Arrange + var projectData = new ProjectData + { + FileNames = [] + }; + + // Act + var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Empty, "Empty project should return empty list"); + } + + [Test] + public void ProjectFileResolver_ResolveProjectFiles_ReturnsReadOnlyCollection () + { + // Arrange + CreateTestLogFiles("test.log"); + var projectData = new ProjectData + { + FileNames = [Path.Join(_testDirectory, "test.log")] + }; + + // Act + var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.InstanceOf>()); + } + + #endregion + + #region ProjectLoadResult Tests + + [Test] + public void ProjectLoadResult_HasValidFiles_AllFilesValid_ReturnsTrue () + { + // Arrange + var projectData = new ProjectData(); + var validationResult = new ProjectValidationResult(); + validationResult.ValidFiles.Add("file1.log"); + validationResult.ValidFiles.Add("file2.log"); + + var result = new ProjectLoadResult + { + ProjectData = projectData, + ValidationResult = validationResult + }; + + // Act + var hasValidFiles = result.HasValidFiles; + + // Assert + Assert.That(hasValidFiles, Is.True, "Should have valid files"); + } + + [Test] + public void ProjectLoadResult_HasValidFiles_NoValidFiles_ReturnsFalse () + { + // Arrange + var projectData = new ProjectData(); + var validationResult = new ProjectValidationResult(); + validationResult.MissingFiles.Add("file1.log"); + validationResult.MissingFiles.Add("file2.log"); + + var result = new ProjectLoadResult + { + ProjectData = projectData, + ValidationResult = validationResult + }; + + // Act + var hasValidFiles = result.HasValidFiles; + + // Assert + Assert.That(hasValidFiles, Is.False, "Should not have valid files"); + } + + [Test] + public void ProjectLoadResult_HasValidFiles_SomeValidFiles_ReturnsTrue () + { + // Arrange + var projectData = new ProjectData(); + var validationResult = new ProjectValidationResult(); + validationResult.ValidFiles.Add("file1.log"); + validationResult.MissingFiles.Add("file2.log"); + validationResult.MissingFiles.Add("file3.log"); + + var result = new ProjectLoadResult + { + ProjectData = projectData, + ValidationResult = validationResult + }; + + // Act + var hasValidFiles = result.HasValidFiles; + + // Assert + Assert.That(hasValidFiles, Is.True, "Should have at least one valid file"); + } + + [Test] + public void ProjectLoadResult_RequiresUserIntervention_AllFilesValid_ReturnsFalse () + { + // Arrange + var projectData = new ProjectData(); + var validationResult = new ProjectValidationResult(); + validationResult.ValidFiles.Add("file1.log"); + validationResult.ValidFiles.Add("file2.log"); + + var result = new ProjectLoadResult + { + ProjectData = projectData, + ValidationResult = validationResult + }; + + // Act + var requiresIntervention = result.RequiresUserIntervention; + + // Assert + Assert.That(requiresIntervention, Is.False, "Should not require user intervention"); + } + + [Test] + public void ProjectLoadResult_RequiresUserIntervention_SomeMissingFiles_ReturnsTrue () + { + // Arrange + var projectData = new ProjectData(); + var validationResult = new ProjectValidationResult(); + validationResult.ValidFiles.Add("file1.log"); + validationResult.MissingFiles.Add("file2.log"); + + var result = new ProjectLoadResult + { + ProjectData = projectData, + ValidationResult = validationResult + }; + + // Act + var requiresIntervention = result.RequiresUserIntervention; + + // Assert + Assert.That(requiresIntervention, Is.True, "Should require user intervention"); + } + + [Test] + public void ProjectLoadResult_LogToOriginalFileMapping_StoresMapping () + { + // Arrange + var mapping = new Dictionary + { + ["C:\\logs\\actual.log"] = "C:\\settings\\config.lxp", + ["C:\\logs\\direct.log"] = "C:\\logs\\direct.log" + }; + + var result = new ProjectLoadResult + { + LogToOriginalFileMapping = mapping + }; + + // Act & Assert + Assert.That(result.LogToOriginalFileMapping, Has.Count.EqualTo(2)); + Assert.That(result.LogToOriginalFileMapping["C:\\logs\\actual.log"], Is.EqualTo("C:\\settings\\config.lxp")); + Assert.That(result.LogToOriginalFileMapping["C:\\logs\\direct.log"], Is.EqualTo("C:\\logs\\direct.log")); + } + + #endregion + + #region ProjectValidationResult Tests + + [Test] + public void ProjectValidationResult_HasMissingFiles_WithMissingFiles_ReturnsTrue () + { + // Arrange + var result = new ProjectValidationResult(); + result.ValidFiles.Add("file1.log"); + result.MissingFiles.Add("file2.log"); + + // Act + var hasMissing = result.HasMissingFiles; + + // Assert + Assert.That(hasMissing, Is.True, "Should have missing files"); + } + + [Test] + public void ProjectValidationResult_HasMissingFiles_WithoutMissingFiles_ReturnsFalse () + { + // Arrange + var result = new ProjectValidationResult(); + result.ValidFiles.Add("file1.log"); + result.ValidFiles.Add("file2.log"); + + // Act + var hasMissing = result.HasMissingFiles; + + // Assert + Assert.That(hasMissing, Is.False, "Should not have missing files"); + } + + #endregion + + #region ProjectPersister.LoadProjectData - All Files Valid + + [Test] + public void LoadProjectData_AllFilesExist_ReturnsSuccessResult () + { + // Arrange + CreateTestLogFiles("log1.log", "log2.log", "log3.log"); + CreateTestProjectFile("log1.log", "log2.log", "log3.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.ProjectData, Is.Not.Null, "ProjectData should not be null"); + Assert.That(result.ValidationResult, Is.Not.Null, "ValidationResult should not be null"); + Assert.That(result.HasValidFiles, Is.True, "Should have valid files"); + Assert.That(result.RequiresUserIntervention, Is.False, "Should not require intervention"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(3), "Should have 3 valid files"); + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(0), "Should have 0 missing files"); + } + + [Test] + public void LoadProjectData_AllFilesExist_ProjectDataContainsCorrectFiles () + { + // Arrange + CreateTestLogFiles("alpha.log", "beta.log", "gamma.log"); + CreateTestProjectFile("alpha.log", "beta.log", "gamma.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + var fileNames = result.ProjectData.FileNames.Select(Path.GetFileName).ToList(); + Assert.That(fileNames, Does.Contain("alpha.log"), "Should contain alpha.log"); + Assert.That(fileNames, Does.Contain("beta.log"), "Should contain beta.log"); + Assert.That(fileNames, Does.Contain("gamma.log"), "Should contain gamma.log"); + } + + [Test] + public void LoadProjectData_AllFilesExist_PreservesTabLayoutXml () + { + // Arrange + CreateTestLogFiles("test.log"); + CreateTestProjectFile("test.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result.ProjectData.TabLayoutXml, Is.Not.Null.And.Not.Empty, "TabLayoutXml should be preserved"); + Assert.That(result.ProjectData.TabLayoutXml, Does.Contain(""), "Should contain layout XML"); + } + + [Test] + public void LoadProjectData_WithLxpFiles_ResolvesToActualLogs () + { + // Arrange + CreateTestLogFiles("actual1.log", "actual2.log"); + CreatePersistenceFile("settings1.lxp", "actual1.log"); + CreatePersistenceFile("settings2.lxp", "actual2.log"); + + // Create project referencing .lxp files + var projectData = new ProjectData + { + FileNames = + [ + Path.Join(_testDirectory, "settings1.lxp"), + Path.Join(_testDirectory, "settings2.lxp") + ] + }; + ProjectPersister.SaveProjectData(_projectFile, projectData); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(2), "Should validate actual log files"); + var fileNames = result.ProjectData.FileNames.Select(Path.GetFileName).ToList(); + Assert.That(fileNames, Does.Contain("actual1.log"), "Should contain resolved log file"); + Assert.That(fileNames, Does.Contain("actual2.log"), "Should contain resolved log file"); + } + + [Test] + public void LoadProjectData_WithLxpFiles_PreservesMapping () + { + // Arrange + CreateTestLogFiles("actual.log"); + CreatePersistenceFile("settings.lxp", "actual.log"); + + var projectData = new ProjectData + { + FileNames = [Path.Join(_testDirectory, "settings.lxp")] + }; + ProjectPersister.SaveProjectData(_projectFile, projectData); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result.LogToOriginalFileMapping, Is.Not.Null); + Assert.That(result.LogToOriginalFileMapping, Has.Count.EqualTo(1)); + var actualLogPath = Path.Join(_testDirectory, "actual.log"); + var lxpPath = Path.Join(_testDirectory, "settings.lxp"); + Assert.That(result.LogToOriginalFileMapping[actualLogPath], Is.EqualTo(lxpPath)); + } + + #endregion + + #region ProjectPersister.LoadProjectData - Some Files Missing + + [Test] + public void LoadProjectData_SomeFilesMissing_ReturnsPartialSuccessResult () + { + // Arrange + CreateTestLogFiles("exists1.log", "exists2.log", "missing.log"); + DeleteLogFiles("missing.log"); // Delete to simulate missing + CreateTestProjectFile("exists1.log", "exists2.log", "missing.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.HasValidFiles, Is.True, "Should have some valid files"); + Assert.That(result.RequiresUserIntervention, Is.True, "Should require user intervention"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(2), "Should have 2 valid files"); + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(1), "Should have 1 missing file"); + } + + [Test] + public void LoadProjectData_SomeFilesMissing_ValidFilesListIsCorrect () + { + // Arrange + CreateTestLogFiles("valid1.log", "valid2.log", "invalid.log"); + DeleteLogFiles("invalid.log"); + CreateTestProjectFile("valid1.log", "valid2.log", "invalid.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + var validFileNames = result.ValidationResult.ValidFiles.Select(Path.GetFileName).ToList(); + Assert.That(validFileNames, Does.Contain("valid1.log"), "Should contain valid1.log"); + Assert.That(validFileNames, Does.Contain("valid2.log"), "Should contain valid2.log"); + Assert.That(validFileNames, Does.Not.Contain("invalid.log"), "Should not contain invalid.log"); + } + + [Test] + public void LoadProjectData_SomeFilesMissing_MissingFilesListIsCorrect () + { + // Arrange + CreateTestLogFiles("present.log", "absent1.log", "absent2.log"); + DeleteLogFiles("absent1.log", "absent2.log"); + CreateTestProjectFile("present.log", "absent1.log", "absent2.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + var missingFileNames = result.ValidationResult.MissingFiles.Select(Path.GetFileName).ToList(); + Assert.That(missingFileNames, Does.Contain("absent1.log"), "Should contain absent1.log"); + Assert.That(missingFileNames, Does.Contain("absent2.log"), "Should contain absent2.log"); + Assert.That(missingFileNames, Does.Not.Contain("present.log"), "Should not contain present.log"); + } + + [Test] + public void LoadProjectData_MajorityFilesMissing_StillReturnsValidFiles () + { + // Arrange + CreateTestLogFiles("only_valid.log", "missing1.log", "missing2.log", "missing3.log", "missing4.log"); + DeleteLogFiles("missing1.log", "missing2.log", "missing3.log", "missing4.log"); + CreateTestProjectFile("only_valid.log", "missing1.log", "missing2.log", "missing3.log", "missing4.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result.HasValidFiles, Is.True, "Should have at least one valid file"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(1), "Should have 1 valid file"); + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(4), "Should have 4 missing files"); + } + + [Test] + public void LoadProjectData_LxpReferencingMissingLog_ReportsLogAsMissing () + { + // Arrange + CreateTestLogFiles("missing.log"); + CreatePersistenceFile("settings.lxp", "missing.log"); + DeleteLogFiles("missing.log"); + + var projectData = new ProjectData + { + FileNames = [Path.Join(_testDirectory, "settings.lxp")] + }; + ProjectPersister.SaveProjectData(_projectFile, projectData); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(1), "Should report missing log file"); + Assert.That(result.ValidationResult.MissingFiles[0], Does.EndWith("missing.log")); + } + + #endregion + + #region ProjectPersister.LoadProjectData - All Files Missing + + [Test] + public void LoadProjectData_AllFilesMissing_ReturnsFailureResult () + { + // Arrange + CreateTestLogFiles("missing1.log", "missing2.log"); + DeleteLogFiles("missing1.log", "missing2.log"); + CreateTestProjectFile("missing1.log", "missing2.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.HasValidFiles, Is.False, "Should not have valid files"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(0), "Should have 0 valid files"); + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(2), "Should have 2 missing files"); + } + + [Test] + public void LoadProjectData_AllFilesMissing_MissingFilesListComplete () + { + // Arrange + CreateTestLogFiles("gone1.log", "gone2.log", "gone3.log"); + DeleteLogFiles("gone1.log", "gone2.log", "gone3.log"); + CreateTestProjectFile("gone1.log", "gone2.log", "gone3.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(3), "Should have 3 missing files"); + var missingFileNames = result.ValidationResult.MissingFiles.Select(Path.GetFileName).ToList(); + Assert.That(missingFileNames, Does.Contain("gone1.log")); + Assert.That(missingFileNames, Does.Contain("gone2.log")); + Assert.That(missingFileNames, Does.Contain("gone3.log")); + } + + #endregion + + #region ProjectPersister.LoadProjectData - Empty/Invalid Projects + + [Test] + public void LoadProjectData_EmptyProject_ReturnsEmptyResult () + { + // Arrange + CreateTestProjectFile(); // Empty project with no files + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.ProjectData.FileNames, Is.Empty, "FileNames should be empty"); + Assert.That(result.ValidationResult.ValidFiles, Is.Empty, "ValidFiles should be empty"); + Assert.That(result.ValidationResult.MissingFiles, Is.Empty, "MissingFiles should be empty"); + } + + [Test] + public void LoadProjectData_NonExistentProjectFile_ReturnsNull () + { + // Arrange + var nonExistentProject = Path.Join(_testDirectory, "does_not_exist.lxj"); + + // Act + var result = ProjectPersister.LoadProjectData(nonExistentProject, PluginRegistry.PluginRegistry.Instance); + + // Assert + // FIXED: Now returns empty result instead of null when file doesn't exist + Assert.That(result, Is.Not.Null, "Result should not be null even for non-existent file"); + Assert.That(result.ProjectData, Is.Not.Null, "ProjectData should be initialized"); + } + + [Test] + public void LoadProjectData_CorruptedProjectFile_ThrowsJsonReaderException () + { + // Arrange + var corruptedProject = Path.Join(_testDirectory, "corrupted.lxj"); + File.WriteAllText(corruptedProject, "This is not valid XML or JSON"); + + // Act & Assert - JsonReaderException is not caught, so it propagates + _ = Assert.Throws(() => + ProjectPersister.LoadProjectData(corruptedProject, PluginRegistry.PluginRegistry.Instance)); + } + + #endregion + + #region Edge Cases and Special Scenarios + + [Test] + public void LoadProjectData_DuplicateFileReferences_HandlesCorrectly () + { + // Arrange + CreateTestLogFiles("duplicate.log"); + var projectData = new ProjectData + { + FileNames = + [ + Path.Join(_testDirectory, "duplicate.log"), + Path.Join(_testDirectory, "duplicate.log"), + Path.Join(_testDirectory, "duplicate.log") + ] + }; + ProjectPersister.SaveProjectData(_projectFile, projectData); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.HasValidFiles, Is.True, "Should have valid files"); + // Validation should handle duplicates gracefully + } + + [Test] + public void LoadProjectData_FilesWithSpecialCharacters_ValidatesCorrectly () + { + // Arrange + CreateTestLogFiles("file with spaces.log", "file-with-dashes.log", "file_with_underscores.log"); + CreateTestProjectFile("file with spaces.log", "file-with-dashes.log", "file_with_underscores.log"); + + // Act + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(3), "Should validate all files with special characters"); + } + + [Test] + public void LoadProjectData_VeryLargeProject_ValidatesEfficiently () + { + // Arrange + const int fileCount = 100; + var fileNames = new List(); + + for (int i = 0; i < fileCount; i++) + { + var fileName = $"log_{i:D4}.log"; + fileNames.Add(fileName); + } + + CreateTestLogFiles([.. fileNames]); + CreateTestProjectFile([.. fileNames]); + + // Act + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + stopwatch.Stop(); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(fileCount), $"Should validate all {fileCount} files"); + Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(5000), "Should complete validation in reasonable time"); + } + + #endregion + + #region Performance and Stress Tests + + [Test] + public void LoadProjectData_ManyMissingFiles_PerformsEfficiently () + { + // Arrange + const int totalFiles = 50; + var fileNames = new List(); + + // Create only first 10 files, rest will be missing + for (int i = 0; i < 10; i++) + { + var fileName = $"exists_{i}.log"; + fileNames.Add(fileName); + CreateTestLogFiles(fileName); + } + + for (int i = 10; i < totalFiles; i++) + { + fileNames.Add($"missing_{i}.log"); + } + + CreateTestProjectFile([.. fileNames]); + + // Act + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance); + stopwatch.Stop(); + + // Assert + Assert.That(result, Is.Not.Null, "Result should not be null"); + Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(10), "Should have 10 valid files"); + Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(40), "Should have 40 missing files"); + Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(2000), "Should handle many missing files efficiently"); + } + + #endregion + + #region Null and Exception Handling + + [Test] + public void LoadProjectData_NullProjectFile_ThrowsArgumentNullException () + { + // Act & Assert - File.ReadAllText throws ArgumentNullException for null path + _ = Assert.Throws(() => + ProjectPersister.LoadProjectData(null, PluginRegistry.PluginRegistry.Instance)); + } + + [Test] + public void LoadProjectData_EmptyProjectFile_ThrowsArgumentException () + { + // Act & Assert - File.ReadAllText throws ArgumentException for empty string + _ = Assert.Throws(() => + ProjectPersister.LoadProjectData(string.Empty, PluginRegistry.PluginRegistry.Instance)); + } + + [Test] + public void LoadProjectData_NullPluginRegistry_ThrowsArgumentNullException () + { + // Arrange + CreateTestProjectFile("test.log"); + + // Act & Assert + _ = Assert.Throws(() => + ProjectPersister.LoadProjectData(_projectFile, null)); + } + + #endregion + + #region Backward Compatibility + + [Test] + public void LoadProjectData_LegacyProjectFormat_ThrowsJsonReaderException () + { + // Arrange + CreateTestLogFiles("legacy.log"); + + // Create a legacy format project file (XML) + var legacyXml = @" + + + + +"; + + var legacyContent = string.Format(CultureInfo.InvariantCulture, legacyXml, Path.Join(_testDirectory, "legacy.log")); + File.WriteAllText(_projectFile, legacyContent); + + // Act & Assert - JsonReaderException is not caught, so XML fallback doesn't trigger + _ = Assert.Throws(() => + ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance)); + } + + #endregion +} \ No newline at end of file diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index f9797035..68f34bbe 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -966,8 +966,7 @@ public static string HighlightDialog_UI_ErrorDuringAddOfHighLightEntry { } /// - /// Looks up a localized string similar to Error during save of entry. - /// {0}. + /// Looks up a localized string similar to Error during save of entry. {0}. /// public static string HighlightDialog_UI_ErrorDuringSavingOfHighlightEntry { get { @@ -1245,6 +1244,78 @@ public static string KeywordActionDlg_UI_Title { } } + /// + /// Looks up a localized string similar to Error loading project file. The file may be corrupted or inaccessible.. + /// + public static string LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to update session file: {0}. + /// + public static string LoadProject_UI_Message_Error_Message_FailedToUpdateSessionFile { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_Message_FailedToUpdateSessionFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session file has been updated with the new file paths.. + /// + public static string LoadProject_UI_Message_Error_Message_UpdateSessionFile { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_Message_UpdateSessionFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session Update Failed. + /// + public static string LoadProject_UI_Message_Error_Title_FailedToUpdateSessionFile { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_FailedToUpdateSessionFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project Load Failed. + /// + public static string LoadProject_UI_Message_Error_Title_ProjectLoadFailed { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_ProjectLoadFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session Load Failed. + /// + public static string LoadProject_UI_Message_Error_Title_SessionLoadFailed { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_SessionLoadFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session Updated. + /// + public static string LoadProject_UI_Message_Error_Title_UpdateSessionFile { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_UpdateSessionFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to None of the files in this session could be found. The session cannot be loaded.. + /// + public static string LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound { + get { + return ResourceManager.GetString("LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Could not begin restart session. Unable to determine file locker.. /// @@ -3695,6 +3766,141 @@ public static string LogWindow_UI_WriteFilterToTab_NamePrefix_ForFilter { } } + /// + /// Looks up a localized string similar to Close existing tabs. + /// + public static string MissingFilesDialog_UI_Button_CloseTabs { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Button_CloseTabs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ignore layout data. + /// + public static string MissingFilesDialog_UI_Button_Ignore { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Button_Ignore", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Load && Update Session. + /// + public static string MissingFilesDialog_UI_Button_LoadUpdateSession { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Button_LoadUpdateSession", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open new window. + /// + public static string MissingFilesDialog_UI_Button_NewWindow { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Button_NewWindow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Load && Update Session ({0}). + /// + public static string MissingFilesDialog_UI_Button_UpdateSessionAlternativeCount { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Button_UpdateSessionAlternativeCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alternative. + /// + public static string MissingFilesDialog_UI_FileStatus_Alternative { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Alternative", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing. + /// + public static string MissingFilesDialog_UI_FileStatus_Missing { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Missing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Selected. + /// + public static string MissingFilesDialog_UI_FileStatus_Selected { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Selected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Valid. + /// + public static string MissingFilesDialog_UI_FileStatus_Valid { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Valid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log Files (*.lxp)|*.lxp|All Files (*.*)|*.*. + /// + public static string MissingFilesDialog_UI_Filter_Logfiles { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Filter_Logfiles", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Locate: {0}. + /// + public static string MissingFilesDialog_UI_Filter_Title { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Filter_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please choose how to proceed:. + /// + public static string MissingFilesDialog_UI_Label_ChooseHowToProceed { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Label_ChooseHowToProceed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restoring layout requires an empty workbench.. + /// + public static string MissingFilesDialog_UI_Label_Informational { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Label_Informational", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Found: {0} of {1} files ({2} missing). + /// + public static string MissingFilesDialog_UI_Label_Summary { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Label_Summary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loading Session. + /// + public static string MissingFilesDialog_UI_Title { + get { + return ResourceManager.GetString("MissingFilesDialog_UI_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to File name pattern:. /// @@ -4390,60 +4596,6 @@ public static string Program_UI_Error_Pipe_CannotConnectToFirstInstance { } } - /// - /// Looks up a localized string similar to Close existing tabs. - /// - public static string ProjectLoadDlg_UI_Button_CloseTabs { - get { - return ResourceManager.GetString("ProjectLoadDlg_UI_Button_CloseTabs", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ignore layout data. - /// - public static string ProjectLoadDlg_UI_Button_Ignore { - get { - return ResourceManager.GetString("ProjectLoadDlg_UI_Button_Ignore", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Open new window. - /// - public static string ProjectLoadDlg_UI_Button_NewWindow { - get { - return ResourceManager.GetString("ProjectLoadDlg_UI_Button_NewWindow", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please choose how to proceed:. - /// - public static string ProjectLoadDlg_UI_Label_ChooseHowToProceed { - get { - return ResourceManager.GetString("ProjectLoadDlg_UI_Label_ChooseHowToProceed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Restoring layout requires an empty workbench.. - /// - public static string ProjectLoadDlg_UI_Label_Informational { - get { - return ResourceManager.GetString("ProjectLoadDlg_UI_Label_Informational", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Loading Session. - /// - public static string ProjectLoadDlg_UI_Title { - get { - return ResourceManager.GetString("ProjectLoadDlg_UI_Title", resourceCulture); - } - } - /// /// Looks up a localized string similar to RegEx.htm. /// diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index 10814e45..4d66d6b2 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -1689,22 +1689,22 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools Start: {0} Ende: {1} - + Sitzung laden - + Das Wiederherstellen des Layouts erfordert eine leere Arbeitsfläche. - + Bitte wählen Sie, wie Sie fortfahren möchten: - + Vorhandene Tabs schließen - + Neues Fenster öffnen - + Layoutdaten ignorieren @@ -1988,7 +1988,13 @@ Regex-Suche/Ersetzen in der aktuell ausgewählten Zeile Vertrauen bestätigen - Vertrauen für Plugin entfernen:`n`n{0}`n`nDas Plugin wird nicht geladen, bis es erneut zur Vertrauensliste hinzugefügt wird.`n`nFortfahren? + Vertrauen für Plugin entfernen: + +{0} + +Das Plugin wird nicht geladen, bis es erneut zur Vertrauensliste hinzugefügt wird. + +Fortfahren? Entfernung bestätigen @@ -2038,4 +2044,75 @@ Regex-Suche/Ersetzen in der aktuell ausgewählten Zeile Plugin &Trust Management... + + Fehlen + + + Ungültiges Regex-Muster: {0} + + + Suchen: {0} + + + Protokolldateien (*.lxp)|*.lxp|Alle Dateien (*.*)|*.* + + + && Aktualisierungssitzung laden + + + && Aktualisierungssitzung laden ({0}) + + + Gefunden: {0} von {1} Dateien ({2} fehlen) + + + Ausgewählt + + + Alternative + + + Gültig + + + Sitzungsaktualisierung fehlgeschlagen + + + Sitzungsdatei konnte nicht aktualisiert werden: {0} + + + Sitzung aktualisiert + + + Die Sitzungsdatei wurde mit den neuen Dateipfaden aktualisiert. + + + Das Laden der Sitzung ist fehlgeschlagen + + + Keine der Dateien in dieser Sitzung konnte gefunden werden. Die Sitzung kann nicht geladen werden. + + + Das Laden des Projekts ist fehlgeschlagen + + + Fehler beim Laden der Projektdatei. Die Datei ist möglicherweise beschädigt oder nicht zugänglich. + + + Standard (einzelne Zeile) + + + Keine Spaltenaufteilung. Die gesamte Zeile wird in einer einzigen Spalte angezeigt. + + + Es muss mindestens eine Datei bereitgestellt werden. + + + Neustart empfohlen + + + Plugin-Vertrauenskonfiguration aktualisiert. + +LogExpert neu starten, um die Änderungen zu übernehmen? + \ No newline at end of file diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index ff6ae79b..a9133a4f 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -255,8 +255,7 @@ [Default] - Error during save of entry. - {0} + Error during save of entry. {0} No processes are locking the path specified @@ -1692,22 +1691,22 @@ Checked tools will appear in the icon bar. All other tools are available in the Start: {0} End: {1} - + Loading Session - + Restoring layout requires an empty workbench. - + Please choose how to proceed: - + Close existing tabs - + Open new window - + Ignore layout data @@ -2074,4 +2073,55 @@ Restart LogExpert to apply changes? Default (single line) + + Error loading project file. The file may be corrupted or inaccessible. + + + Project Load Failed + + + None of the files in this session could be found. The session cannot be loaded. + + + Session Load Failed + + + Session file has been updated with the new file paths. + + + Session Updated + + + Failed to update session file: {0} + + + Session Update Failed + + + Valid + + + Alternative + + + Selected + + + Missing + + + Found: {0} of {1} files ({2} missing) + + + Load && Update Session ({0}) + + + Load && Update Session + + + Log Files (*.lxp)|*.lxp|All Files (*.*)|*.* + + + Locate: {0} + \ No newline at end of file diff --git a/src/LogExpert.Tests/ConfigManagerTest.cs b/src/LogExpert.Tests/ConfigManagerTest.cs index 6b58183b..0c835a0f 100644 --- a/src/LogExpert.Tests/ConfigManagerTest.cs +++ b/src/LogExpert.Tests/ConfigManagerTest.cs @@ -675,8 +675,7 @@ public void Import_WithValidPopulatedSettings_ShouldSucceed () // Verify settings were actually imported Settings currentSettings = _configManager.Settings; - Assert.That(currentSettings.FilterList.Any(f => f.SearchText == "IMPORT_TEST_FILTER"), Is.True, - "Imported filter should be present"); + Assert.That(currentSettings.FilterList.Any(f => f.SearchText == "IMPORT_TEST_FILTER"), Is.True, "Imported filter should be present"); } [Test] diff --git a/src/LogExpert.UI/Dialogs/FileStatus.cs b/src/LogExpert.UI/Dialogs/FileStatus.cs new file mode 100644 index 00000000..cdf8f891 --- /dev/null +++ b/src/LogExpert.UI/Dialogs/FileStatus.cs @@ -0,0 +1,27 @@ +namespace LogExpert.UI.Dialogs; + +/// +/// Represents the status of a file in the missing files dialog. +/// +public enum FileStatus +{ + /// + /// File exists and is accessible. + /// + Valid, + + /// + /// File is missing but alternatives are available. + /// + MissingWithAlternatives, + + /// + /// File is missing and no alternatives found. + /// + Missing, + + /// + /// User has manually selected an alternative path. + /// + AlternativeSelected +} diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 0c39bf56..85fecd78 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -19,7 +19,6 @@ using LogExpert.Core.Interface; using LogExpert.Dialogs; using LogExpert.Entities; -using LogExpert.PluginRegistry.FileSystem; using LogExpert.UI.Dialogs; using LogExpert.UI.Entities; using LogExpert.UI.Extensions; @@ -41,7 +40,6 @@ internal partial class LogTabWindow : Form, ILogTabWindow private const int MAX_COLUMNIZER_HISTORY = 40; private const int MAX_COLOR_HISTORY = 40; private const int DIFF_MAX = 100; - private const int MAX_FILE_HISTORY = 10; private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly Icon _deadIcon; @@ -546,7 +544,7 @@ public LogWindow.LogWindow AddFileTabDeferred (string givenFileName, bool isTemp [SupportedOSPlatform("windows")] public LogWindow.LogWindow AddFileTab (string givenFileName, bool isTempFile, string title, bool forcePersistenceLoading, ILogLineMemoryColumnizer preProcessColumnizer, bool doNotAddToDockPanel = false) { - var logFileName = FindFilenameForSettings(givenFileName); + var logFileName = PersisterHelpers.FindFilenameForSettings(givenFileName, PluginRegistry.PluginRegistry.Instance); var win = FindWindowForFile(logFileName); if (win != null) { @@ -585,20 +583,21 @@ public LogWindow.LogWindow AddFileTab (string givenFileName, bool isTempFile, st var data = logWindow.Tag as LogWindowData; data.Color = _defaultTabColor; - SetTabColor(logWindow, _defaultTabColor); + //TODO SetTabColor and the Coloring must be reimplemented with a different UI Framework + //SetTabColor(logWindow, _defaultTabColor); //data.tabPage.BorderColor = this.defaultTabBorderColor; - if (!isTempFile) - { - foreach (var colorEntry in ConfigManager.Settings.FileColors) - { - if (colorEntry.FileName.ToUpperInvariant().Equals(logFileName.ToUpperInvariant(), StringComparison.Ordinal)) - { - data.Color = colorEntry.Color; - SetTabColor(logWindow, colorEntry.Color); - break; - } - } - } + //if (!isTempFile) + //{ + // foreach (var colorEntry in ConfigManager.Settings.FileColors) + // { + // if (colorEntry.FileName.ToUpperInvariant().Equals(logFileName.ToUpperInvariant(), StringComparison.Ordinal)) + // { + // data.Color = colorEntry.Color; + // //SetTabColor(logWindow, colorEntry.Color); + // break; + // } + // } + //} if (!isTempFile) { @@ -910,8 +909,7 @@ private void DestroyBookmarkWindow () private void SaveLastOpenFilesList () { - ConfigManager.Settings.LastOpenFilesList.Clear(); - foreach (DockContent content in dockPanel.Contents) + foreach (DockContent content in dockPanel.Contents.Cast()) { if (content is LogWindow.LogWindow logWin) { @@ -1045,24 +1043,7 @@ private void DisconnectEventHandlers (LogWindow.LogWindow logWindow) [SupportedOSPlatform("windows")] private void AddToFileHistory (string fileName) { - bool FindName (string s) => s.ToUpperInvariant().Equals(fileName.ToUpperInvariant(), StringComparison.Ordinal); - - var index = ConfigManager.Settings.FileHistoryList.FindIndex(FindName); - - if (index != -1) - { - ConfigManager.Settings.FileHistoryList.RemoveAt(index); - } - - ConfigManager.Settings.FileHistoryList.Insert(0, fileName); - - while (ConfigManager.Settings.FileHistoryList.Count > MAX_FILE_HISTORY) - { - ConfigManager.Settings.FileHistoryList.RemoveAt(ConfigManager.Settings.FileHistoryList.Count - 1); - } - - ConfigManager.Save(SettingsFlags.FileHistory); - + ConfigManager.AddToFileHistory(fileName); FillHistoryMenu(); } @@ -1083,46 +1064,6 @@ private LogWindow.LogWindow FindWindowForFile (string fileName) return null; } - /// - /// Checks if the file name is a settings file. If so, the contained logfile name - /// is returned. If not, the given file name is returned unchanged. - /// - /// - /// - private static string FindFilenameForSettings (string fileName) - { - if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase)) - { - var persistenceData = Persister.Load(fileName); - if (persistenceData == null) - { - return fileName; - } - - if (!string.IsNullOrEmpty(persistenceData.FileName)) - { - var fs = PluginRegistry.PluginRegistry.Instance.FindFileSystemForUri(persistenceData.FileName); - if (fs != null && !fs.GetType().Equals(typeof(LocalFileSystem))) - { - return persistenceData.FileName; - } - - // On relative paths the URI check (and therefore the file system plugin check) will fail. - // So fs == null and fs == LocalFileSystem are handled here like normal files. - if (Path.IsPathRooted(persistenceData.FileName)) - { - return persistenceData.FileName; - } - - // handle relative paths in .lxp files - var dir = Path.GetDirectoryName(fileName); - return Path.Join(dir, persistenceData.FileName); - } - } - - return fileName; - } - [SupportedOSPlatform("windows")] private void FillHistoryMenu () { @@ -1372,7 +1313,7 @@ private void ChangeCurrentLogWindow (LogWindow.LogWindow newLogWindow) oldLogWindow.BookmarkAdded -= OnBookmarkAdded; oldLogWindow.BookmarkRemoved -= OnBookmarkRemoved; oldLogWindow.BookmarkTextChanged -= OnBookmarkTextChanged; - DisconnectToolWindows(oldLogWindow); + DisconnectToolWindows(); } if (newLogWindow != null) @@ -1434,13 +1375,12 @@ private void ConnectBookmarkWindow (LogWindow.LogWindow logWindow) _bookmarkWindow.SetCurrentFile(ctx); } - private void DisconnectToolWindows (LogWindow.LogWindow logWindow) + private void DisconnectToolWindows () { - DisconnectBookmarkWindow(logWindow); + DisconnectBookmarkWindow(); } - //TODO Find out if logwindow is necessary here - private void DisconnectBookmarkWindow (LogWindow.LogWindow logWindow) + private void DisconnectBookmarkWindow () { _bookmarkWindow.SetBookmarkData(null); _bookmarkWindow.SetCurrentFile(null); @@ -1460,6 +1400,7 @@ private void GuiStateUpdateWorker (GuiStateEventArgs e) multiFileToolStripMenuItem.Checked = e.IsMultiFileActive; multiFileEnabledStripMenuItem.Checked = e.IsMultiFileActive; cellSelectModeToolStripMenuItem.Checked = e.CellSelectMode; + RefreshEncodingMenuBar(e.CurrentEncoding); if (e.TimeshiftPossible && ConfigManager.Settings.Preferences.TimestampControl) @@ -1679,6 +1620,7 @@ private static int GetLevelFromDiff (int diff) } [SupportedOSPlatform("windows")] + //TODO Task based private void LedThreadProc () { Thread.CurrentThread.Name = "LED Thread"; @@ -2002,44 +1944,96 @@ private void CloseAllTabs () } //TODO Reimplementation needs a new UI Framework since, DockpanelSuite has no easy way to change TabColor - private static void SetTabColor (LogWindow.LogWindow logWindow, Color color) - { - //tabPage.BackLowColor = color; - //tabPage.BackLowColorDisabled = Color.FromArgb(255, - // Math.Max(0, color.R - 50), - // Math.Max(0, color.G - 50), - // Math.Max(0, color.B - 50) - // ); - } + //private static void SetTabColor (LogWindow.LogWindow logWindow, Color color) + //{ + // //tabPage.BackLowColor = color; + // //tabPage.BackLowColorDisabled = Color.FromArgb(255, + // // Math.Max(0, color.R - 50), + // // Math.Max(0, color.G - 50), + // // Math.Max(0, color.B - 50) + // // ); + //} [SupportedOSPlatform("windows")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0010:Add missing cases", Justification = "no need for the other switch cases")] private void LoadProject (string projectFileName, bool restoreLayout) { - var projectData = ProjectPersister.LoadProjectData(projectFileName); - var hasLayoutData = projectData.TabLayoutXml != null; - - if (hasLayoutData && restoreLayout && _logWindowList.Count > 0) + try { - ProjectLoadDlg dlg = new(); - if (DialogResult.Cancel != dlg.ShowDialog()) + // Load project with validation + var loadResult = ProjectPersister.LoadProjectData(projectFileName, PluginRegistry.PluginRegistry.Instance); + + if (loadResult?.ProjectData == null) + { + ShowOkMessage( + Resources.LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible, + Resources.LoadProject_UI_Message_Error_Title_ProjectLoadFailed, + MessageBoxIcon.Error); + + return; + } + + var projectData = loadResult.ProjectData; + var hasLayoutData = projectData.TabLayoutXml != null; + + if (projectData.FileNames.Count == 0) { - switch (dlg.ProjectLoadResult) + ShowOkMessage( + Resources.LoadProject_UI_Message_Error_Title_SessionLoadFailed, + Resources.LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound, + MessageBoxIcon.Error); + return; + } + + // Handle missing files or layout options + if (loadResult.RequiresUserIntervention) + { + // Show enhanced dialog with browsing capability and layout options + var (dialogResult, updateSessionFile, selectedAlternatives) = MissingFilesDialog.ShowDialog(loadResult.ValidationResult, hasLayoutData); + + if (dialogResult == MissingFilesDialogResult.Cancel) { - case ProjectLoadDlgResult.IgnoreLayout: - hasLayoutData = false; - break; - case ProjectLoadDlgResult.CloseTabs: + return; + } + + if (updateSessionFile) + { + // Replace original paths with selected alternatives in project data + for (int i = 0; i < projectData.FileNames.Count; i++) + { + var originalPath = projectData.FileNames[i]; + if (selectedAlternatives.TryGetValue(originalPath, out string value)) + { + projectData.FileNames[i] = value; + } + } + + ProjectPersister.SaveProjectData(projectFileName, projectData); + + ShowOkMessage( + Resources.LoadProject_UI_Message_Error_Message_UpdateSessionFile, + Resources.LoadProject_UI_Message_Error_Title_UpdateSessionFile, + MessageBoxIcon.Information); + } + + // Handle layout-related results + switch (dialogResult) + { + case MissingFilesDialogResult.CloseTabsAndRestoreLayout: CloseAllTabs(); break; - case ProjectLoadDlgResult.NewWindow: - LogExpertProxy.NewWindow([projectFileName]); - return; + case MissingFilesDialogResult.OpenInNewWindow: + { + var logFileNames = PersisterHelpers.FindFilenameForSettings(projectData.FileNames.AsReadOnly(), PluginRegistry.PluginRegistry.Instance); + LogExpertProxy.NewWindow([.. logFileNames]); + return; + } + case MissingFilesDialogResult.IgnoreLayout: + hasLayoutData = false; + break; } } - } - if (projectData != null) - { foreach (var fileName in projectData.FileNames) { _ = hasLayoutData @@ -2047,16 +2041,38 @@ private void LoadProject (string projectFileName, bool restoreLayout) : AddFileTab(fileName, false, null, true, null); } - if (hasLayoutData && restoreLayout) + // Restore layout only if we loaded at least one file + if (hasLayoutData && restoreLayout && _logWindowList.Count > 0) { + _logger.Info("Restoring layout"); // Re-creating tool (non-document) windows is needed because the DockPanel control would throw strange errors DestroyToolWindows(); InitToolWindows(); RestoreLayout(projectData.TabLayoutXml); } + else if (_logWindowList.Count == 0) + { + _logger.Warn("No files loaded, skipping layout restoration"); + } + } + catch (Exception ex) + { + ShowOkMessage( + $"Error loading project: {ex.Message}", + Resources.LogExpert_Common_UI_Title_Error, + MessageBoxIcon.Error); } } + private static void ShowOkMessage (string title, string message, MessageBoxIcon icon) + { + _ = MessageBox.Show( + message, + title, + MessageBoxButtons.OK, + icon); + } + [SupportedOSPlatform("windows")] private void ApplySelectedHighlightGroup () { @@ -2229,6 +2245,8 @@ private void OnLogTabWindowLoad (object sender, EventArgs e) AddFileTab(name, false, null, false, null); } } + + ConfigManager.ClearLastOpenFilesList(); } if (_startupFileNames != null) @@ -2623,8 +2641,7 @@ private void OnFileSizeChanged (object sender, LogEventArgs e) //if (this.dockPanel.ActiveContent != null && // this.dockPanel.ActiveContent != sender || data.tailState != 0) - if ((CurrentLogWindow != null && - CurrentLogWindow != sender) || data.TailState != 0) + if (CurrentLogWindow != null && CurrentLogWindow != sender || data.TailState != 0) { data.Dirty = true; } @@ -2928,45 +2945,46 @@ private void OnCloseAllTabsToolStripMenuItemClick (object sender, EventArgs e) [SupportedOSPlatform("windows")] private void OnTabColorToolStripMenuItemClick (object sender, EventArgs e) { - var logWindow = dockPanel.ActiveContent as LogWindow.LogWindow; + //Todo TabColoring must be reimplemented with a different UI Framework + //var logWindow = dockPanel.ActiveContent as LogWindow.LogWindow; - if (logWindow.Tag is not LogWindowData data) - { - return; - } + //if (logWindow.Tag is not LogWindowData data) + //{ + // return; + //} - ColorDialog dlg = new() - { - Color = data.Color - }; + //ColorDialog dlg = new() + //{ + // Color = data.Color + //}; - if (dlg.ShowDialog() == DialogResult.OK) - { - data.Color = dlg.Color; - SetTabColor(logWindow, data.Color); - } + //if (dlg.ShowDialog() == DialogResult.OK) + //{ + // data.Color = dlg.Color; + // //SetTabColor(logWindow, data.Color); + //} - List delList = []; + //List delList = []; - foreach (var entry in ConfigManager.Settings.FileColors) - { - if (entry.FileName.Equals(logWindow.FileName, StringComparison.Ordinal)) - { - delList.Add(entry); - } - } + //foreach (var entry in ConfigManager.Settings.FileColors) + //{ + // if (entry.FileName.Equals(logWindow.FileName, StringComparison.Ordinal)) + // { + // delList.Add(entry); + // } + //} - foreach (var entry in delList) - { - _ = ConfigManager.Settings.FileColors.Remove(entry); - } + //foreach (var entry in delList) + //{ + // _ = ConfigManager.Settings.FileColors.Remove(entry); + //} - ConfigManager.Settings.FileColors.Add(new ColorEntry(logWindow.FileName, dlg.Color)); + //ConfigManager.Settings.FileColors.Add(new ColorEntry(logWindow.FileName, dlg.Color)); - while (ConfigManager.Settings.FileColors.Count > MAX_COLOR_HISTORY) - { - ConfigManager.Settings.FileColors.RemoveAt(0); - } + //while (ConfigManager.Settings.FileColors.Count > MAX_COLOR_HISTORY) + //{ + // ConfigManager.Settings.FileColors.RemoveAt(0); + //} } [SupportedOSPlatform("windows")] diff --git a/src/LogExpert.UI/Dialogs/MissingFileItem.cs b/src/LogExpert.UI/Dialogs/MissingFileItem.cs new file mode 100644 index 00000000..73c35497 --- /dev/null +++ b/src/LogExpert.UI/Dialogs/MissingFileItem.cs @@ -0,0 +1,61 @@ +namespace LogExpert.UI.Dialogs; + +/// +/// Represents a file item in the Missing Files Dialog ListView. +/// +public class MissingFileItem +{ + /// + /// Original file path from the session/project file. + /// + public string OriginalPath { get; set; } + + /// + /// Current status of the file. + /// + public FileStatus Status { get; set; } + + /// + /// List of alternative paths that might be the same file. + /// + public List Alternatives { get; set; } = []; + + /// + /// Currently selected path (original or alternative). + /// + public string SelectedPath { get; set; } + + /// + /// Indicates whether the file is accessible. + /// + public bool IsAccessible => Status is FileStatus.Valid or FileStatus.AlternativeSelected; + + /// + /// Gets the display name for the ListView (just the filename). + /// + public string DisplayName => Path.GetFileName(OriginalPath) ?? OriginalPath; + + /// + /// Gets the status text for display. + /// + public string StatusText => Status switch + { + FileStatus.Valid => "Found", + FileStatus.MissingWithAlternatives => $"Missing ({Alternatives.Count} alternatives)", + FileStatus.Missing => "Missing", + FileStatus.AlternativeSelected => "Alternative Selected", + _ => "Unknown" + }; + + /// + /// Constructor for MissingFileItem. + /// + /// Original path from session file + /// Current file status + public MissingFileItem (string originalPath, FileStatus status) + { + OriginalPath = originalPath; + Status = status; + SelectedPath = originalPath; + } +} diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialog.Designer.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialog.Designer.cs new file mode 100644 index 00000000..1ae13b31 --- /dev/null +++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.Designer.cs @@ -0,0 +1,311 @@ +using System.Runtime.Versioning; + +namespace LogExpert.UI.Dialogs; + +[SupportedOSPlatform("windows")] +partial class MissingFilesDialog +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + components?.Dispose(); + imageListStatus?.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + + listViewFiles = new ListView(); + columnFileName = new ColumnHeader(); + columnStatus = new ColumnHeader(); + columnPath = new ColumnHeader(); + buttonLoadAndUpdate = new Button(); + buttonLoad = new Button(); + buttonBrowse = new Button(); + buttonCancel = new Button(); + labelInfo = new Label(); + labelSummary = new Label(); + imageListStatus = new ImageList(components); + panelButtons = new Panel(); + panelTop = new Panel(); + panelLayoutOptions = new Panel(); + labelLayoutInfo = new Label(); + radioButtonCloseTabs = new RadioButton(); + radioButtonNewWindow = new RadioButton(); + radioButtonIgnoreLayout = new RadioButton(); + buttonLayoutPanel = new FlowLayoutPanel(); + + SuspendLayout(); + panelLayoutOptions.SuspendLayout(); + panelTop.SuspendLayout(); + panelButtons.SuspendLayout(); + + // + // imageListStatus + // + imageListStatus.ColorDepth = ColorDepth.Depth32Bit; + imageListStatus.ImageSize = new Size(16, 16); + CreateStatusIcons(); + + // + // panelTop + // + panelTop.Controls.Add(labelSummary); + panelTop.Controls.Add(labelInfo); + panelTop.Dock = DockStyle.Top; + panelTop.Height = 80; + panelTop.Padding = new Padding(10); + panelTop.TabIndex = 0; + + // + // labelInfo + // + labelInfo.AutoSize = false; + labelInfo.Dock = DockStyle.Top; + labelInfo.Height = 40; + labelInfo.Text = "Some files from the session could not be found. You can browse for missing files or load only the files that were found."; + labelInfo.TextAlign = ContentAlignment.MiddleLeft; + labelInfo.TabIndex = 0; + + // + // labelSummary + // + labelSummary.AutoSize = false; + labelSummary.Dock = DockStyle.Top; + labelSummary.Font = new Font(Font, FontStyle.Bold); + labelSummary.Height = 30; + labelSummary.TextAlign = ContentAlignment.MiddleLeft; + labelSummary.TabIndex = 1; + + // + // labelLayoutInfo + // + labelLayoutInfo.AutoSize = false; + labelLayoutInfo.Location = new Point(10, 5); + labelLayoutInfo.Size = new Size(400, 25); + labelLayoutInfo.Text = "This session contains layout data. How would you like to proceed?"; + labelLayoutInfo.TextAlign = ContentAlignment.MiddleLeft; + labelLayoutInfo.TabIndex = 0; + + // + // radioButtonCloseTabs + // + radioButtonCloseTabs.AutoSize = true; + radioButtonCloseTabs.Checked = true; + radioButtonCloseTabs.Location = new Point(10, 35); + radioButtonCloseTabs.Name = "radioButtonCloseTabs"; + radioButtonCloseTabs.Size = new Size(200, 24); + radioButtonCloseTabs.TabIndex = 1; + radioButtonCloseTabs.TabStop = true; + radioButtonCloseTabs.Text = "Close existing tabs and restore layout"; + radioButtonCloseTabs.UseVisualStyleBackColor = true; + + // + // radioButtonNewWindow + // + radioButtonNewWindow.AutoSize = true; + radioButtonNewWindow.Location = new Point(10, 60); + radioButtonNewWindow.Name = "radioButtonNewWindow"; + radioButtonNewWindow.Size = new Size(200, 24); + radioButtonNewWindow.TabIndex = 2; + radioButtonNewWindow.Text = "Open in a new window"; + radioButtonNewWindow.UseVisualStyleBackColor = true; + + // + // radioButtonIgnoreLayout + // + radioButtonIgnoreLayout.AutoSize = true; + radioButtonIgnoreLayout.Location = new Point(10, 85); + radioButtonIgnoreLayout.Name = "radioButtonIgnoreLayout"; + radioButtonIgnoreLayout.Size = new Size(200, 24); + radioButtonIgnoreLayout.TabIndex = 3; + radioButtonIgnoreLayout.Text = "Ignore layout data"; + radioButtonIgnoreLayout.UseVisualStyleBackColor = true; + + // + // panelLayoutOptions + // + panelLayoutOptions.Controls.Add(radioButtonIgnoreLayout); + panelLayoutOptions.Controls.Add(radioButtonNewWindow); + panelLayoutOptions.Controls.Add(radioButtonCloseTabs); + panelLayoutOptions.Controls.Add(labelLayoutInfo); + panelLayoutOptions.Dock = DockStyle.Bottom; + panelLayoutOptions.Height = 115; + panelLayoutOptions.TabIndex = 3; + panelLayoutOptions.Visible = false; + + // + // listViewFiles + // + listViewFiles.Columns.AddRange(columnFileName, columnStatus, columnPath); + listViewFiles.Dock = DockStyle.Fill; + listViewFiles.FullRowSelect = true; + listViewFiles.GridLines = true; + listViewFiles.MultiSelect = false; + listViewFiles.SmallImageList = imageListStatus; + listViewFiles.TabIndex = 1; + listViewFiles.View = View.Details; + listViewFiles.SelectedIndexChanged += OnListViewSelectedIndexChanged; + listViewFiles.DoubleClick += OnListViewDoubleClick; + + // + // columnFileName + // + columnFileName.Text = "File Name"; + columnFileName.Width = 200; + + // + // columnStatus + // + columnStatus.Text = "Status"; + columnStatus.Width = 150; + + // + // columnPath + // + columnPath.Text = "Path"; + columnPath.Width = 400; + + // + // buttonLoad + // + buttonLoad.AutoSize = true; + buttonLoad.Height = 30; + buttonLoad.Margin = new Padding(3); + buttonLoad.MinimumSize = new Size(100, 30); + buttonLoad.TabIndex = 0; + buttonLoad.Text = "Load Files"; + buttonLoad.UseVisualStyleBackColor = true; + buttonLoad.Click += OnButtonLoadClick; + + // + // buttonBrowse + // + buttonBrowse.AutoSize = true; + buttonBrowse.Enabled = false; + buttonBrowse.Height = 30; + buttonBrowse.Margin = new Padding(3); + buttonBrowse.MinimumSize = new Size(100, 30); + buttonBrowse.TabIndex = 1; + buttonBrowse.Text = "Browse..."; + buttonBrowse.UseVisualStyleBackColor = true; + buttonBrowse.Click += OnButtonBrowseClick; + + // + // buttonLoadAndUpdate + // + buttonLoadAndUpdate.AutoSize = true; + buttonLoadAndUpdate.Enabled = false; + buttonLoadAndUpdate.Height = 30; + buttonLoadAndUpdate.Margin = new Padding(3); + buttonLoadAndUpdate.MinimumSize = new Size(150, 30); + buttonLoadAndUpdate.TabIndex = 2; + buttonLoadAndUpdate.Text = "Load && Update Session"; + buttonLoadAndUpdate.UseVisualStyleBackColor = true; + buttonLoadAndUpdate.Click += OnButtonLoadAndUpdateClick; + + // + // buttonCancel + // + buttonCancel.AutoSize = true; + buttonCancel.DialogResult = DialogResult.Cancel; + buttonCancel.Height = 30; + buttonCancel.Margin = new Padding(3); + buttonCancel.MinimumSize = new Size(100, 30); + buttonCancel.TabIndex = 3; + buttonCancel.Text = "Cancel"; + buttonCancel.UseVisualStyleBackColor = true; + buttonCancel.Click += OnButtonCancelClick; + + // + // buttonLayoutPanel + // + buttonLayoutPanel.AutoSize = true; + buttonLayoutPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink; + buttonLayoutPanel.Controls.Add(buttonLoad); + buttonLayoutPanel.Controls.Add(buttonBrowse); + buttonLayoutPanel.Controls.Add(buttonLoadAndUpdate); + buttonLayoutPanel.Controls.Add(buttonCancel); + buttonLayoutPanel.Dock = DockStyle.Right; + buttonLayoutPanel.FlowDirection = FlowDirection.LeftToRight; + buttonLayoutPanel.Location = new Point(0, 10); + buttonLayoutPanel.Padding = new Padding(10, 10, 10, 10); + buttonLayoutPanel.TabIndex = 0; + buttonLayoutPanel.WrapContents = false; + + // + // panelButtons + // + panelButtons.Controls.Add(buttonLayoutPanel); + panelButtons.Dock = DockStyle.Bottom; + panelButtons.Height = 60; + panelButtons.TabIndex = 2; + + // + // MissingFilesDialog + // + AcceptButton = buttonLoad; + AutoScaleDimensions = new SizeF(96F, 96F); + AutoScaleMode = AutoScaleMode.Dpi; + CancelButton = buttonCancel; + ClientSize = new Size(840, 500); + Controls.Add(listViewFiles); + Controls.Add(panelLayoutOptions); + Controls.Add(panelButtons); + Controls.Add(panelTop); + FormBorderStyle = FormBorderStyle.Sizable; + MinimumSize = new Size(600, 400); + ShowIcon = false; + ShowInTaskbar = false; + StartPosition = FormStartPosition.CenterParent; + Text = "Missing Files"; + + panelLayoutOptions.ResumeLayout(false); + panelLayoutOptions.PerformLayout(); + panelTop.ResumeLayout(false); + panelButtons.ResumeLayout(false); + panelButtons.PerformLayout(); + ResumeLayout(false); + } + + #endregion + + private ListView listViewFiles; + private ColumnHeader columnFileName; + private ColumnHeader columnStatus; + private ColumnHeader columnPath; + private Button buttonLoad; + private Button buttonLoadAndUpdate; + private Button buttonBrowse; + private Button buttonCancel; + private Label labelInfo; + private Label labelSummary; + private ImageList imageListStatus; + private Panel panelButtons; + private Panel panelTop; + private Panel panelLayoutOptions; + private Label labelLayoutInfo; + private RadioButton radioButtonCloseTabs; + private RadioButton radioButtonNewWindow; + private RadioButton radioButtonIgnoreLayout; + private FlowLayoutPanel buttonLayoutPanel; +} diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs new file mode 100644 index 00000000..04d8b34d --- /dev/null +++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs @@ -0,0 +1,421 @@ +using System.Globalization; +using System.Runtime.Versioning; + +using LogExpert.Core.Classes.Persister; + +namespace LogExpert.UI.Dialogs; + +/// +/// Enhanced dialog for handling missing files with browsing and alternative selection. +/// Also handles layout restoration options when loading a project with existing tabs. +/// Phase 2 implementation of the Project File Validator. +/// +[SupportedOSPlatform("windows")] +public partial class MissingFilesDialog : Form +{ + #region Fields + + private readonly ProjectValidationResult _validationResult; + private readonly Dictionary _fileItems; + private readonly bool _hasLayoutData; + + #endregion + + #region Properties + + /// + /// Gets the dialog result indicating the user's choice. + /// + public MissingFilesDialogResult Result { get; private set; } + + /// + /// Gets whether the user wants to update the session file. + /// + public bool UpdateSessionFile { get; private set; } + + /// + /// Gets the dictionary of selected alternative paths for missing files. + /// Key: original path, Value: selected alternative path + /// + public Dictionary SelectedAlternatives { get; private set; } + + #endregion + + #region Constructor + + /// + /// Constructor for MissingFilesDialog. + /// + /// Validation result containing file information + /// Whether to show layout restoration options + /// Whether the project has layout data to restore + public MissingFilesDialog (ProjectValidationResult validationResult, bool hasLayoutData = false) + { + ArgumentNullException.ThrowIfNull(validationResult); + + _validationResult = validationResult; + _fileItems = []; + SelectedAlternatives = []; + Result = MissingFilesDialogResult.Cancel; + UpdateSessionFile = false; + _hasLayoutData = hasLayoutData; + + InitializeComponent(); + InitializeFileItems(); + PopulateListView(); + UpdateSummary(); + ConfigureLayoutOptions(); + } + + #endregion + + #region Public Methods + + /// + /// Shows the dialog with layout options and returns alternatives if selected. + /// + /// Validation result + /// Whether the project has layout data + /// Tuple containing the dialog result, whether to update session file, and selected alternatives + public static (MissingFilesDialogResult Result, bool UpdateSessionFile, Dictionary SelectedAlternatives) ShowDialog (ProjectValidationResult validationResult, bool hasLayoutData) + { + using var dialog = new MissingFilesDialog(validationResult, hasLayoutData); + _ = dialog.ShowDialog(); + return (dialog.Result, dialog.UpdateSessionFile, dialog.SelectedAlternatives); + } + + #endregion + + #region Private Methods + + /// + /// Configures visibility and state of layout options panel. + /// + private void ConfigureLayoutOptions () + { + Text = Resources.MissingFilesDialog_UI_Title; + + panelLayoutOptions.Visible = true; + labelLayoutInfo.Text = Resources.MissingFilesDialog_UI_Label_Informational; + radioButtonCloseTabs.Text = Resources.MissingFilesDialog_UI_Button_CloseTabs; + radioButtonNewWindow.Text = Resources.MissingFilesDialog_UI_Button_NewWindow; + radioButtonIgnoreLayout.Text = Resources.MissingFilesDialog_UI_Button_Ignore; + radioButtonCloseTabs.Checked = true; + panelLayoutOptions.BringToFront(); + Height += panelLayoutOptions.Height; + } + + /// + /// Creates status icons for the ImageList. + /// + private void CreateStatusIcons () + { + // Create simple colored circles as status indicators + + // Valid - Green circle + var validIcon = new Bitmap(16, 16); + using (var g = Graphics.FromImage(validIcon)) + { + g.Clear(Color.Transparent); + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + g.FillEllipse(Brushes.Green, 2, 2, 12, 12); + } + + imageListStatus.Images.Add("Valid", validIcon); + + // Missing - Red circle + var missingIcon = new Bitmap(16, 16); + using (var g = Graphics.FromImage(missingIcon)) + { + g.Clear(Color.Transparent); + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + g.FillEllipse(Brushes.Red, 2, 2, 12, 12); + } + + imageListStatus.Images.Add("Missing", missingIcon); + + // Alternative available - Orange circle + var alternativeIcon = new Bitmap(16, 16); + using (var g = Graphics.FromImage(alternativeIcon)) + { + g.Clear(Color.Transparent); + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + g.FillEllipse(Brushes.Orange, 2, 2, 12, 12); + } + + imageListStatus.Images.Add("Alternative", alternativeIcon); + + // Alternative selected - Blue circle + var selectedIcon = new Bitmap(16, 16); + using (var g = Graphics.FromImage(selectedIcon)) + { + g.Clear(Color.Transparent); + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + g.FillEllipse(Brushes.Blue, 2, 2, 12, 12); + } + + imageListStatus.Images.Add("Selected", selectedIcon); + } + + /// + /// Initializes the file items dictionary from validation result. + /// + private void InitializeFileItems () + { + // Add valid files + foreach (var validPath in _validationResult.ValidFiles) + { + var item = new MissingFileItem(validPath, FileStatus.Valid); + _fileItems[validPath] = item; + } + + // Add missing files + foreach (var missingPath in _validationResult.MissingFiles) + { + var alternatives = _validationResult.PossibleAlternatives.TryGetValue(missingPath, out List? value) + ? value + : []; + + var status = alternatives.Count > 0 + ? FileStatus.MissingWithAlternatives + : FileStatus.Missing; + + var item = new MissingFileItem(missingPath, status) + { + Alternatives = alternatives + }; + + _fileItems[missingPath] = item; + } + } + + /// + /// Populates the ListView with file items. + /// + private void PopulateListView () + { + listViewFiles.BeginUpdate(); + listViewFiles.Items.Clear(); + + foreach (var fileItem in _fileItems.Values) + { + var listItem = new ListViewItem(fileItem.DisplayName) + { + Tag = fileItem, + ImageKey = fileItem.Status switch + { + FileStatus.Valid => Resources.MissingFilesDialog_UI_FileStatus_Valid, + FileStatus.MissingWithAlternatives => Resources.MissingFilesDialog_UI_FileStatus_Alternative, + FileStatus.AlternativeSelected => Resources.MissingFilesDialog_UI_FileStatus_Selected, + FileStatus.Missing => Resources.MissingFilesDialog_UI_FileStatus_Missing, + _ => Resources.MissingFilesDialog_UI_FileStatus_Missing + } + }; + + _ = listItem.SubItems.Add(fileItem.StatusText); + _ = listItem.SubItems.Add(fileItem.SelectedPath); + + // Color code the row based on status + if (fileItem.Status == FileStatus.Missing) + { + listItem.ForeColor = Color.Red; + } + else if (fileItem.Status == FileStatus.MissingWithAlternatives) + { + listItem.ForeColor = Color.DarkOrange; + } + else if (fileItem.Status == FileStatus.AlternativeSelected) + { + listItem.ForeColor = Color.Blue; + } + + _ = listViewFiles.Items.Add(listItem); + } + + listViewFiles.EndUpdate(); + } + + /// + /// Updates the summary label and control states. + /// + private void UpdateSummary () + { + var validCount = _fileItems.Values.Count(f => f.IsAccessible); + var totalCount = _fileItems.Count; + var missingCount = totalCount - validCount; + + labelSummary.Text = string.Format(CultureInfo.InvariantCulture, Resources.MissingFilesDialog_UI_Label_Summary, validCount, totalCount, missingCount); + + // Enable "Load and Update Session" only if user has selected alternatives + var hasSelectedAlternatives = _fileItems.Values.Any(f => f.Status == FileStatus.AlternativeSelected); + buttonLoadAndUpdate.Enabled = hasSelectedAlternatives; + + // Update button text based on selection + if (hasSelectedAlternatives) + { + var alternativeCount = _fileItems.Values.Count(f => f.Status == FileStatus.AlternativeSelected); + buttonLoadAndUpdate.Text = string.Format(CultureInfo.InvariantCulture, Resources.MissingFilesDialog_UI_Button_UpdateSessionAlternativeCount, alternativeCount); + } + else + { + buttonLoadAndUpdate.Text = Resources.MissingFilesDialog_UI_Button_LoadUpdateSession; + } + } + + /// + /// Opens a file browser dialog for the specified missing file. + /// + /// The file item to browse for + private void BrowseForFile (MissingFileItem fileItem) + { + using var openFileDialog = new OpenFileDialog + { + Title = string.Format(CultureInfo.InvariantCulture, Resources.MissingFilesDialog_UI_Filter_Title, fileItem.DisplayName), + Filter = Resources.MissingFilesDialog_UI_Filter_Logfiles, + FileName = fileItem.DisplayName, + CheckFileExists = true, + Multiselect = false + }; + + // Try to set initial directory from original path + try + { + var directory = Path.GetDirectoryName(fileItem.OriginalPath); + if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory)) + { + openFileDialog.InitialDirectory = directory; + } + } + catch (Exception ex) when (ex is ArgumentException or + PathTooLongException or + NotSupportedException or + UnauthorizedAccessException) + { + // Ignore if path is invalid + } + + if (openFileDialog.ShowDialog(this) == DialogResult.OK) + { + // User selected a file + fileItem.SelectedPath = openFileDialog.FileName; + fileItem.Status = FileStatus.AlternativeSelected; + + // Store the alternative + SelectedAlternatives[fileItem.OriginalPath] = fileItem.SelectedPath; + + // Refresh the ListView + PopulateListView(); + UpdateSummary(); + } + } + + /// + /// Determines the appropriate layout result based on radio button selection. + /// + /// The layout-related result + private MissingFilesDialogResult DetermineLayoutResult () + { + // If layout options are not shown or there's no layout data, return LoadValidFiles + if (!_hasLayoutData || !panelLayoutOptions.Visible) + { + return MissingFilesDialogResult.LoadValidFiles; + } + + // Determine layout-related result + if (radioButtonCloseTabs.Checked) + { + return MissingFilesDialogResult.CloseTabsAndRestoreLayout; + } + else if (radioButtonNewWindow.Checked) + { + return MissingFilesDialogResult.OpenInNewWindow; + } + else if (radioButtonIgnoreLayout.Checked) + { + return MissingFilesDialogResult.IgnoreLayout; + } + + // Default to LoadValidFiles + return MissingFilesDialogResult.LoadValidFiles; + } + + #endregion + + #region Event Handlers + + private void OnListViewSelectedIndexChanged (object sender, EventArgs e) + { + if (listViewFiles.SelectedItems.Count > 0) + { + var selectedItem = listViewFiles.SelectedItems[0]; + var fileItem = selectedItem.Tag as MissingFileItem; + + // Enable browse button for any file that is not valid (allow browsing/re-browsing) + buttonBrowse.Enabled = fileItem?.Status is + FileStatus.Missing or + FileStatus.MissingWithAlternatives or + FileStatus.AlternativeSelected; + } + else + { + buttonBrowse.Enabled = false; + } + } + + private void OnListViewDoubleClick (object sender, EventArgs e) + { + // Double-click to browse for missing file + if (listViewFiles.SelectedItems.Count > 0) + { + var selectedItem = listViewFiles.SelectedItems[0]; + var fileItem = selectedItem.Tag as MissingFileItem; + + if (fileItem?.Status is + FileStatus.Missing or + FileStatus.MissingWithAlternatives or + FileStatus.AlternativeSelected) + { + BrowseForFile(fileItem); + } + } + } + + private void OnButtonBrowseClick (object sender, EventArgs e) + { + if (listViewFiles.SelectedItems.Count > 0) + { + var selectedItem = listViewFiles.SelectedItems[0]; + + if (selectedItem.Tag is MissingFileItem fileItem) + { + BrowseForFile(fileItem); + } + } + } + + private void OnButtonLoadClick (object sender, EventArgs e) + { + Result = DetermineLayoutResult(); + UpdateSessionFile = false; + DialogResult = DialogResult.OK; + Close(); + } + + private void OnButtonLoadAndUpdateClick (object sender, EventArgs e) + { + Result = DetermineLayoutResult(); + UpdateSessionFile = true; + DialogResult = DialogResult.OK; + Close(); + } + + private void OnButtonCancelClick (object sender, EventArgs e) + { + Result = MissingFilesDialogResult.Cancel; + UpdateSessionFile = false; + DialogResult = DialogResult.Cancel; + Close(); + } + + #endregion +} \ No newline at end of file diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialog.resx b/src/LogExpert.UI/Dialogs/MissingFilesDialog.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs new file mode 100644 index 00000000..d0ada4ec --- /dev/null +++ b/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs @@ -0,0 +1,55 @@ +namespace LogExpert.UI.Dialogs; + +/// +/// Represents the result of the Missing Files Dialog interaction. +/// +public enum MissingFilesDialogResult +{ + /// + /// User cancelled the operation. + /// + Cancel, + + /// + /// Load only the valid files that were found. + /// + LoadValidFiles, + + /// + /// Load valid files and update the session file with new paths. + /// + LoadAndUpdateSession, + + /// + /// Close existing tabs before loading the project with layout restoration. + /// Used when there are existing tabs open and the project has layout data. + /// + CloseTabsAndRestoreLayout, + + /// + /// Open the project in a new window. + /// Used when there are existing tabs open and the project has layout data. + /// + OpenInNewWindow, + + /// + /// Ignore the layout data and just load the files. + /// Used when there are existing tabs open and the project has layout data. + /// + IgnoreLayout, + + /// + /// Show a message box with information about the missing files. + /// + ShowMissingFilesMessage, + + /// + /// Retry loading the files after resolving the issues. + /// + RetryLoadFiles, + + /// + /// Skip the missing files and continue with the operation. + /// + SkipMissingFiles +} diff --git a/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs b/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs new file mode 100644 index 00000000..da39de1f --- /dev/null +++ b/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs @@ -0,0 +1,56 @@ +using System.Runtime.Versioning; +using System.Text; + +using LogExpert.Core.Classes.Persister; + +namespace LogExpert.UI.Dialogs; + +/// +/// Temporary helper for showing missing file alerts until full dialog is implemented. +/// This provides a simple MessageBox-based notification system for Phase 1 of the implementation. +/// +[SupportedOSPlatform("windows")] +internal static class MissingFilesMessageBox +{ + /// + /// Shows a message box alerting the user about missing files from a project/session. + /// + /// The validation result containing missing file information + /// True if user wants to continue loading valid files, false to cancel + public static bool Show (ProjectValidationResult validationResult) + { + ArgumentNullException.ThrowIfNull(validationResult); + + var sb = new StringBuilder(); + _ = sb.AppendLine("Some files from the session could not be found:"); + _ = sb.AppendLine(); + + // Show first 10 missing files + var displayCount = Math.Min(10, validationResult.MissingFiles.Count); + for (var i = 0; i < displayCount; i++) + { + var missing = validationResult.MissingFiles[i]; + _ = sb.AppendLine($" • {Path.GetFileName(missing)}"); + } + + // If there are more than 10, show count of remaining + if (validationResult.MissingFiles.Count > 10) + { + _ = sb.AppendLine($" ... and {validationResult.MissingFiles.Count - 10} more"); + } + + _ = sb.AppendLine(); + var totalFiles = validationResult.ValidFiles.Count + validationResult.MissingFiles.Count; + _ = sb.AppendLine($"Found: {validationResult.ValidFiles.Count} of {totalFiles} files"); + _ = sb.AppendLine(); + _ = sb.AppendLine("Do you want to load the files that were found?"); + + var result = MessageBox.Show( + sb.ToString(), + "Missing Files", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + + return result == DialogResult.Yes; + } +} diff --git a/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs b/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs index 304b2e3d..5b0d78ff 100644 --- a/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs +++ b/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs @@ -30,12 +30,12 @@ public ProjectLoadDlg () private void ApplyResources () { - Text = Resources.ProjectLoadDlg_UI_Title; - labelInformational.Text = Resources.ProjectLoadDlg_UI_Label_Informational; - labelChooseHowToProceed.Text = Resources.ProjectLoadDlg_UI_Label_ChooseHowToProceed; - buttonCloseTabs.Text = Resources.ProjectLoadDlg_UI_Button_CloseTabs; - buttonNewWindow.Text = Resources.ProjectLoadDlg_UI_Button_NewWindow; - buttonIgnore.Text = Resources.ProjectLoadDlg_UI_Button_Ignore; + Text = Resources.MissingFilesDialog_UI_Title; + labelInformational.Text = Resources.MissingFilesDialog_UI_Label_Informational; + labelChooseHowToProceed.Text = Resources.MissingFilesDialog_UI_Label_ChooseHowToProceed; + buttonCloseTabs.Text = Resources.MissingFilesDialog_UI_Button_CloseTabs; + buttonNewWindow.Text = Resources.MissingFilesDialog_UI_Button_NewWindow; + buttonIgnore.Text = Resources.MissingFilesDialog_UI_Button_Ignore; } #endregion diff --git a/src/PluginRegistry/FileSystem/LocalFileSystem.cs b/src/PluginRegistry/FileSystem/LocalFileSystem.cs index c684bb87..262f33df 100644 --- a/src/PluginRegistry/FileSystem/LocalFileSystem.cs +++ b/src/PluginRegistry/FileSystem/LocalFileSystem.cs @@ -21,6 +21,12 @@ ArgumentNullException or } } + /// + /// Retrieves information about a log file specified by a file URI. + /// + /// The URI string that identifies the log file. Must be a valid file URI. + /// An object that provides information about the specified log file. + /// Thrown if the provided URI string does not represent a file URI. public ILogFileInfo GetLogfileInfo (string uriString) { Uri uri = new(uriString); diff --git a/src/PluginRegistry/FileSystem/LogFileInfo.cs b/src/PluginRegistry/FileSystem/LogFileInfo.cs index ef7e12c5..bfcdbe00 100644 --- a/src/PluginRegistry/FileSystem/LogFileInfo.cs +++ b/src/PluginRegistry/FileSystem/LogFileInfo.cs @@ -10,7 +10,7 @@ public class LogFileInfo : ILogFileInfo private const int RETRY_COUNT = 5; private const int RETRY_SLEEP = 250; - private static readonly ILogger _logger = LogManager.GetCurrentClassLogger(); + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); //FileStream fStream; private readonly FileInfo fInfo; @@ -21,7 +21,7 @@ public class LogFileInfo : ILogFileInfo #region cTor - public LogFileInfo(Uri fileUri) + public LogFileInfo (Uri fileUri) { fInfo = new FileInfo(fileUri.LocalPath); Uri = fileUri; @@ -37,7 +37,6 @@ public LogFileInfo(Uri fileUri) public string FileName => fInfo.Name; - public string DirectoryName => fInfo.DirectoryName; public char DirectorySeparatorChar => Path.DirectorySeparatorChar; @@ -124,7 +123,7 @@ public long LengthWithoutRetry /// rollover situations. /// /// - public Stream OpenStream() + public Stream OpenStream () { var retry = RETRY_COUNT; @@ -158,7 +157,7 @@ public Stream OpenStream() } //TODO Replace with Event from FileSystemWatcher - public bool FileHasChanged() + public bool FileHasChanged () { if (LengthWithoutRetry != lastLength) { @@ -169,7 +168,7 @@ public bool FileHasChanged() return false; } - public override string ToString() + public override string ToString () { return fInfo.FullName + ", OldLen: " + OriginalLength + ", Len: " + Length; } diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs index e5d5109a..7a878dc0 100644 --- a/src/PluginRegistry/PluginHashGenerator.Generated.cs +++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs @@ -10,7 +10,7 @@ public static partial class PluginValidator { /// /// Gets pre-calculated SHA256 hashes for built-in plugins. - /// Generated: 2025-12-12 22:08:58 UTC + /// Generated: 2025-12-16 15:51:20 UTC /// Configuration: Release /// Plugin count: 22 /// @@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { - ["AutoColumnizer.dll"] = "9BBF1F553FD9E5A9F669F107232985F48D93C6D132069464700201247CEDE5DD", + ["AutoColumnizer.dll"] = "FBFCB4C9FEBF8DA0DDB1822BDF7CFCADF1185574917D8705C597864D1ACBFE0C", ["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", ["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6", - ["CsvColumnizer.dll"] = "B67093810D0144BAC80E70F981590AEF5E341C13852F151633345B5D1C4A33E5", - ["CsvColumnizer.dll (x86)"] = "B67093810D0144BAC80E70F981590AEF5E341C13852F151633345B5D1C4A33E5", - ["DefaultPlugins.dll"] = "76ABF37B8C9DD574EE6D9C42860D4D60818EA0407B6B3EBB03F31B33C8DCC50A", - ["FlashIconHighlighter.dll"] = "CDDD76BC56EAFDB6D5003E3EFC80122F4D20BE05F59F71FB094127C5FE02D700", - ["GlassfishColumnizer.dll"] = "198BECCD93612FC5041EE37E5E69E2352AC7A18416EAA7E205FB99133AB8261C", - ["JsonColumnizer.dll"] = "9E801A0C414CF6512087730906CDD9759908CD78CAB547B55C4DE16727B10922", - ["JsonCompactColumnizer.dll"] = "70EABBEA5CA5B32255CFB02C87B6512CE9C3B20B21F444DC5716E3F8A7512FD0", - ["Log4jXmlColumnizer.dll"] = "6F64839262E7DBEF08000812D84FC591965835B74EF8551C022E827070A136A0", - ["LogExpert.Core.dll"] = "F07BD482E92D8E17669C71F020BD392979177BE8E4F43AE3F4EC544411EB849E", - ["LogExpert.Resources.dll"] = "ED0E7ABC183982C10D7F48ECEA12B3AA4F2D518298E6BFB168F2E0921EF063DB", + ["CsvColumnizer.dll"] = "672B48664FDBDB7E71A01AEE6704D34544257B6A977E01EC0A514FAEE030E982", + ["CsvColumnizer.dll (x86)"] = "672B48664FDBDB7E71A01AEE6704D34544257B6A977E01EC0A514FAEE030E982", + ["DefaultPlugins.dll"] = "53FA9690E55C7A1C90601CCCCA4FFC808BB783DFAA1FDF42EB924DF3CDB65D40", + ["FlashIconHighlighter.dll"] = "AC101CC62538DB565C462D5A9A6EAA57D85AEA4954FAAEF271FEAC7F98FD1CB1", + ["GlassfishColumnizer.dll"] = "915BDD327C531973F3355BC5B24AE70B0EA618269E9367DA4FA919B4BA08D171", + ["JsonColumnizer.dll"] = "8A8B8021A4D146424947473A5FC2F0C07248BD4FAC079C00E586ADAA153CA9ED", + ["JsonCompactColumnizer.dll"] = "6B26775A81C6FC4232C08BBAFDCE35A0EFA80A367FC00271DF00E89AF2F74802", + ["Log4jXmlColumnizer.dll"] = "B645973F639B23A51D066DC283A92E3BDAF058934C6C6378B6F13247B75077CB", + ["LogExpert.Core.dll"] = "A370D1AEF2A790D16622CD15C67270799A87555996A7A0D0A4D98B950B9C3CB7", + ["LogExpert.Resources.dll"] = "1D761591625ED49FD0DE29ABDB2800D865BD99B359397CF7790004B3CC9E6738", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93", ["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", ["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D", - ["RegexColumnizer.dll"] = "A4C92D2E70491F3D65CDA46106F08277562950800FB0DE8230201CE129F5FB5C", - ["SftpFileSystem.dll"] = "D9643E14F3CF02F849BA12618D6F092ABEA07724ED9837F858AD88317E28B4C8", - ["SftpFileSystem.dll (x86)"] = "E1B167E38290E50E5EDC0754804792425363DA8EC7C38645C7AFA1ECB7118157", - ["SftpFileSystem.Resources.dll"] = "445FEAD03A0B535813A737A41B0C62C9E9341578FD3EACA3309766683B774561", - ["SftpFileSystem.Resources.dll (x86)"] = "445FEAD03A0B535813A737A41B0C62C9E9341578FD3EACA3309766683B774561", + ["RegexColumnizer.dll"] = "DD44507671520B3E2CD93311A651EFC7632C757FDA85750470A6FFCB74F1AB65", + ["SftpFileSystem.dll"] = "16818A6B8B7178372CCB1CD6ED8C4B0542FDD7D10C8C9AAE76704F0ED2CC4362", + ["SftpFileSystem.dll (x86)"] = "94B725554D2BBF79BBB0B4776C01AE0D1E8F06C158F5D234C2D94F5E62A900BC", + ["SftpFileSystem.Resources.dll"] = "FEFD8C274032A70FDB03EA8363D7F877BEBAF2C454238820D6D17ECEF79FC8A4", + ["SftpFileSystem.Resources.dll (x86)"] = "FEFD8C274032A70FDB03EA8363D7F877BEBAF2C454238820D6D17ECEF79FC8A4", }; }