diff --git a/src/Applications/ConvAssistTerminate/Program.cs b/src/Applications/ConvAssistTerminate/Program.cs index 0ba19cbd..88fb0ceb 100644 --- a/src/Applications/ConvAssistTerminate/Program.cs +++ b/src/Applications/ConvAssistTerminate/Program.cs @@ -187,7 +187,7 @@ private static void SendMessage() { Console.WriteLine("Sending Request to close ConvAssist"); ConvAssistMessage message = new ConvAssistMessage(WordPredictorMessageTypes.ForceQuitApp, WordPredictionModes.None, "NA"); - string jsonMessage = JsonSerializer.Serialize(message); + string jsonMessage = JsonSerializer.SerializeForInterop(message); _pipeServer.Send(jsonMessage); } catch (Exception es) diff --git a/src/Extensions/ACAT.Extensions.Onboarding/ACAT.Extensions.Onboarding.csproj b/src/Extensions/ACAT.Extensions.Onboarding/ACAT.Extensions.Onboarding.csproj index 2e90d4c9..76bd1462 100644 --- a/src/Extensions/ACAT.Extensions.Onboarding/ACAT.Extensions.Onboarding.csproj +++ b/src/Extensions/ACAT.Extensions.Onboarding/ACAT.Extensions.Onboarding.csproj @@ -55,9 +55,7 @@ UserControlKeyboardConfigSelect.cs - - UserControl - + UserControlLanguageSelect.cs diff --git a/src/Extensions/Default/WordPredictors/ConvAssist/ConvAssistWordPredictor.cs b/src/Extensions/Default/WordPredictors/ConvAssist/ConvAssistWordPredictor.cs index 7230a25e..c8d8427e 100644 --- a/src/Extensions/Default/WordPredictors/ConvAssist/ConvAssistWordPredictor.cs +++ b/src/Extensions/Default/WordPredictors/ConvAssist/ConvAssistWordPredictor.cs @@ -292,7 +292,7 @@ public string SendMessageConvAssistSentencePrediction(string text, WordPredictio public string SendMessageConvAssistWordPrediction(string text, WordPredictionModes mode) { ConvAssistMessage message = new(WordPredictorMessageTypes.NextWordPredictionRequest, mode, text); - string jsonMessage = JsonSerializer.Serialize(message); + string jsonMessage = JsonSerializer.SerializeForInterop(message); //var answer = namedPipe.WriteSync(text, 150); return namedPipe.WriteSync(jsonMessage, 10000); } diff --git a/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistMessage.cs b/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistMessage.cs index 1ccaf53c..2ea28462 100644 --- a/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistMessage.cs +++ b/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistMessage.cs @@ -18,13 +18,17 @@ namespace ACAT.Extensions.WordPredictors.ConvAssist.MessageTypes [Serializable] internal class ConvAssistMessage { - public string Data { get; set; } - public WordPredictorMessageTypes MessageType { get; set; } - public WordPredictionModes PredictionType { get; set; } + public string Data { get; set; } = string.Empty; + public WordPredictorMessageTypes MessageType { get; set; } = WordPredictorMessageTypes.None; + public WordPredictionModes PredictionType { get; set; } = WordPredictionModes.None; - // Parameterless constructor for deserialization + // Parameterless constructor for deserialization - explicitly initialize all properties public ConvAssistMessage() - { } + { + Data = string.Empty; + MessageType = WordPredictorMessageTypes.None; + PredictionType = WordPredictionModes.None; + } // this is the JSON representation of the data /// @@ -37,7 +41,7 @@ public ConvAssistMessage(WordPredictorMessageTypes msgType, WordPredictionModes { MessageType = msgType; PredictionType = PredictionMode; - Data = message; + Data = message ?? string.Empty; } } } \ No newline at end of file diff --git a/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistSetParam.cs b/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistSetParam.cs index f81ef6a5..03f34ead 100644 --- a/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistSetParam.cs +++ b/src/Extensions/Default/WordPredictors/ConvAssist/MessageTypes/ConvAssistSetParam.cs @@ -17,17 +17,20 @@ namespace ACAT.Extensions.WordPredictors.ConvAssist.MessageTypes [Serializable] internal class ConvAssistSetParam { - public ConvAssistParameterType Parameter { get; set; } - public string Value { get; set; } + public ConvAssistParameterType Parameter { get; set; } = ConvAssistParameterType.None; + public string Value { get; set; } = string.Empty; - // Parameterless constructor for deserialization + // Parameterless constructor for deserialization - explicitly initialize all properties public ConvAssistSetParam() - { } + { + Parameter = ConvAssistParameterType.None; + Value = string.Empty; + } public ConvAssistSetParam(ConvAssistParameterType param, string value) { Parameter = param; - Value = value; + Value = value ?? string.Empty; } public ConvAssistSetParam(ConvAssistParameterType param, int value) diff --git a/src/Extensions/Default/WordPredictors/ConvAssist/NamedPipeServerConvAssist.cs b/src/Extensions/Default/WordPredictors/ConvAssist/NamedPipeServerConvAssist.cs index 01198444..f8da32fa 100644 --- a/src/Extensions/Default/WordPredictors/ConvAssist/NamedPipeServerConvAssist.cs +++ b/src/Extensions/Default/WordPredictors/ConvAssist/NamedPipeServerConvAssist.cs @@ -223,14 +223,14 @@ public async Task SendParams() List parameters = new() { - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.EnableLog, Common.AppPreferences.EnableLogs.ToString())), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.PathLog, FileUtils.GetLogsDir())), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.Suggestions, Common.AppPreferences.WordsSuggestions.ToString())), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.TestGeneralSentencePrediction, ConvAssistWordPredictor.settings.Test_GeneralSentencePrediction.ToString())), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.RetrieveACC, ConvAssistWordPredictor.settings.EnableSmallVocabularySentencePrediction.ToString())), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.PathStatic, staticPath)), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.PathPersonilized, personalizedPath)), - JsonSerializer.Serialize(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.Path, _pathToFiles)) + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.EnableLog, Common.AppPreferences.EnableLogs.ToString())), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.PathLog, FileUtils.GetLogsDir())), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.Suggestions, Common.AppPreferences.WordsSuggestions.ToString())), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.TestGeneralSentencePrediction, ConvAssistWordPredictor.settings.Test_GeneralSentencePrediction.ToString())), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.RetrieveACC, ConvAssistWordPredictor.settings.EnableSmallVocabularySentencePrediction.ToString())), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.PathStatic, staticPath)), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.PathPersonilized, personalizedPath)), + JsonSerializer.SerializeForInterop(new ConvAssistSetParam(ConvAssistSetParam.ConvAssistParameterType.Path, _pathToFiles)) }; @@ -238,8 +238,10 @@ public async Task SendParams() { try { - var message = JsonSerializer.Serialize(new ConvAssistMessage(WordPredictorMessageTypes.SetParam, WordPredictionModes.None, param)); - + var message = JsonSerializer.SerializeForInterop(new ConvAssistMessage(WordPredictorMessageTypes.SetParam, WordPredictionModes.None, param)); + + _logger.LogDebug("Sending ConvAssist message: {Message}", message); + //TODO: Check result and handle appropriately. _ = WriteAsync(message, waitDelay).ConfigureAwait(false).GetAwaiter().GetResult(); } @@ -251,7 +253,7 @@ public async Task SendParams() // ConvAssist needs some time to get ready. Send a message to check if it is ready - string msg = JsonSerializer.Serialize(new ConvAssistMessage(WordPredictorMessageTypes.NextSentencePredictionRequest, WordPredictionModes.None, string.Empty)); + string msg = JsonSerializer.SerializeForInterop(new ConvAssistMessage(WordPredictorMessageTypes.NextSentencePredictionRequest, WordPredictionModes.None, string.Empty)); bool clientReady = false; var tcs = new TaskCompletionSource(); @@ -260,14 +262,35 @@ await Task.Run(async () => { while (!clientReady) { - var result = WriteAsync(msg, waitDelay).ConfigureAwait(false).GetAwaiter().GetResult(); - var resultObject = JsonSerializer.Deserialize(result); - if (resultObject != null && resultObject.MessageType != WordAndCharacterPredictionResponse.ConvAssistMessageTypes.NotReady) + try { - clientReady = true; - tcs.SetResult(true); - break; + var result = WriteAsync(msg, waitDelay).ConfigureAwait(false).GetAwaiter().GetResult(); + + // Check if we got a valid response + if (string.IsNullOrWhiteSpace(result)) + { + _logger.LogDebug("No response from ConvAssist, retrying..."); + await Task.Delay(waitDelay); + continue; + } + + var resultObject = JsonSerializer.Deserialize(result); + if (resultObject != null && resultObject.MessageType != WordAndCharacterPredictionResponse.ConvAssistMessageTypes.NotReady) + { + clientReady = true; + tcs.SetResult(true); + break; + } + } + catch (System.Text.Json.JsonException jsonEx) + { + _logger.LogWarning(jsonEx, "Failed to deserialize ConvAssist response, retrying..."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking ConvAssist ready state"); } + await Task.Delay(waitDelay); } }); @@ -456,7 +479,8 @@ public async Task WriteAsync(string value, int msDelay) var writeTcs = new TaskCompletionSource(); byte[] payload = Encoding.UTF8.GetBytes(value); - + + _logger.LogDebug("Sending ConvAssist message asynchronously: {Message}", value); _logger.LogDebug("Payload hex: {Payload}", BitConverter.ToString(payload)); byte[] lengthPrefix = BitConverter.GetBytes(payload.Length); diff --git a/src/Extensions/Default/WordPredictors/ConvAssist/SentencePredictionsRequestHandler.cs b/src/Extensions/Default/WordPredictors/ConvAssist/SentencePredictionsRequestHandler.cs index a97dce9b..cfbccd11 100644 --- a/src/Extensions/Default/WordPredictors/ConvAssist/SentencePredictionsRequestHandler.cs +++ b/src/Extensions/Default/WordPredictors/ConvAssist/SentencePredictionsRequestHandler.cs @@ -162,7 +162,23 @@ private List ProcessSentencesPredictions(string predictions, string curr StringBuilder resultFullPredictionWords = new(); WordAndCharacterPredictionResponse answer = new(); var retVal = new List(); - answer = JsonSerializer.Deserialize(predictions); + + // Check for empty response before deserializing + if (string.IsNullOrWhiteSpace(predictions)) + { + return retVal; // Return empty list + } + + try + { + answer = JsonSerializer.Deserialize(predictions); + } + catch (System.Text.Json.JsonException ex) + { + // Log and return empty list if deserialization fails + return retVal; + } + List predictSenetnces = new(); List predictLettersSentence = new(); int i = 0; diff --git a/src/Extensions/Default/WordPredictors/ConvAssist/WordPredictionsRequestHandler.cs b/src/Extensions/Default/WordPredictors/ConvAssist/WordPredictionsRequestHandler.cs index dd65707f..42711dc4 100644 --- a/src/Extensions/Default/WordPredictors/ConvAssist/WordPredictionsRequestHandler.cs +++ b/src/Extensions/Default/WordPredictors/ConvAssist/WordPredictionsRequestHandler.cs @@ -165,7 +165,23 @@ private List ProcessWordPredictions(ConvAssistWordPredictor wordPredicto StringBuilder resultFullPredictionWords = new(); WordAndCharacterPredictionResponse answer = new(); var retVal = new List(); - answer = JsonSerializer.Deserialize(predictions); + + // Check for empty response before deserializing + if (string.IsNullOrWhiteSpace(predictions)) + { + return retVal; // Return empty list + } + + try + { + answer = JsonSerializer.Deserialize(predictions); + } + catch (System.Text.Json.JsonException ex) + { + // Log and return empty list if deserialization fails + return retVal; + } + List predictWords = new(); List predictLetters = new(); int i = 0; diff --git a/src/Libraries/ACATCore.Tests.Configuration/PreferredWordPredictorsTests.cs b/src/Libraries/ACATCore.Tests.Configuration/PreferredWordPredictorsTests.cs new file mode 100644 index 00000000..177d94e9 --- /dev/null +++ b/src/Libraries/ACATCore.Tests.Configuration/PreferredWordPredictorsTests.cs @@ -0,0 +1,269 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2013-2019; 2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// +// PreferredWordPredictorsTests.cs +// +// Unit tests for PreferredWordPredictors JSON configuration +// +//////////////////////////////////////////////////////////////////////////// + +using ACAT.Core.Configuration; +using ACAT.Core.Utility; +using ACAT.Core.Validation; +using ACAT.Core.WordPredictorManagement; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.IO; + +namespace ACATCore.Tests.Configuration +{ + [TestClass] + public class PreferredWordPredictorsJsonTests + { + private string _testDirectory; + + [TestInitialize] + public void Setup() + { + _testDirectory = Path.Combine(Path.GetTempPath(), "ACATTests_WordPred_" + Guid.NewGuid().ToString()); + Directory.CreateDirectory(_testDirectory); + } + + [TestCleanup] + public void Cleanup() + { + if (Directory.Exists(_testDirectory)) + { + Directory.Delete(_testDirectory, true); + } + } + + [TestMethod] + public void CanCreateDefaultPreferredWordPredictors() + { + // Act + var config = PreferredWordPredictorsJson.CreateDefault(); + + // Assert + Assert.IsNotNull(config); + Assert.IsNotNull(config.WordPredictors); + Assert.AreEqual(0, config.WordPredictors.Count); + } + + [TestMethod] + public void CanSerializeToJson() + { + // Arrange + var config = new PreferredWordPredictorsJson(); + var testGuid = Guid.NewGuid(); + config.WordPredictors.Add(new PreferredWordPredictorJson + { + Language = "en", + Id = testGuid.ToString() + }); + + // Act + var json = JsonSerializer.Serialize(config); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(json)); + Assert.IsTrue(json.Contains("en")); + Assert.IsTrue(json.Contains(testGuid.ToString())); + } + + [TestMethod] + public void CanDeserializeFromJson() + { + // Arrange + var testGuid1 = Guid.NewGuid(); + var testGuid2 = Guid.NewGuid(); + var json = $@"{{ + ""wordPredictors"": [ + {{ + ""language"": ""en"", + ""id"": ""{testGuid1}"" + }}, + {{ + ""language"": ""fr"", + ""id"": ""{testGuid2}"" + }} + ] + }}"; + + // Act + var config = JsonSerializer.Deserialize(json); + + // Assert + Assert.IsNotNull(config); + Assert.AreEqual(2, config.WordPredictors.Count); + Assert.AreEqual("en", config.WordPredictors[0].Language); + Assert.AreEqual(testGuid1.ToString(), config.WordPredictors[0].Id); + Assert.AreEqual("fr", config.WordPredictors[1].Language); + Assert.AreEqual(testGuid2.ToString(), config.WordPredictors[1].Id); + } + + [TestMethod] + public void ValidatorAcceptsValidConfiguration() + { + // Arrange + var validator = new PreferredWordPredictorsValidator(); + var config = new PreferredWordPredictorsJson(); + config.WordPredictors.Add(new PreferredWordPredictorJson + { + Language = "en", + Id = Guid.NewGuid().ToString() + }); + + // Act + var result = validator.Validate(config); + + // Assert + Assert.IsTrue(result.IsValid); + } + + [TestMethod] + public void ValidatorRejectsEmptyLanguage() + { + // Arrange + var validator = new PreferredWordPredictorsValidator(); + var config = new PreferredWordPredictorsJson(); + config.WordPredictors.Add(new PreferredWordPredictorJson + { + Language = "", + Id = Guid.NewGuid().ToString() + }); + + // Act + var result = validator.Validate(config); + + // Assert + Assert.IsFalse(result.IsValid); + Assert.IsTrue(result.Errors.Count > 0); + } + + [TestMethod] + public void ValidatorRejectsInvalidGuid() + { + // Arrange + var validator = new PreferredWordPredictorsValidator(); + var config = new PreferredWordPredictorsJson(); + config.WordPredictors.Add(new PreferredWordPredictorJson + { + Language = "en", + Id = "not-a-valid-guid" + }); + + // Act + var result = validator.Validate(config); + + // Assert + Assert.IsFalse(result.IsValid); + } + + [TestMethod] + public void CanLoadAndSaveJsonConfiguration() + { + // Arrange + var loader = new JsonConfigurationLoader(new PreferredWordPredictorsValidator()); + var testFile = Path.Combine(_testDirectory, "test-wordpredictors.json"); + + var config = new PreferredWordPredictorsJson(); + var testGuid = Guid.NewGuid(); + config.WordPredictors.Add(new PreferredWordPredictorJson + { + Language = "es", + Id = testGuid.ToString() + }); + + // Act - Save + bool saveSuccess = loader.Save(config, testFile); + + // Act - Load + var loadedConfig = loader.Load(testFile, createDefaultOnError: false); + + // Assert + Assert.IsTrue(saveSuccess); + Assert.IsNotNull(loadedConfig); + Assert.AreEqual(1, loadedConfig.WordPredictors.Count); + Assert.AreEqual("es", loadedConfig.WordPredictors[0].Language); + Assert.AreEqual(testGuid.ToString(), loadedConfig.WordPredictors[0].Id); + } + + [TestMethod] + public void PreferredWordPredictorsCanLoadFromJson() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "test-wordpredictors.json"); + var testGuid = Guid.NewGuid(); + var json = $@"{{ + ""wordPredictors"": [ + {{ + ""language"": ""de"", + ""id"": ""{testGuid}"" + }} + ] + }}"; + File.WriteAllText(testFile, json); + + PreferredWordPredictors.FilePath = testFile; + + // Act + var config = PreferredWordPredictors.Load(); + + // Assert + Assert.IsNotNull(config); + Assert.AreEqual(1, config.WordPredictors.Count); + Assert.AreEqual("de", config.WordPredictors[0].Language); + Assert.AreEqual(testGuid, config.WordPredictors[0].ID); + } + + [TestMethod] + public void PreferredWordPredictorsCanSaveToJson() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "test-wordpredictors.json"); + var testGuid = Guid.NewGuid(); + + PreferredWordPredictors.FilePath = testFile; + var config = new PreferredWordPredictors(); + config.WordPredictors.Add(new PreferredWordPredictor(testGuid, "it")); + + // Act + bool saved = config.Save(); + + // Assert + Assert.IsTrue(saved); + Assert.IsTrue(File.Exists(testFile)); + + // Verify content + var content = File.ReadAllText(testFile); + Assert.IsTrue(content.Contains("it")); + Assert.IsTrue(content.Contains(testGuid.ToString())); + } + + [TestMethod] + public void ConverterHandlesEmptyList() + { + // Act + var jsonConfig = PreferredWordPredictorsConverter.ToJson(new System.Collections.Generic.List()); + + // Assert + Assert.IsNotNull(jsonConfig); + Assert.AreEqual(0, jsonConfig.WordPredictors.Count); + } + + [TestMethod] + public void ConverterHandlesNullList() + { + // Act + var jsonConfig = PreferredWordPredictorsConverter.ToJson(null); + + // Assert + Assert.IsNotNull(jsonConfig); + Assert.AreEqual(0, jsonConfig.WordPredictors.Count); + } + } +} diff --git a/src/Libraries/ACATCore/ACAT.Core.csproj b/src/Libraries/ACATCore/ACAT.Core.csproj index 6eb76c20..decc19fa 100644 --- a/src/Libraries/ACATCore/ACAT.Core.csproj +++ b/src/Libraries/ACATCore/ACAT.Core.csproj @@ -15,6 +15,7 @@ + @@ -178,6 +179,7 @@ + @@ -428,6 +430,7 @@ + @@ -506,6 +509,7 @@ + diff --git a/src/Libraries/ACATCore/ActuatorManagement/Actuators.cs b/src/Libraries/ACATCore/ActuatorManagement/Actuators.cs index aac216b5..a93b66b9 100644 --- a/src/Libraries/ACATCore/ActuatorManagement/Actuators.cs +++ b/src/Libraries/ACATCore/ActuatorManagement/Actuators.cs @@ -203,12 +203,21 @@ public bool Load(IEnumerable extensionDirs, String configFile, bool load } if (_DLLError) return false; + + ActuatorConfig.ActuatorSettingsFileName = configFile; + + // If config file doesn't exist, create a default one if (!File.Exists(configFile)) { - return false; + _logger?.LogWarning("ActuatorSettings config file not found at {ConfigFile}, creating default", configFile); + + // Create a default config and save it + var defaultConfig = new ActuatorConfig(); + defaultConfig.Save(); + + _logger?.LogInformation("Created default ActuatorSettings.json at {ConfigFile}", configFile); } - ActuatorConfig.ActuatorSettingsFileName = configFile; Config = ActuatorConfig.Load(); // walk through the settings file create and configure diff --git a/src/Libraries/ACATCore/Configuration/PreferredWordPredictorsJson.cs b/src/Libraries/ACATCore/Configuration/PreferredWordPredictorsJson.cs new file mode 100644 index 00000000..1814939e --- /dev/null +++ b/src/Libraries/ACATCore/Configuration/PreferredWordPredictorsJson.cs @@ -0,0 +1,65 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2013-2019; 2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// +// PreferredWordPredictorsJson.cs +// +// JSON-serializable POCO classes for preferred word predictors configuration +// with System.Text.Json attributes for modern JSON serialization +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace ACAT.Core.Configuration +{ + /// + /// Root configuration for preferred word predictors in ACAT + /// Maps languages/cultures to their preferred word predictor + /// + public class PreferredWordPredictorsJson + { + /// + /// List of preferred word predictors for different languages + /// + [JsonPropertyName("wordPredictors")] + [Required] + public List WordPredictors { get; set; } = new(); + + /// + /// Factory method to create default empty configuration + /// + public static PreferredWordPredictorsJson CreateDefault() + { + return new PreferredWordPredictorsJson + { + WordPredictors = new List() + }; + } + } + + /// + /// Configuration mapping a language to its preferred word predictor + /// + public class PreferredWordPredictorJson + { + /// + /// Language code (e.g., "en", "fr", "es") + /// + [JsonPropertyName("language")] + [Required(ErrorMessage = "Language is required")] + public string Language { get; set; } = string.Empty; + + /// + /// GUID of the preferred word predictor for this language + /// + [JsonPropertyName("id")] + [Required(ErrorMessage = "Word predictor ID is required")] + public string Id { get; set; } = Guid.Empty.ToString(); + } +} diff --git a/src/Libraries/ACATCore/Utility/JsonSerializer.cs b/src/Libraries/ACATCore/Utility/JsonSerializer.cs index 7de57ed9..d2b59e53 100644 --- a/src/Libraries/ACATCore/Utility/JsonSerializer.cs +++ b/src/Libraries/ACATCore/Utility/JsonSerializer.cs @@ -20,7 +20,7 @@ public static class JsonSerializer WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never }; /// @@ -35,6 +35,18 @@ public static class JsonSerializer PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + /// + /// Serialization options for interop scenarios (named pipes, external apps) + /// Uses PascalCase to match C# property names exactly - no transformation + /// + private static readonly JsonSerializerOptions _interopWriteOptions = new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNamingPolicy = null, // Use property names as-is (PascalCase) + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never + }; + /// /// Serializes an object to JSON string /// @@ -66,5 +78,32 @@ public static TValue Deserialize(string json) return res; } + + /// + /// Serializes an object to JSON string using PascalCase property names (as-is). + /// Used for interop scenarios like named pipes where external applications + /// expect exact property name matches. + /// + public static string SerializeForInterop(TValue message) + { + var res = System.Text.Json.JsonSerializer.Serialize(message, _interopWriteOptions); + + if (string.IsNullOrEmpty(res)) + { + throw new InvalidOperationException("Serialization Failed."); + } + + return res; + } + + /// + /// Deserializes JSON string to an object using case-insensitive property matching. + /// Used for interop scenarios where property names might vary in casing. + /// + public static TValue DeserializeForInterop(string json) + { + // Reuse _readOptions which already has PropertyNameCaseInsensitive = true + return Deserialize(json); + } } } \ No newline at end of file diff --git a/src/Libraries/ACATCore/Utility/LoggingConfiguration.cs b/src/Libraries/ACATCore/Utility/LoggingConfiguration.cs index 341f0c9b..47bb2a53 100644 --- a/src/Libraries/ACATCore/Utility/LoggingConfiguration.cs +++ b/src/Libraries/ACATCore/Utility/LoggingConfiguration.cs @@ -14,7 +14,7 @@ namespace ACAT.Core.Utility { /// /// Configures Microsoft.Extensions.Logging infrastructure for ACAT - /// Provides structured logging with Console and File sinks + /// Provides structured logging with Console, Debug, and File sinks /// public static class LoggingConfiguration { @@ -40,6 +40,9 @@ public static IServiceCollection AddACATLogging(this IServiceCollection services // Add console logging builder.AddConsole(); + // Add debug output logging for Visual Studio Debug window + builder.AddDebug(); + // Set minimum log level based on build configuration #if DEBUG builder.SetMinimumLevel(LogLevel.Debug); diff --git a/src/Libraries/ACATCore/Validation/PreferredWordPredictorsValidator.cs b/src/Libraries/ACATCore/Validation/PreferredWordPredictorsValidator.cs new file mode 100644 index 00000000..2a7dd105 --- /dev/null +++ b/src/Libraries/ACATCore/Validation/PreferredWordPredictorsValidator.cs @@ -0,0 +1,58 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2013-2019; 2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// +// PreferredWordPredictorsValidator.cs +// +// FluentValidation validator for PreferredWordPredictorsJson configuration +// +//////////////////////////////////////////////////////////////////////////// + +using ACAT.Core.Configuration; +using FluentValidation; +using System; + +namespace ACAT.Core.Validation +{ + /// + /// Validates PreferredWordPredictorsJson configuration using FluentValidation + /// + public class PreferredWordPredictorsValidator : AbstractValidator + { + public PreferredWordPredictorsValidator() + { + RuleFor(x => x.WordPredictors) + .NotNull() + .WithMessage("WordPredictors list cannot be null"); + + RuleForEach(x => x.WordPredictors) + .SetValidator(new PreferredWordPredictorItemValidator()); + } + } + + /// + /// Validates individual PreferredWordPredictorJson items + /// + public class PreferredWordPredictorItemValidator : AbstractValidator + { + public PreferredWordPredictorItemValidator() + { + RuleFor(x => x.Language) + .NotEmpty() + .WithMessage("Language cannot be empty"); + + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage("Word predictor ID cannot be empty") + .Must(BeValidGuid) + .WithMessage("Word predictor ID must be a valid GUID"); + } + + private bool BeValidGuid(string id) + { + return Guid.TryParse(id, out _); + } + } +} diff --git a/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictors.cs b/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictors.cs index acc4ba22..c306a0f9 100644 --- a/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictors.cs +++ b/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictors.cs @@ -5,10 +5,13 @@ // //////////////////////////////////////////////////////////////////////////// +using ACAT.Core.Configuration; using ACAT.Core.PreferencesManagement; +using ACAT.Core.Utility; +using ACAT.Core.Validation; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Xml.Serialization; @@ -23,6 +26,8 @@ namespace ACAT.Core.WordPredictorManagement [Serializable] public class PreferredWordPredictors : PreferencesBase { + private static readonly ILogger _logger = LoggingConfiguration.CreateLogger(); + /// /// Path to the file to serialize to /// @@ -51,13 +56,55 @@ public IEnumerable List } /// - /// Deserializes list of word predictors from the file and + /// Deserializes list of word predictors from JSON file and /// returns an instance of this class /// /// an object of this class public static PreferredWordPredictors Load() { - return Load(FilePath); + return LoadFromJson(FilePath); + } + + /// + /// Loads settings from JSON file with validation + /// + /// Path to JSON configuration file + /// PreferredWordPredictors object + private static PreferredWordPredictors LoadFromJson(string filePath) + { + if (string.IsNullOrEmpty(filePath)) + { + _logger.LogError("PreferredWordPredictors FilePath is null or empty"); + return LoadDefaults(); + } + + try + { + // Use JsonConfigurationLoader with validation + var validator = new PreferredWordPredictorsValidator(); + var loader = new JsonConfigurationLoader(validator, _logger); + + var jsonSettings = loader.Load(filePath, createDefaultOnError: true); + + if (jsonSettings == null) + { + _logger.LogWarning("Failed to load JSON settings, using defaults"); + return LoadDefaults(); + } + + // Convert JSON model to legacy model + var config = new PreferredWordPredictors(); + config.WordPredictors = PreferredWordPredictorsConverter.FromJson(jsonSettings); + + _logger.LogInformation("Successfully loaded {Count} preferred word predictor(s) from JSON", config.WordPredictors.Count); + + return config; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading preferred word predictors from JSON: {FilePath}", filePath); + return LoadDefaults(); + } } /// @@ -66,7 +113,7 @@ public static PreferredWordPredictors Load() /// /// culture /// id, guid.empty if none found - public Guid GetByCulture(CultureInfo ci) + public Guid GetByCulture(System.Globalization.CultureInfo ci) { if (ci == null) { @@ -80,24 +127,68 @@ public Guid GetByCulture(CultureInfo ci) public override bool ResetToDefault() { var tmp = LoadDefaults(); - var res = Save(tmp, FilePath); + var res = SaveToJson(tmp, FilePath); Load(); return res; } /// - /// Persists this object to a file + /// Persists this object to a JSON file /// /// true on success public override bool Save() { - if (!String.IsNullOrEmpty(FilePath)) + return !string.IsNullOrEmpty(FilePath) && SaveToJson(this, FilePath); + } + + /// + /// Saves settings to JSON file with validation + /// + /// Configuration to save + /// Path to JSON file + /// True if successful + private static bool SaveToJson(PreferredWordPredictors config, string filePath) + { + if (config == null) { - return Save(this, FilePath); + _logger.LogError("Cannot save null PreferredWordPredictors"); + return false; } - return false; + if (string.IsNullOrEmpty(filePath)) + { + _logger.LogError("PreferredWordPredictors FilePath is null or empty"); + return false; + } + + try + { + // Convert legacy model to JSON model + var jsonSettings = PreferredWordPredictorsConverter.ToJson(config.WordPredictors); + + // Use JsonConfigurationLoader with validation + var validator = new PreferredWordPredictorsValidator(); + var loader = new JsonConfigurationLoader(validator, _logger); + + bool success = loader.Save(jsonSettings, filePath); + + if (success) + { + _logger.LogInformation("Successfully saved preferred word predictors to JSON: {FilePath}", filePath); + } + else + { + _logger.LogError("Failed to save preferred word predictors to JSON: {FilePath}", filePath); + } + + return success; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving preferred word predictors to JSON: {FilePath}", filePath); + return false; + } } /// diff --git a/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictorsConverter.cs b/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictorsConverter.cs new file mode 100644 index 00000000..22f066de --- /dev/null +++ b/src/Libraries/ACATCore/WordPredictorManagement/PreferredWordPredictorsConverter.cs @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2013-2019; 2023 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// +// +// PreferredWordPredictorsConverter.cs +// +// Converts between legacy PreferredWordPredictors XML model and +// new PreferredWordPredictorsJson model +// +//////////////////////////////////////////////////////////////////////////// + +using ACAT.Core.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ACAT.Core.WordPredictorManagement +{ + /// + /// Converts between legacy XML-based PreferredWordPredictors model + /// and new JSON-based configuration model + /// + public static class PreferredWordPredictorsConverter + { + /// + /// Converts JSON model to legacy model + /// + /// JSON configuration + /// List of PreferredWordPredictor objects + public static List FromJson(PreferredWordPredictorsJson jsonConfig) + { + if (jsonConfig == null) + { + return new List(); + } + + var result = new List(); + + foreach (var jsonItem in jsonConfig.WordPredictors) + { + if (Guid.TryParse(jsonItem.Id, out Guid guid)) + { + result.Add(new PreferredWordPredictor(guid, jsonItem.Language)); + } + } + + return result; + } + + /// + /// Converts legacy model to JSON model + /// + /// List of PreferredWordPredictor objects + /// JSON configuration + public static PreferredWordPredictorsJson ToJson(List wordPredictors) + { + if (wordPredictors == null) + { + return PreferredWordPredictorsJson.CreateDefault(); + } + + var jsonConfig = new PreferredWordPredictorsJson(); + + foreach (var item in wordPredictors) + { + jsonConfig.WordPredictors.Add(new PreferredWordPredictorJson + { + Language = item.Language ?? string.Empty, + Id = item.ID.ToString() + }); + } + + return jsonConfig; + } + } +} diff --git a/src/Libraries/ACATCore/WordPredictorManagement/WordPredictors.cs b/src/Libraries/ACATCore/WordPredictorManagement/WordPredictors.cs index 53e86e94..9883ecf2 100644 --- a/src/Libraries/ACATCore/WordPredictorManagement/WordPredictors.cs +++ b/src/Libraries/ACATCore/WordPredictorManagement/WordPredictors.cs @@ -31,7 +31,7 @@ public class WordPredictors : IDisposable /// /// Name of the config file where Id's of preferred word predictors are stored /// - private const string PreferredConfigFile = "PreferredWordPredictors.xml"; + private const string PreferredConfigFile = "PreferredWordPredictors.json"; /// /// Null word predictor. Doesn't do anything :-)