From 5e9760db181978e6dfa99b91657504c45fad1865 Mon Sep 17 00:00:00 2001 From: Nils Andresen Date: Mon, 21 Jun 2021 21:23:10 +0200 Subject: [PATCH] (#17) added some configurations --- recipe.cake | 2 - .../ConfigurationServiceTests.cs | 124 ++++++++++++++++ .../Fixtures/CommandFixture.cs | 58 ++++++++ .../Fixtures/ConfigurationServiceFixture.cs | 96 +++++++++++++ .../GetConfigCommandTests.cs | 20 +++ .../JavaVersionSwitcher.Tests.csproj | 1 + src/JavaVersionSwitcher.Tests/LoggerTests.cs | 45 ++++++ src/JavaVersionSwitcher.Tests/PathTests.cs | 4 +- .../TestConfigurationService.cs | 39 +++++ .../Adapters/IStorageAdapter.cs | 19 +++ .../Adapters/JavaInstallationsAdapter.cs | 54 +++---- .../Adapters/PathAdapter.cs | 5 +- .../Adapters/StorageAdapter.cs | 29 ++++ .../Commands/CommonCommandSettings.cs | 1 - .../config/CommonConfigCommandSettings.cs | 33 +++++ .../Commands/config/GetConfigCommand.cs | 39 +++++ .../Commands/config/SetConfigCommand.cs | 43 ++++++ src/JavaVersionSwitcher/Logging/ILogger.cs | 6 + src/JavaVersionSwitcher/Logging/Logger.cs | 5 + src/JavaVersionSwitcher/Program.cs | 64 ++++++--- .../Services/ConfigurationService.cs | 134 ++++++++++++++++++ .../Services/IConfigurationProvider.cs | 20 +++ .../Services/IConfigurationService.cs | 26 ++++ ...stallationsAdapterConfigurationProvider.cs | 82 +++++++++++ .../SimpleInjectorRegistrar.cs | 2 +- 25 files changed, 895 insertions(+), 56 deletions(-) create mode 100644 src/JavaVersionSwitcher.Tests/ConfigurationServiceTests.cs create mode 100644 src/JavaVersionSwitcher.Tests/Fixtures/CommandFixture.cs create mode 100644 src/JavaVersionSwitcher.Tests/Fixtures/ConfigurationServiceFixture.cs create mode 100644 src/JavaVersionSwitcher.Tests/GetConfigCommandTests.cs create mode 100644 src/JavaVersionSwitcher.Tests/LoggerTests.cs create mode 100644 src/JavaVersionSwitcher.Tests/TestImplementations/TestConfigurationService.cs create mode 100644 src/JavaVersionSwitcher/Adapters/IStorageAdapter.cs create mode 100644 src/JavaVersionSwitcher/Adapters/StorageAdapter.cs create mode 100644 src/JavaVersionSwitcher/Commands/config/CommonConfigCommandSettings.cs create mode 100644 src/JavaVersionSwitcher/Commands/config/GetConfigCommand.cs create mode 100644 src/JavaVersionSwitcher/Commands/config/SetConfigCommand.cs create mode 100644 src/JavaVersionSwitcher/Services/ConfigurationService.cs create mode 100644 src/JavaVersionSwitcher/Services/IConfigurationProvider.cs create mode 100644 src/JavaVersionSwitcher/Services/IConfigurationService.cs create mode 100644 src/JavaVersionSwitcher/Services/JavaInstallationsAdapterConfigurationProvider.cs diff --git a/recipe.cake b/recipe.cake index f7af1316..760bb535 100644 --- a/recipe.cake +++ b/recipe.cake @@ -14,8 +14,6 @@ BuildParameters.SetParameters( shouldRunDotNetCorePack: true, preferredBuildProviderType: BuildProviderType.GitHubActions, twitterMessage: standardNotificationMessage, - shouldRunCoveralls: false, // no tests, currently - shouldRunCodecov: false, shouldRunIntegrationTests: false); BuildParameters.PrintParameters(Context); diff --git a/src/JavaVersionSwitcher.Tests/ConfigurationServiceTests.cs b/src/JavaVersionSwitcher.Tests/ConfigurationServiceTests.cs new file mode 100644 index 00000000..02540c83 --- /dev/null +++ b/src/JavaVersionSwitcher.Tests/ConfigurationServiceTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JavaVersionSwitcher.Tests.Fixtures; +using Shouldly; +using Xunit; + +namespace JavaVersionSwitcher.Tests +{ + public class ConfigurationServiceTests + { + [Fact] + public async Task SetConfiguration_throws_on_wrong_provider() + { + // arrange + using var fixture = new ConfigurationServiceFixture(); + const string providerName = "non-existent-provider"; + + // act + // ReSharper disable once AccessToDisposedClosure + async Task Act() => await fixture.Service.SetConfiguration(providerName, null, null); + + // assert + (await Should.ThrowAsync((Func)Act)) + .Message + .ShouldSatisfyAllConditions( + m => m.ShouldStartWith("No ConfigurationProvider"), + m => m.ShouldContain(providerName)); + } + + [Fact] + public async Task SetConfiguration_throws_on_wrong_setting() + { + // arrange + const string providerName = "provider"; + using var fixture = new ConfigurationServiceFixture(); + fixture.WithConfigurationProvider(providerName); + const string setting = "non-existent-setting"; + + // act' + // ReSharper disable once AccessToDisposedClosure + async Task Act() => await fixture.Service.SetConfiguration(providerName, setting, null); + + // assert + (await Should.ThrowAsync((Func)Act)) + .Message + .ShouldSatisfyAllConditions( + m => m.ShouldStartWith("No Configuration with the name"), + m => m.ShouldContain(setting)); + } + + [Fact] + public async Task SetConfiguration_writes_value_to_xml() + { + // arrange + const string providerName = "pName"; + const string settingsName = "settingsName"; + const string value = "a value"; + using var fixture = new ConfigurationServiceFixture(); + fixture.WithConfigurationProvider(providerName, settingsName); + + // act' + await fixture.Service.SetConfiguration(providerName, settingsName, value); + + // assert + var xml = fixture.ReadXml(providerName, settingsName); + xml.Value.ShouldBe(value); + } + + [Fact] + public async Task GetConfiguration_returns_empty_for_not_set_setting() + { + // arrange + const string providerName = "pName"; + const string settingsName = "settingsName"; + using var fixture = new ConfigurationServiceFixture(); + fixture.WithConfigurationProvider(providerName, settingsName); + + // act' + var actual = await fixture.Service.GetConfiguration(providerName, settingsName); + + // assert + actual.ShouldBe(string.Empty); + } + + [Fact] + public async Task GetConfiguration_returns_the_value_from_xml() + { + // arrange + const string providerName = "pName"; + const string settingsName = "settingsName"; + const string expected = "some value"; + using var fixture = new ConfigurationServiceFixture(); + fixture.WithConfigurationProvider(providerName, settingsName); + fixture.EnsureSetting(providerName, settingsName, expected); + + // act' + var actual = await fixture.Service.GetConfiguration(providerName, settingsName); + + // assert + actual.ShouldBe(expected); + } + + [Fact] + public async Task SetConfiguration_removes_empty_settings() + { + // arrange + const string providerName = "pName"; + const string settingsName = "settingsName"; + using var fixture = new ConfigurationServiceFixture(); + fixture.WithConfigurationProvider(providerName, settingsName); + fixture.EnsureSetting(providerName, settingsName, "some value"); + + // act' + await fixture.Service.SetConfiguration(providerName, settingsName, null); + + // assert + var xml = fixture.ReadXml(); + xml.Parent.ShouldBeNull("this should be the root node."); + xml.Elements().Count().ShouldBe(0); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher.Tests/Fixtures/CommandFixture.cs b/src/JavaVersionSwitcher.Tests/Fixtures/CommandFixture.cs new file mode 100644 index 00000000..9813e8a3 --- /dev/null +++ b/src/JavaVersionSwitcher.Tests/Fixtures/CommandFixture.cs @@ -0,0 +1,58 @@ +using JavaVersionSwitcher.Adapters; +using JavaVersionSwitcher.Logging; +using JavaVersionSwitcher.Services; +using JavaVersionSwitcher.Tests.TestImplementations; +using Moq; +using SimpleInjector; +using Spectre.Console; +using Spectre.Console.Cli; +using Spectre.Console.Testing; + +namespace JavaVersionSwitcher.Tests.Fixtures +{ + public class CommandFixture + { + public TestConsole Console => new TestConsole(); + + public Logger Logger => new Logger(); + + public TestConfigurationService ConfigurationService => new TestConfigurationService(); + + public Mock JavaHomeAdapter => new Mock(); + + public Mock PathAdapter => new Mock(); + + public Mock JavaInstallationsAdapter => new Mock(); + + private ITypeRegistrar BuildRegistrar() + { + var container = new Container(); + container.RegisterInstance(Logger); + container.RegisterInstance(ConfigurationService); + container.RegisterInstance(JavaHomeAdapter.Object); + container.RegisterInstance(PathAdapter.Object); + container.RegisterInstance(JavaInstallationsAdapter.Object); + + container.Register(Lifestyle.Singleton); + + container.Collection.Register( + new[] + { + typeof(JavaInstallationsAdapterConfigurationProvider), + }, + Lifestyle.Singleton); + + return new SimpleInjectorRegistrar(container); + } + + public int Run(params string[] args) + { + AnsiConsole.Console = Console; + var registrar = BuildRegistrar(); + var app = new CommandApp(registrar); + app.Configure(Program.ConfigureApp); + + return app.Run(args); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher.Tests/Fixtures/ConfigurationServiceFixture.cs b/src/JavaVersionSwitcher.Tests/Fixtures/ConfigurationServiceFixture.cs new file mode 100644 index 00000000..f552802a --- /dev/null +++ b/src/JavaVersionSwitcher.Tests/Fixtures/ConfigurationServiceFixture.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using JavaVersionSwitcher.Adapters; +using JavaVersionSwitcher.Services; +using JetBrains.Annotations; +using Moq; +using Shouldly; + +namespace JavaVersionSwitcher.Tests.Fixtures +{ + public class ConfigurationServiceFixture : IDisposable + { + private readonly List _configurationProviders = new List(); + private readonly Mock _storageAdapter; + private readonly string _tmpFile; + + public ConfigurationServiceFixture() + { + _tmpFile = Path.GetTempFileName()+".xml"; + _storageAdapter = new Mock(); + _storageAdapter.Setup(x => x.ConfigurationFilePath).Returns(_tmpFile); + } + + public ConfigurationService Service => new ConfigurationService(_configurationProviders, _storageAdapter.Object); + + public void WithConfigurationProvider(string providerName, params string[] settings) + { + var configurationProvider = new Mock(); + configurationProvider.Setup(x => x.ProviderName).Returns(providerName); + configurationProvider.Setup(x => x.Settings).Returns(settings); + + _configurationProviders.Add(configurationProvider.Object); + } + + public void EnsureSetting([NotNull]string providerName, [NotNull]string setting, string value) + { + var doc = new XDocument(); + doc.Add(ReadXml()); + var root = doc.Root; + + var providerElm = root!.Elements(providerName).SingleOrDefault(); + if (providerElm == null) + { + providerElm = new XElement(providerName); + root.Add(providerElm); + } + + var settingElm = providerElm.Elements(setting).SingleOrDefault(); + if (settingElm == null) + { + settingElm = new XElement(setting); + providerElm.Add(settingElm); + } + + settingElm.Value = value; + doc.Save(_tmpFile); + } + + public XElement ReadXml(string providerName = null, string setting = null) + { + if (!File.Exists(_tmpFile)) + { + return new XElement("temp-settings"); + } + + var xml = XDocument.Load(_tmpFile); + if (providerName == null) + { + return xml.Root; + } + + var providerElm = xml.Root!.Elements(providerName).SingleOrDefault(); + providerElm.ShouldNotBeNull("a provider element should have been created."); + var settingElm = providerElm.Elements(setting).SingleOrDefault(); + if (setting == null) + { + return providerElm; + } + + settingElm.ShouldNotBeNull("a settings element should have been created."); + return settingElm; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + if (_tmpFile != null && File.Exists(_tmpFile)) + { + File.Delete(_tmpFile); + } + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher.Tests/GetConfigCommandTests.cs b/src/JavaVersionSwitcher.Tests/GetConfigCommandTests.cs new file mode 100644 index 00000000..77ac9002 --- /dev/null +++ b/src/JavaVersionSwitcher.Tests/GetConfigCommandTests.cs @@ -0,0 +1,20 @@ +using Xunit; + +namespace JavaVersionSwitcher.Tests +{ + public class GetConfigCommandTests + { + [Fact] + public void Can_Set_CacheTimeout_Configuration() + { + /* + var fixture = new CommandFixture(); + + var result = fixture.Run("config", "set", "cache", "timeout", "12"); + + result.ShouldBe(0); + fixture.ConfigurationService.Configuration["cache"]["timeout"].ShouldBe("12"); + */ + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher.Tests/JavaVersionSwitcher.Tests.csproj b/src/JavaVersionSwitcher.Tests/JavaVersionSwitcher.Tests.csproj index 0625430c..92508a35 100644 --- a/src/JavaVersionSwitcher.Tests/JavaVersionSwitcher.Tests.csproj +++ b/src/JavaVersionSwitcher.Tests/JavaVersionSwitcher.Tests.csproj @@ -9,6 +9,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JavaVersionSwitcher.Tests/LoggerTests.cs b/src/JavaVersionSwitcher.Tests/LoggerTests.cs new file mode 100644 index 00000000..9a545324 --- /dev/null +++ b/src/JavaVersionSwitcher.Tests/LoggerTests.cs @@ -0,0 +1,45 @@ +using JavaVersionSwitcher.Logging; +using Shouldly; +using Spectre.Console; +using Spectre.Console.Testing; +using Xunit; + +namespace JavaVersionSwitcher.Tests +{ + public class LoggerTests + { + private readonly TestConsole _console = new TestConsole(); + private readonly ILogger _logger = new Logger(); + + public LoggerTests() + { + AnsiConsole.Console = _console; + } + + [Fact] + public void Writes_warning_with_prefix() + { + _logger.LogWarning("test"); + + _console.Output.ShouldStartWith("WARNING:"); + } + + [Fact] + public void Writes_verbose_when_verbose_is_set() + { + _logger.PrintVerbose = true; + _logger.LogVerbose("test"); + + _console.Output.ShouldBe("test\n"); + } + + [Fact] + public void Writes_nothing_when_verbose_is_not_set() + { + _logger.PrintVerbose = false; + _logger.LogVerbose("test"); + + _console.Output.ShouldBe(string.Empty); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher.Tests/PathTests.cs b/src/JavaVersionSwitcher.Tests/PathTests.cs index 4687517d..af0862da 100644 --- a/src/JavaVersionSwitcher.Tests/PathTests.cs +++ b/src/JavaVersionSwitcher.Tests/PathTests.cs @@ -2,8 +2,6 @@ using System.Linq; using System.Threading.Tasks; using JavaVersionSwitcher.Adapters; -using JavaVersionSwitcher.Logging; -using Moq; using Shouldly; using Xunit; @@ -11,7 +9,7 @@ namespace JavaVersionSwitcher.Tests { public class PathTests { - private readonly PathAdapter _adapter = new PathAdapter(new Mock().Object); + private readonly PathAdapter _adapter = new PathAdapter(); [Fact] public async Task Can_Set_per_process() diff --git a/src/JavaVersionSwitcher.Tests/TestImplementations/TestConfigurationService.cs b/src/JavaVersionSwitcher.Tests/TestImplementations/TestConfigurationService.cs new file mode 100644 index 00000000..69992646 --- /dev/null +++ b/src/JavaVersionSwitcher.Tests/TestImplementations/TestConfigurationService.cs @@ -0,0 +1,39 @@ + +using System.Collections.Generic; +using System.Threading.Tasks; +using JavaVersionSwitcher.Services; + +namespace JavaVersionSwitcher.Tests.TestImplementations +{ + public class TestConfigurationService : IConfigurationService + { + public Dictionary> Configuration => + new Dictionary>(); + + public Task GetConfiguration(string provider, string settingName) + { + + if (Configuration.TryGetValue(provider, out var settings)) + { + if (settings.TryGetValue(settingName, out var value)) + { + return Task.FromResult(value); + } + } + + return Task.FromResult(string.Empty); + } + + public Task SetConfiguration(string provider, string settingName, string value) + { + if (!Configuration.TryGetValue(provider, out var settings)) + { + settings = new Dictionary(); + Configuration.Add(provider, settings); + } + + settings[settingName] = value; + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Adapters/IStorageAdapter.cs b/src/JavaVersionSwitcher/Adapters/IStorageAdapter.cs new file mode 100644 index 00000000..20d98103 --- /dev/null +++ b/src/JavaVersionSwitcher/Adapters/IStorageAdapter.cs @@ -0,0 +1,19 @@ +namespace JavaVersionSwitcher.Adapters +{ + /// + /// Information about where (in the FileSystem) + /// "our" files are located. + /// + public interface IStorageAdapter + { + /// + /// Gets the path to the main config file + /// + string ConfigurationFilePath { get; } + + /// + /// Gets the path to the cache file + /// + string JavaInstallationCacheFilePath { get; } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Adapters/JavaInstallationsAdapter.cs b/src/JavaVersionSwitcher/Adapters/JavaInstallationsAdapter.cs index 072f69d7..1c78f535 100644 --- a/src/JavaVersionSwitcher/Adapters/JavaInstallationsAdapter.cs +++ b/src/JavaVersionSwitcher/Adapters/JavaInstallationsAdapter.cs @@ -8,6 +8,7 @@ using System.Xml.Serialization; using JavaVersionSwitcher.Logging; using JavaVersionSwitcher.Models; +using JavaVersionSwitcher.Services; using Spectre.Console; namespace JavaVersionSwitcher.Adapters @@ -16,16 +17,26 @@ namespace JavaVersionSwitcher.Adapters public class JavaInstallationsAdapter : IJavaInstallationsAdapter { private readonly ILogger _logger; - - public JavaInstallationsAdapter(ILogger logger) + private readonly IConfigurationService _configurationService; + private readonly JavaInstallationsAdapterConfigurationProvider _configurationProvider; + private readonly IStorageAdapter _storageAdapter; + + public JavaInstallationsAdapter( + ILogger logger, + IConfigurationService configurationService, + JavaInstallationsAdapterConfigurationProvider configurationProvider, + IStorageAdapter storageAdapter) { _logger = logger; + _configurationService = configurationService; + _configurationProvider = configurationProvider; + _storageAdapter = storageAdapter; } /// public async Task> GetJavaInstallations(bool forceReScan = false) { - if (!forceReScan && HasRecentCacheData()) + if (!forceReScan && await HasRecentCacheData()) { try { @@ -51,9 +62,12 @@ public async Task> GetJavaInstallations(bool force return data; } + + + private async Task SaveCacheData(IEnumerable data) { - var file = GetCacheFileName(); + var file = _storageAdapter.JavaInstallationCacheFilePath; Directory.CreateDirectory(Path.GetDirectoryName(file)!); var serializer = new XmlSerializer(typeof(JavaInstallation[])); await using var stream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None); @@ -64,7 +78,7 @@ private async Task SaveCacheData(IEnumerable data) private async Task> LoadCacheData() { - var file = GetCacheFileName(); + var file = _storageAdapter.JavaInstallationCacheFilePath; var serializer = new XmlSerializer(typeof(JavaInstallation[])); await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); using var reader = new StreamReader(stream, Encoding.UTF8); @@ -72,26 +86,17 @@ private async Task> LoadCacheData() return (JavaInstallation[])serializer.Deserialize(reader); } - private bool HasRecentCacheData() + private async Task HasRecentCacheData() { - var fileName = GetCacheFileName(); + var fileName = _storageAdapter.JavaInstallationCacheFilePath; var file = new FileInfo(fileName); if (!file.Exists) { return false; } - // TODO: Configure Timeout? - return file.LastWriteTime.AddDays(7) >= DateTime.Now; - } - - private string GetCacheFileName() - { - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var app = typeof(JavaInstallation).Assembly.GetName().Name ?? "JavaVersionSwitcher"; - const string file = "installations.xml"; - - return Path.Combine(appData, app, file); + var timeout = await _configurationProvider.GetCacheTimeout(_configurationService); + return file.LastWriteTime.AddDays(timeout) >= DateTime.Now; } private async Task> ForceScan() @@ -104,13 +109,12 @@ await AnsiConsole.Status() ctx.Spinner(Spinner.Known.Star); ctx.SpinnerStyle(Style.Parse("green")); - // todo configurable start paths? - var start = new[] - { - Environment.ExpandEnvironmentVariables("%ProgramW6432%"), - Environment.ExpandEnvironmentVariables("%ProgramFiles(x86)%") - }.Distinct() - .Where(x => !string.IsNullOrEmpty(x)); + var start = + (await _configurationProvider.GetStartPaths(_configurationService)) + .Select(Environment.ExpandEnvironmentVariables) + .Distinct() + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); _logger.LogVerbose( $@"Scanning for installations in:{Environment.NewLine} - {string.Join($"{Environment.NewLine} - ", start)}"); diff --git a/src/JavaVersionSwitcher/Adapters/PathAdapter.cs b/src/JavaVersionSwitcher/Adapters/PathAdapter.cs index b0747d0c..25cd0585 100644 --- a/src/JavaVersionSwitcher/Adapters/PathAdapter.cs +++ b/src/JavaVersionSwitcher/Adapters/PathAdapter.cs @@ -2,19 +2,16 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using JavaVersionSwitcher.Logging; namespace JavaVersionSwitcher.Adapters { /// public class PathAdapter : IPathAdapter { - private readonly ILogger _logger; private readonly SimpleEnvironmentAdapter _adapter; - public PathAdapter(ILogger logger) + public PathAdapter() { - _logger = logger; _adapter = new SimpleEnvironmentAdapter("PATH"); } diff --git a/src/JavaVersionSwitcher/Adapters/StorageAdapter.cs b/src/JavaVersionSwitcher/Adapters/StorageAdapter.cs new file mode 100644 index 00000000..a7d9ae01 --- /dev/null +++ b/src/JavaVersionSwitcher/Adapters/StorageAdapter.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; + +namespace JavaVersionSwitcher.Adapters +{ + /// + public class StorageAdapter : IStorageAdapter + { + private string _baseFolder; + + /// + public string ConfigurationFilePath => GetPath("settings.xml"); + + /// + public string JavaInstallationCacheFilePath => GetPath(""); + + private string GetPath(string fileName) + { + if (_baseFolder == null) + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var app = typeof(StorageAdapter).Assembly.GetName().Name ?? "JavaVersionSwitcher"; + _baseFolder = Path.Combine(appData, app); + } + + return Path.Combine(_baseFolder, fileName); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Commands/CommonCommandSettings.cs b/src/JavaVersionSwitcher/Commands/CommonCommandSettings.cs index 38d15058..8a1a7a6c 100644 --- a/src/JavaVersionSwitcher/Commands/CommonCommandSettings.cs +++ b/src/JavaVersionSwitcher/Commands/CommonCommandSettings.cs @@ -1,5 +1,4 @@ using System.ComponentModel; -using System.Threading.Tasks; using Spectre.Console.Cli; namespace JavaVersionSwitcher.Commands diff --git a/src/JavaVersionSwitcher/Commands/config/CommonConfigCommandSettings.cs b/src/JavaVersionSwitcher/Commands/config/CommonConfigCommandSettings.cs new file mode 100644 index 00000000..c2bfc271 --- /dev/null +++ b/src/JavaVersionSwitcher/Commands/config/CommonConfigCommandSettings.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using JetBrains.Annotations; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace JavaVersionSwitcher.Commands.config +{ + public abstract class CommonConfigCommandSettings : CommonCommandSettings + { + [CommandArgument(0, "[Provider]")] + [Description("Name of the provider.")] + public string Provider { get; [UsedImplicitly] set; } + + [CommandArgument(1, "[Name]")] + [Description("Name of the option for that provider.")] + public string Name { get; [UsedImplicitly] set; } + + public override ValidationResult Validate() + { + if (string.IsNullOrEmpty(Provider)) + { + return ValidationResult.Error("Provider must be set."); + } + + if (string.IsNullOrEmpty(Name)) + { + return ValidationResult.Error("Name must be set."); + } + + return ValidationResult.Success(); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Commands/config/GetConfigCommand.cs b/src/JavaVersionSwitcher/Commands/config/GetConfigCommand.cs new file mode 100644 index 00000000..34a1583a --- /dev/null +++ b/src/JavaVersionSwitcher/Commands/config/GetConfigCommand.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using JavaVersionSwitcher.Logging; +using JavaVersionSwitcher.Services; +using JetBrains.Annotations; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace JavaVersionSwitcher.Commands.config +{ + [UsedImplicitly] + public class GetConfigCommand : AsyncCommand + { + private readonly ILogger _logger; + private readonly IConfigurationService _service; + + public GetConfigCommand( + ILogger logger, + IConfigurationService service) + { + _logger = logger; + _service = service; + } + + [UsedImplicitly] + public sealed class Settings : CommonConfigCommandSettings + { + } + + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + _logger.PrintVerbose = settings.Verbose; + + var val = await _service.GetConfiguration(settings.Provider, settings.Name); + AnsiConsole.MarkupLine(val); + + return 0; + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Commands/config/SetConfigCommand.cs b/src/JavaVersionSwitcher/Commands/config/SetConfigCommand.cs new file mode 100644 index 00000000..fe3c9b58 --- /dev/null +++ b/src/JavaVersionSwitcher/Commands/config/SetConfigCommand.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using JavaVersionSwitcher.Logging; +using JavaVersionSwitcher.Services; +using JetBrains.Annotations; +using Spectre.Console.Cli; + +namespace JavaVersionSwitcher.Commands.config +{ + [UsedImplicitly] + public class SetConfigCommand : AsyncCommand + { + private readonly ILogger _logger; + private readonly IConfigurationService _service; + + public SetConfigCommand( + ILogger logger, + IConfigurationService service) + { + _logger = logger; + _service = service; + } + + [UsedImplicitly] + public sealed class Settings : CommonConfigCommandSettings + { + [CommandArgument(2, "[Value]")] + [Description("The value to set.")] + [UsedImplicitly] + public string Value { get; set; } + } + + public override async Task ExecuteAsync(CommandContext context, Settings settings) + { + _logger.PrintVerbose = settings.Verbose; + + await _service.SetConfiguration(settings.Provider, settings.Name, settings.Value); + + return 0; + } + } + +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Logging/ILogger.cs b/src/JavaVersionSwitcher/Logging/ILogger.cs index 9f61ff63..c1c8ef14 100644 --- a/src/JavaVersionSwitcher/Logging/ILogger.cs +++ b/src/JavaVersionSwitcher/Logging/ILogger.cs @@ -17,5 +17,11 @@ public interface ILogger /// /// The text to print. void LogVerbose(string text); + + /// + /// Logs a warning. + /// + /// The text to log. + void LogWarning(string text); } } \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Logging/Logger.cs b/src/JavaVersionSwitcher/Logging/Logger.cs index c73db317..1dc81c4a 100644 --- a/src/JavaVersionSwitcher/Logging/Logger.cs +++ b/src/JavaVersionSwitcher/Logging/Logger.cs @@ -15,5 +15,10 @@ public void LogVerbose(string text) AnsiConsole.MarkupLine($"[gray]{text}[/]"); } + + public void LogWarning(string text) + { + AnsiConsole.MarkupLine($"[yellow]WARNING: {text}[/]"); + } } } \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Program.cs b/src/JavaVersionSwitcher/Program.cs index 7a739259..b0332f34 100644 --- a/src/JavaVersionSwitcher/Program.cs +++ b/src/JavaVersionSwitcher/Program.cs @@ -1,6 +1,8 @@ using JavaVersionSwitcher.Adapters; using JavaVersionSwitcher.Commands; +using JavaVersionSwitcher.Commands.config; using JavaVersionSwitcher.Logging; +using JavaVersionSwitcher.Services; using JetBrains.Annotations; using SimpleInjector; using Spectre.Console.Cli; @@ -14,38 +16,60 @@ private static int Main(string[] args) { var registrar = BuildRegistrar(); var app = new CommandApp(registrar); - app.Configure(config => + app.Configure(ConfigureApp); + return app.Run(args); + } + + public static void ConfigureApp(IConfigurator config) + { + config.SetApplicationName("dotnet jvs"); + config.AddExample(new[] { "scan", "--force" }); + config.AddExample(new[] { "switch" }); + + config.AddCommand("scan") + .WithAlias("scan-for-java") + .WithDescription("Scan for existing java installations."); + config.AddCommand("check") + .WithDescription("Checks if environment is set up correctly."); + config.AddCommand("switch") + .WithDescription("Switch to a different Java version."); + config.AddBranch("config", cfg => { - config.SetApplicationName("dotnet jvs"); - config.AddExample(new []{ "scan", "--force"}); - config.AddExample(new []{ "switch" }); - - config.AddCommand("scan") - .WithAlias("scan-for-java") - .WithDescription("Scan for existing java installations."); - config.AddCommand("check") - .WithDescription("Checks if environment is set up correctly."); - config.AddCommand("switch") - .WithDescription("Switch to a different Java version."); + cfg.SetDescription("get or set configurations options."); + + cfg.AddCommand("get") + .WithDescription("get configuration options."); + cfg.AddCommand("set") + .WithDescription("set configuration options."); + }); #if DEBUG - config.PropagateExceptions(); - config.ValidateExamples(); + config.PropagateExceptions(); + config.ValidateExamples(); #endif - }); - - return app.Run(args); } - + private static ITypeRegistrar BuildRegistrar() { var container = new Container(); container.Register(Lifestyle.Singleton); + + container.Register(Lifestyle.Singleton); + + container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); - + container.Register(Lifestyle.Singleton); + + container.Collection.Register( + new[] + { + typeof(JavaInstallationsAdapterConfigurationProvider), + }, + Lifestyle.Singleton); + return new SimpleInjectorRegistrar(container); } } -} +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Services/ConfigurationService.cs b/src/JavaVersionSwitcher/Services/ConfigurationService.cs new file mode 100644 index 00000000..c0129213 --- /dev/null +++ b/src/JavaVersionSwitcher/Services/ConfigurationService.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using JavaVersionSwitcher.Adapters; + +namespace JavaVersionSwitcher.Services +{ + /// + public class ConfigurationService : IConfigurationService + { + private readonly IStorageAdapter _storageAdapter; + private readonly IReadOnlyCollection _providers; + private readonly XNamespace _ns = XNamespace.None; + + public ConfigurationService( + IEnumerable providers, + IStorageAdapter storageAdapter) + { + _storageAdapter = storageAdapter; + _providers = providers?.ToList(); + } + + /// + public async Task GetConfiguration(string providerName, string settingName) + { + var provider = GetProvider(providerName); + EnsureSettingNameIsValid(provider, settingName); + + return await ReadSetting(provider.ProviderName, settingName); + } + + /// + public async Task SetConfiguration(string providerName, string settingName, string value) + { + var provider = GetProvider(providerName); + EnsureSettingNameIsValid(provider, settingName); + + await WriteSetting(provider.ProviderName, settingName, value); + } + + private async Task WriteSetting(string providerName, string settingName, string value) + { + var doc = await GetXmlConfig(); + var root = doc.Root; + var providerElement = root!.Elements(_ns + providerName).FirstOrDefault(); + if (providerElement == null) + { + providerElement = new XElement(_ns + providerName); + root.Add(providerElement); + } + + var settingElement = providerElement.Elements(_ns + settingName).FirstOrDefault(); + if (settingElement == null) + { + settingElement = new XElement(_ns + settingName); + providerElement.Add(settingElement); + } + + if (string.IsNullOrEmpty(value)) + { + settingElement.Remove(); + } + else + { + settingElement.Value = value; + } + + if (!providerElement.Elements().Any()) + { + providerElement.Remove(); + } + + await SaveXmlConfig(doc); + } + + private async Task SaveXmlConfig(XDocument doc) + { + var fileName = _storageAdapter.ConfigurationFilePath; + await using var stream = new StreamWriter(fileName, false, Encoding.UTF8); + await doc.SaveAsync(stream, SaveOptions.None, CancellationToken.None); + } + + private async Task ReadSetting(string providerName, string settingName) + { + var doc = await GetXmlConfig(); + var providerElement = doc.Root!.Elements(_ns + providerName).FirstOrDefault(); + if (providerElement == null) + { + return string.Empty; + } + + var settingElement = providerElement.Elements(_ns + settingName).FirstOrDefault(); + return settingElement?.Value ?? string.Empty; + } + + private async Task GetXmlConfig() + { + var fileName = _storageAdapter.ConfigurationFilePath; + var file = new FileInfo(fileName); + if (!file.Exists) + { + var doc = new XDocument(); + doc.Add(new XElement(_ns + "JavaVersionSwitcher")); + return doc; + } + + await using var stream = file.OpenRead(); + var loaded = XDocument.Load(stream); + return loaded; + } + + private void EnsureSettingNameIsValid(IConfigurationProvider provider, string name) + { + var exists = provider.Settings.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (!exists) + { + throw new KeyNotFoundException( + $"No Configuration with the name of {name} exists in provider {provider.ProviderName}."); + } + } + + private IConfigurationProvider GetProvider(string name) + { + return _providers.FirstOrDefault(x => + x.ProviderName.Equals(name, StringComparison.OrdinalIgnoreCase)) ?? + throw new KeyNotFoundException($"No ConfigurationProvider with the name of {name} exists."); + } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Services/IConfigurationProvider.cs b/src/JavaVersionSwitcher/Services/IConfigurationProvider.cs new file mode 100644 index 00000000..d312764b --- /dev/null +++ b/src/JavaVersionSwitcher/Services/IConfigurationProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace JavaVersionSwitcher.Services +{ + /// + /// Provides some configurations + /// + public interface IConfigurationProvider + { + /// + /// Gets the Name of this provider. + /// + string ProviderName { get; } + + /// + /// Gets all settings this provider provides. + /// + IEnumerable Settings { get; } + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Services/IConfigurationService.cs b/src/JavaVersionSwitcher/Services/IConfigurationService.cs new file mode 100644 index 00000000..640e30d4 --- /dev/null +++ b/src/JavaVersionSwitcher/Services/IConfigurationService.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; + +namespace JavaVersionSwitcher.Services +{ + /// + /// Access configuration settings. + /// + public interface IConfigurationService + { + /// + /// Get a configuration setting. + /// + /// The name of the provider, see . + /// The name of the setting of that provider. See + /// The value. + public Task GetConfiguration(string provider, string settingName); + + /// + /// Set a configuration setting. + /// + /// The name of the provider, see . + /// The name of the setting of that provider. See + /// The value to set. + public Task SetConfiguration(string provider, string settingName, string value); + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/Services/JavaInstallationsAdapterConfigurationProvider.cs b/src/JavaVersionSwitcher/Services/JavaInstallationsAdapterConfigurationProvider.cs new file mode 100644 index 00000000..efc7570c --- /dev/null +++ b/src/JavaVersionSwitcher/Services/JavaInstallationsAdapterConfigurationProvider.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JavaVersionSwitcher.Logging; + +namespace JavaVersionSwitcher.Services +{ + /// + public class JavaInstallationsAdapterConfigurationProvider : IConfigurationProvider + { + private readonly ILogger _logger; + private const string Timeout = "timeout"; + private const string StartPaths = "startPaths"; + + public JavaInstallationsAdapterConfigurationProvider( + ILogger logger) + { + _logger = logger; + } + + /// + public string ProviderName => "scan"; + + /// + public IEnumerable Settings => new []{ StartPaths, Timeout }; + + public async Task GetCacheTimeout(IConfigurationService service) + { + const int @default = 7; + var providerName = ProviderName; + var displayName = $"{providerName}:{Timeout}"; + var config = await service.GetConfiguration( + providerName, + Timeout); + if (string.IsNullOrEmpty(config)) + { + return @default; + } + + if (int.TryParse(config, out var parsed)) + { + _logger.LogVerbose($"Using configured value for {displayName} of {parsed} days."); + return parsed; + } + + _logger.LogWarning( + $"Configured value for {displayName} of {config} could not be parsed."); + return @default; + } + + public async Task> GetStartPaths(IConfigurationService service) + { + var providerName = ProviderName; + var displayName = $"{providerName}:{StartPaths}"; + var @default = new[] + { + "%ProgramW6432%", + "%ProgramFiles(x86)%" + }; + + var config = await service.GetConfiguration(providerName, StartPaths); + if (string.IsNullOrEmpty(config)) + { + return @default; + } + + var parts = config.Split(";", StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + { + _logger.LogWarning( + $"Configured value for {displayName} of '{config}' results in an empty list."); + return @default; + } + + _logger.LogVerbose($"Using configured values for {displayName} "); + parts.ToList().ForEach(x => _logger.LogVerbose($" - {x}")); + return parts; + } + + } +} \ No newline at end of file diff --git a/src/JavaVersionSwitcher/SimpleInjectorRegistrar.cs b/src/JavaVersionSwitcher/SimpleInjectorRegistrar.cs index b5ded201..3bfc369b 100644 --- a/src/JavaVersionSwitcher/SimpleInjectorRegistrar.cs +++ b/src/JavaVersionSwitcher/SimpleInjectorRegistrar.cs @@ -8,7 +8,7 @@ namespace JavaVersionSwitcher /// /// Shallow wrapper around SimpleInjector /// - internal class SimpleInjectorRegistrar : ITypeRegistrar + public class SimpleInjectorRegistrar : ITypeRegistrar { private readonly Container _container; private readonly Type[] _knownMultiRegistrations;