Skip to content

Commit 0fdf7f7

Browse files
committed
ConfigManager to its own project
1 parent 8061d8c commit 0fdf7f7

File tree

12 files changed

+1165
-971
lines changed

12 files changed

+1165
-971
lines changed

src/LogExpert/Config/ConfigManager.cs renamed to src/LogExpert.Configuration/ConfigManager.cs

Lines changed: 989 additions & 942 deletions
Large diffs are not rendered by default.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<EnableWindowsTargeting>true</EnableWindowsTargeting>
6+
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<RootNamespace>LogExpert.Configuration</RootNamespace>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\LogExpert.Core\LogExpert.Core.csproj" />
14+
</ItemGroup>
15+
16+
</Project>

src/LogExpert.Core/Classes/JsonConverters/ColumnizerJsonConverter.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,6 @@ private static Type FindColumnizerTypeByName (string name)
136136
// Search all loaded assemblies for a type implementing ILogLineColumnizer with matching GetName()
137137
foreach (var currentAssembly in AppDomain.CurrentDomain.GetAssemblies())
138138
{
139-
var tempList = currentAssembly.GetTypes().Where(t => typeof(ILogLineColumnizer).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
140-
141-
var test = string.Empty;
142-
143139
foreach (var type in currentAssembly.GetTypes().Where(t => typeof(ILogLineColumnizer).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract))
144140
{
145141
try
@@ -200,7 +196,6 @@ private static Type FindColumnizerTypeByAssemblyQualifiedName (string assemblyQu
200196
// Fallback: try to find by simple type name (without namespace) across all assemblies
201197
var simpleTypeName = typeName.Contains('.', StringComparison.OrdinalIgnoreCase) ? typeName[(typeName.LastIndexOf('.') + 1)..] : typeName;
202198

203-
204199
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
205200
{
206201
foreach (var type in assembly.GetTypes().Where(t =>
Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,145 @@
1+
using System.Drawing;
2+
13
using LogExpert.Core.Config;
24
using LogExpert.Core.EventArguments;
35

46
namespace LogExpert.Core.Interface;
57

6-
//TODO: Add documentation
8+
/// <summary>
9+
/// Manages application configuration settings including loading, saving, importing, and exporting.
10+
/// Provides centralized access to application settings with automatic backup/recovery and validation.
11+
/// </summary>
12+
/// <remarks>
13+
/// This interface defines the contract for configuration management in LogExpert.
14+
/// Implementations use a singleton pattern and require explicit initialization via <see cref="Initialize"/>
15+
/// before accessing any settings. The manager handles JSON serialization, backup file creation,
16+
/// corruption recovery, and thread-safe operations.
17+
/// </remarks>
718
public interface IConfigManager
819
{
20+
/// <summary>
21+
/// Gets the current application settings.
22+
/// </summary>
23+
/// <remarks>
24+
/// Settings are lazy-loaded on first access. The manager must be initialized via
25+
/// <see cref="Initialize"/> before accessing this property.
26+
/// </remarks>
27+
/// <exception cref="InvalidOperationException">Thrown if accessed before initialization.</exception>
928
Settings Settings { get; }
1029

30+
/// <summary>
31+
/// Gets the directory path for portable mode settings.
32+
/// </summary>
33+
/// <remarks>
34+
/// Returns the application startup path combined with "portable" subdirectory.
35+
/// When a portableMode.json file exists in this directory, the application runs in portable mode.
36+
/// </remarks>
1137
string PortableModeDir { get; }
1238

39+
/// <summary>
40+
/// Gets the standard configuration directory path.
41+
/// </summary>
42+
/// <remarks>
43+
/// Returns the path to the AppData\Roaming\LogExpert directory where settings are stored
44+
/// when not running in portable mode.
45+
/// </remarks>
1346
string ConfigDir { get; }
1447

15-
IConfigManager Instance { get; }
16-
48+
/// <summary>
49+
/// Gets the filename for the portable mode indicator file.
50+
/// </summary>
51+
/// <value>Returns "portableMode.json"</value>
1752
string PortableModeSettingsFileName { get; }
1853

54+
/// <summary>
55+
/// Initializes the ConfigManager with application-specific paths and screen information.
56+
/// This method must be called once before accessing Settings or other configuration.
57+
/// </summary>
58+
/// <param name="applicationStartupPath">The application startup path (e.g., Application.StartupPath)</param>
59+
/// <param name="virtualScreenBounds">The virtual screen bounds (e.g., SystemInformation.VirtualScreen)</param>
60+
/// <remarks>
61+
/// This method should be called early in the application startup sequence, before any
62+
/// settings access. Subsequent calls are ignored with a warning logged.
63+
/// The virtual screen bounds are used to validate and correct window positions.
64+
/// </remarks>
65+
/// <exception cref="ArgumentException">Thrown if applicationStartupPath is null or whitespace.</exception>
66+
void Initialize (string applicationStartupPath, Rectangle virtualScreenBounds);
67+
68+
/// <summary>
69+
/// Exports specific settings to a file based on the provided flags.
70+
/// </summary>
71+
/// <param name="fileInfo">The file to export settings to. Will be created or overwritten.</param>
72+
/// <param name="highlightSettings">Flags indicating which settings to export (e.g., SettingsFlags.HighlightSettings)</param>
73+
/// <remarks>
74+
/// Currently only supports exporting highlight settings. Other flags may be ignored.
75+
/// The file is written in JSON format.
76+
/// </remarks>
77+
/// <exception cref="IOException">Thrown if the file cannot be written.</exception>
1978
void Export (FileInfo fileInfo, SettingsFlags highlightSettings);
2079

80+
/// <summary>
81+
/// Exports all current settings to a file.
82+
/// </summary>
83+
/// <param name="fileInfo">The file to export settings to. Will be created or overwritten.</param>
84+
/// <remarks>
85+
/// Exports the complete settings object including preferences, filters, history, and highlights.
86+
/// A backup (.bak) file is created if the target file already exists.
87+
/// </remarks>
88+
/// <exception cref="IOException">Thrown if the file cannot be written.</exception>
89+
/// <exception cref="InvalidOperationException">Thrown if settings validation fails.</exception>
2190
void Export (FileInfo fileInfo);
2291

92+
/// <summary>
93+
/// Imports settings from a file with validation and user confirmation support.
94+
/// </summary>
95+
/// <param name="fileInfo">The file to import settings from. Must exist.</param>
96+
/// <param name="importFlags">Flags controlling which parts of settings to import and how to merge them</param>
97+
/// <returns>
98+
/// An <see cref="ImportResult"/> indicating success, failure, or need for user confirmation.
99+
/// Check <see cref="ImportResult.Success"/> and <see cref="ImportResult.RequiresUserConfirmation"/> to determine the outcome.
100+
/// </returns>
101+
/// <remarks>
102+
/// This method validates the import file before applying changes. It detects corrupted files,
103+
/// empty/default settings, and handles backup recovery. If the import file appears empty,
104+
/// it returns a result requiring user confirmation to prevent accidental data loss.
105+
/// The current settings are only modified if the import is successful.
106+
/// </remarks>
23107
ImportResult Import (FileInfo fileInfo, ExportImportFlags importFlags);
24108

109+
/// <summary>
110+
/// Imports only highlight settings from a file.
111+
/// </summary>
112+
/// <param name="fileInfo">The file containing highlight settings to import. Must exist and contain valid highlight groups.</param>
113+
/// <param name="importFlags">Flags controlling whether to keep existing highlights or replace them</param>
114+
/// <remarks>
115+
/// This is a specialized import method for highlight configurations. If <see cref="ExportImportFlags.KeepExisting"/>
116+
/// is set, imported highlights are added to existing ones; otherwise, existing highlights are replaced.
117+
/// Changes are saved immediately after import.
118+
/// </remarks>
119+
/// <exception cref="ArgumentNullException">Thrown if fileInfo is null.</exception>
25120
void ImportHighlightSettings (FileInfo fileInfo, ExportImportFlags importFlags);
26121

122+
/// <summary>
123+
/// Occurs when configuration settings are changed and saved.
124+
/// </summary>
125+
/// <remarks>
126+
/// This event is raised after settings are successfully saved, allowing UI components and other
127+
/// parts of the application to respond to configuration changes. The event args include
128+
/// <see cref="SettingsFlags"/> indicating which settings were modified.
129+
/// </remarks>
27130
event EventHandler<ConfigChangedEventArgs> ConfigChanged; //TODO: All handlers that are public shoulld be in Core
28131

132+
/// <summary>
133+
/// Saves the current settings with the specified flags.
134+
/// </summary>
135+
/// <param name="flags">Flags indicating which parts of settings have changed and should be saved</param>
136+
/// <remarks>
137+
/// This method saves settings to disk with automatic backup creation. A temporary file is used
138+
/// during the write operation to prevent corruption. The previous settings file is backed up
139+
/// as .bak before being replaced. After successful save, the <see cref="ConfigChanged"/> event is raised.
140+
/// Settings validation is performed before saving to prevent data loss.
141+
/// </remarks>
142+
/// <exception cref="InvalidOperationException">Thrown if settings validation fails.</exception>
143+
/// <exception cref="IOException">Thrown if the file cannot be written.</exception>
29144
void Save (SettingsFlags flags);
30145
}

src/LogExpert.Tests/ConfigManagerTest.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.Reflection;
22

3-
using LogExpert.Config;
3+
using LogExpert.Configuration;
44
using LogExpert.Core.Classes.Filter;
55
using LogExpert.Core.Config;
66
using LogExpert.Core.Entities;
@@ -20,6 +20,7 @@ public class ConfigManagerTest
2020
{
2121
private string _testDir;
2222
private FileInfo _testSettingsFile;
23+
private ConfigManager _configManager;
2324

2425
[SetUp]
2526
public void SetUp ()
@@ -28,6 +29,10 @@ public void SetUp ()
2829
_testDir = Path.Join(Path.GetTempPath(), "LogExpert_Test_" + Guid.NewGuid().ToString("N"));
2930
_ = Directory.CreateDirectory(_testDir);
3031
_testSettingsFile = new FileInfo(Path.Join(_testDir, "settings.json"));
32+
33+
// Initialize ConfigManager for testing
34+
_configManager = ConfigManager.Instance;
35+
_configManager.Initialize(_testDir, new Rectangle(0, 0, 1920, 1080));
3136
}
3237

3338
[TearDown]
@@ -74,7 +79,7 @@ private T InvokePrivateInstanceMethod<T> (string methodName, params object[] par
7479

7580
return method == null
7681
? throw new Exception($"Instance method {methodName} not found")
77-
: (T)method.Invoke(ConfigManager.Instance, parameters);
82+
: (T)method.Invoke(_configManager, parameters);
7883
}
7984

8085
/// <summary>
@@ -85,7 +90,7 @@ private void InvokePrivateInstanceMethod (string methodName, params object[] par
8590
MethodInfo? method = typeof(ConfigManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)
8691
?? throw new Exception($"Instance method {methodName} not found");
8792

88-
method.Invoke(ConfigManager.Instance, parameters);
93+
method.Invoke(_configManager, parameters);
8994
}
9095

9196
/// <summary>
@@ -266,7 +271,7 @@ public void SaveAsJSON_CreatesMainFileAndCleanupsTempFile ()
266271
// Verify content
267272
string json = File.ReadAllText(_testSettingsFile.FullName);
268273
Assert.That(json, Does.Contain("AlwaysOnTop"));
269-
Assert.That(json, Does.Contain("TEST_FILTER").Or.Contain("ERROR"));
274+
Assert.That(json, Does.Contain("ERROR").Or.Contain("WARNING"));
270275
}
271276

272277
[Test]
@@ -359,8 +364,12 @@ public void SaveAsJSON_ValidationFailure_PreventsNullSettingsSave ()
359364
Settings settings = null;
360365

361366
// Act & Assert
362-
_ = Assert.Throws<TargetInvocationException>(() => InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings), "Saving null settings should throw exception");
367+
var ex = Assert.Throws<TargetInvocationException>(() =>
368+
InvokePrivateInstanceMethod("SaveAsJSON", _testSettingsFile, settings),
369+
"Saving null settings should throw exception");
363370

371+
// The inner exception should be InvalidOperationException from ValidateSettings
372+
Assert.That(ex.InnerException, Is.InstanceOf<InvalidOperationException>());
364373
Assert.That(_testSettingsFile.Exists, Is.False, "File should not be created if validation fails");
365374
}
366375

@@ -457,7 +466,8 @@ public void LoadOrCreateNew_EmptyFile_HandlesGracefully ()
457466
Assert.That(loadResult, Is.Not.Null);
458467
Assert.That(loadResult.Settings, Is.Not.Null, "Should return valid settings object, not null");
459468
Assert.That(loadResult.Settings.Preferences, Is.Not.Null, "Settings should have preferences");
460-
// Empty file triggers recovery, may create new settings
469+
// Empty file triggers recovery, creates new settings with critical failure
470+
Assert.That(loadResult.CriticalFailure, Is.True, "Empty file should be treated as critical failure");
461471
}
462472

463473
[Test]
@@ -475,6 +485,8 @@ public void LoadOrCreateNew_NullDeserializationResult_HandlesGracefully ()
475485
Assert.That(loadResult, Is.Not.Null);
476486
Assert.That(loadResult.Settings, Is.Not.Null, "Should not return null settings");
477487
Assert.That(loadResult.Settings.Preferences, Is.Not.Null);
488+
// Null deserialization is treated as critical failure
489+
Assert.That(loadResult.CriticalFailure, Is.True);
478490
}
479491

480492
[Test]
@@ -526,6 +538,7 @@ public void LoadOrCreateNew_InvalidJSON_HandlesGracefully ()
526538
Assert.That(loadResult.Settings, Is.Not.Null, "Should return valid settings object");
527539
// Without backup, will return CriticalFailure or create new settings
528540
Assert.That(loadResult.Settings.Preferences, Is.Not.Null);
541+
Assert.That(loadResult.CriticalFailure, Is.True, "Invalid JSON should result in critical failure");
529542
}
530543

531544
#endregion

src/LogExpert.Tests/JSONSaveTest.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,40 @@
1-
using LogExpert.Config;
1+
using LogExpert.Configuration;
22
using LogExpert.Core.Config;
33

44
using Newtonsoft.Json;
55

66
using NUnit.Framework;
77

8-
using System.IO;
9-
108
namespace LogExpert.Tests;
119

1210
[TestFixture]
1311
public class JSONSaveTest
1412
{
1513
[Test(Author = "Hirogen", Description = "Save Options as JSON and Check if the written file can be cast again into the settings object")]
16-
public void SaveOptionsAsJSON()
14+
public void SaveOptionsAsJSON ()
1715
{
1816
ConfigManager.Instance.Settings.AlwaysOnTop = true;
1917
ConfigManager.Instance.Save(SettingsFlags.All);
2018
var configDir = ConfigManager.Instance.ConfigDir;
2119
var settingsFile = configDir + "\\settings.json";
2220

2321
Settings settings = null;
24-
22+
2523
Assert.DoesNotThrow(CastSettings);
2624
Assert.That(settings, Is.Not.Null);
2725
Assert.That(settings.AlwaysOnTop, Is.True);
2826

2927
ConfigManager.Instance.Settings.AlwaysOnTop = false;
3028
ConfigManager.Instance.Save(SettingsFlags.All);
31-
29+
3230
settings = null;
3331
Assert.DoesNotThrow(CastSettings);
3432

3533
Assert.That(settings, !Is.Null);
3634
Assert.That(settings.AlwaysOnTop, Is.False);
3735

3836

39-
void CastSettings()
37+
void CastSettings ()
4038
{
4139
settings = JsonConvert.DeserializeObject<Settings>(File.ReadAllText(settingsFile));
4240
}

src/LogExpert.UI/Controls/LogWindow/LogWindow.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6135,7 +6135,7 @@ public void LoadFile (string fileName, EncodingOptions encodingOptions)
61356135

61366136
try
61376137
{
6138-
_logFileReader = new(fileName, EncodingOptions, IsMultiFile, Preferences.BufferCount, Preferences.LinesPerBuffer, _multiFileOptions, !Preferences.UseLegacyReader, PluginRegistry.PluginRegistry.Instance, ConfigManager.Instance.Settings.Preferences.MaxLineLength);
6138+
_logFileReader = new(fileName, EncodingOptions, IsMultiFile, Preferences.BufferCount, Preferences.LinesPerBuffer, _multiFileOptions, !Preferences.UseLegacyReader, PluginRegistry.PluginRegistry.Instance, ConfigManager.Settings.Preferences.MaxLineLength);
61396139
}
61406140
catch (LogFileException lfe)
61416141
{
@@ -6198,7 +6198,7 @@ public void LoadFilesAsMulti (string[] fileNames, EncodingOptions encodingOption
61986198
EncodingOptions = encodingOptions;
61996199
_columnCache = new ColumnCache();
62006200

6201-
_logFileReader = new(fileNames, EncodingOptions, Preferences.BufferCount, Preferences.LinesPerBuffer, _multiFileOptions, !Preferences.UseLegacyReader, PluginRegistry.PluginRegistry.Instance, ConfigManager.Instance.Settings.Preferences.MaxLineLength);
6201+
_logFileReader = new(fileNames, EncodingOptions, Preferences.BufferCount, Preferences.LinesPerBuffer, _multiFileOptions, !Preferences.UseLegacyReader, PluginRegistry.PluginRegistry.Instance, ConfigManager.Settings.Preferences.MaxLineLength);
62026202

62036203
RegisterLogFileReaderEvents();
62046204
_logFileReader.StartMonitoring();

src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
110110

111111
Load += OnLogTabWindowLoad;
112112

113-
configManager.Instance.ConfigChanged += OnConfigChanged;
113+
ConfigManager.ConfigChanged += OnConfigChanged;
114114
HighlightGroupList = configManager.Settings.Preferences.HighlightGroupList;
115115

116116
Rectangle led = new(0, 0, 8, 2);
@@ -2290,7 +2290,7 @@ private void OnLogTabWindowFormClosing (object sender, CancelEventArgs e)
22902290

22912291
DestroyBookmarkWindow();
22922292

2293-
ConfigManager.Instance.ConfigChanged -= OnConfigChanged;
2293+
ConfigManager.ConfigChanged -= OnConfigChanged;
22942294

22952295
SaveWindowPosition();
22962296
ConfigManager.Save(SettingsFlags.WindowPosition | SettingsFlags.FileHistory);

src/LogExpert.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GithubActions", "GithubActi
9292
..\.github\workflows\test_dotnet.yml = ..\.github\workflows\test_dotnet.yml
9393
EndProjectSection
9494
EndProject
95+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogExpert.Configuration", "LogExpert.Configuration\LogExpert.Configuration.csproj", "{9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}"
96+
EndProject
9597
Global
9698
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9799
Debug|Any CPU = Debug|Any CPU
@@ -192,6 +194,10 @@ Global
192194
{B2E6D6E0-C995-8F9D-4482-869C6A5A64C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
193195
{B2E6D6E0-C995-8F9D-4482-869C6A5A64C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
194196
{B2E6D6E0-C995-8F9D-4482-869C6A5A64C0}.Release|Any CPU.Build.0 = Release|Any CPU
197+
{9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
198+
{9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
199+
{9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
200+
{9EBCD259-B704-4E1B-81D9-A9DCFD9F62DF}.Release|Any CPU.Build.0 = Release|Any CPU
195201
EndGlobalSection
196202
GlobalSection(SolutionProperties) = preSolution
197203
HideSolutionNode = FALSE

src/LogExpert/Classes/LogExpertProxy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System.Runtime.Versioning;
22
using System.Windows.Forms;
33

4-
using LogExpert.Config;
4+
using LogExpert.Configuration;
55
using LogExpert.Core.Interface;
66
using LogExpert.UI.Extensions.LogWindow;
77

0 commit comments

Comments
 (0)