diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index e81dc0d64..75fa9a3a1 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -42,7 +42,6 @@ - @@ -50,6 +49,7 @@ + diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index ce374ca88..c61aa021c 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -4,10 +4,8 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text.Json; using Artemis.Core.JsonConverters; -using Artemis.Core.Services; -using Artemis.Core.SkiaSharp; -using Newtonsoft.Json; namespace Artemis.Core; @@ -62,7 +60,7 @@ public static class Constants /// The full path to the Artemis user layouts folder /// public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts"); - + /// /// The full path to the Artemis user layouts folder /// @@ -94,20 +92,6 @@ public static class Constants /// public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null); - internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")}; - internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")}; - - internal static JsonSerializerSettings JsonConvertSettings = new() - { - Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} - }; - - internal static JsonSerializerSettings JsonConvertTypedSettings = new() - { - TypeNameHandling = TypeNameHandling.All, - Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} - }; - /// /// A read-only collection containing all primitive numeric types /// @@ -155,4 +139,8 @@ public static class Constants /// Gets the startup arguments provided to the application /// public static ReadOnlyCollection StartupArguments { get; set; } = null!; + + internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")}; + internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")}; + internal static readonly JsonSerializerOptions JsonConvertSettings = new() {Converters = {new SKColorConverter(), new NumericJsonConverter()}}; } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs b/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs deleted file mode 100644 index c43fa7bf8..000000000 --- a/src/Artemis.Core/JsonConverters/ForgivingIntConverter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Artemis.Core.JsonConverters; - -/// -/// An int converter that, if required, will round float values -/// -internal class ForgivingIntConverter : JsonConverter -{ - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, int value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override int ReadJson(JsonReader reader, Type objectType, int existingValue, bool hasExistingValue, JsonSerializer serializer) - { - JValue? jsonValue = serializer.Deserialize(reader); - if (jsonValue == null) - throw new JsonReaderException("Failed to deserialize forgiving int value"); - - if (jsonValue.Type == JTokenType.Float) - return (int) Math.Round(jsonValue.Value()); - if (jsonValue.Type == JTokenType.Integer) - return jsonValue.Value(); - - throw new JsonReaderException("Failed to deserialize forgiving int value"); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs b/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs deleted file mode 100644 index 8a325f7e0..000000000 --- a/src/Artemis.Core/JsonConverters/ForgivingVersionConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System; - -namespace Artemis.Core.JsonConverters -{ - /// - /// Version converter that is forgiving of missing parts of the version string, - /// setting them to zero instead of -1. - /// - internal class ForgivingVersionConverter : VersionConverter - { - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - object? obj = base.ReadJson(reader, objectType, existingValue, serializer); - if (obj is not Version v) - return obj; - - int major = v.Major == -1 ? 0 : v.Major; - int minor = v.Minor == -1 ? 0 : v.Minor; - int build = v.Build == -1 ? 0 : v.Build; - int revision = v.Revision == -1 ? 0 : v.Revision; - return new Version(major, minor, build, revision); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs b/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs index 0dea83b5e..3d018c4c2 100644 --- a/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs +++ b/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs @@ -1,24 +1,26 @@ using System; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; -namespace Artemis.Core.JsonConverters; - -internal class NumericJsonConverter : JsonConverter +namespace Artemis.Core.JsonConverters { - #region Overrides of JsonConverter - - /// - public override void WriteJson(JsonWriter writer, Numeric value, JsonSerializer serializer) + internal class NumericJsonConverter : JsonConverter { - float floatValue = value; - writer.WriteValue(floatValue); - } + public override void Write(Utf8JsonWriter writer, Numeric value, JsonSerializerOptions options) + { + float floatValue = value; + writer.WriteNumberValue(floatValue); + } - /// - public override Numeric ReadJson(JsonReader reader, Type objectType, Numeric existingValue, bool hasExistingValue, JsonSerializer serializer) - { - return new Numeric(reader.Value); - } + public override Numeric Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.Number) + { + throw new JsonException($"Expected a number token, but got {reader.TokenType}."); + } - #endregion + float floatValue = reader.GetSingle(); + return new Numeric(floatValue); + } + } } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/SKColorConverter.cs b/src/Artemis.Core/JsonConverters/SKColorConverter.cs index 40912c153..b2818d754 100644 --- a/src/Artemis.Core/JsonConverters/SKColorConverter.cs +++ b/src/Artemis.Core/JsonConverters/SKColorConverter.cs @@ -1,21 +1,26 @@ using System; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using SkiaSharp; -namespace Artemis.Core.JsonConverters; - -internal class SKColorConverter : JsonConverter +namespace Artemis.Core.JsonConverters { - public override void WriteJson(JsonWriter writer, SKColor value, JsonSerializer serializer) + internal class SKColorConverter : JsonConverter { - writer.WriteValue(value.ToString()); - } + public override void Write(Utf8JsonWriter writer, SKColor value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } - public override SKColor ReadJson(JsonReader reader, Type objectType, SKColor existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.Value is string value && !string.IsNullOrWhiteSpace(value)) - return SKColor.Parse(value); + public override SKColor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Expected a string token, but got {reader.TokenType}."); + } - return SKColor.Empty; + string colorString = reader.GetString() ?? string.Empty; + return SKColor.TryParse(colorString, out SKColor color) ? color : SKColor.Empty; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/JsonConverters/StreamConverter.cs b/src/Artemis.Core/JsonConverters/StreamConverter.cs deleted file mode 100644 index 7ce3529ee..000000000 --- a/src/Artemis.Core/JsonConverters/StreamConverter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; - -namespace Artemis.Core.JsonConverters; - -/// -public class StreamConverter : JsonConverter -{ - #region Overrides of JsonConverter - - /// - public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - return; - } - - using MemoryStream memoryStream = new(); - value.Position = 0; - value.CopyTo(memoryStream); - writer.WriteValue(memoryStream.ToArray()); - } - - /// - public override Stream? ReadJson(JsonReader reader, Type objectType, Stream? existingValue, bool hasExistingValue, JsonSerializer serializer) - { - if (reader.Value is not string base64) - return null; - - if (existingValue == null || !hasExistingValue || !existingValue.CanRead) - return new MemoryStream(Convert.FromBase64String(base64)); - - using MemoryStream memoryStream = new(Convert.FromBase64String(base64)); - existingValue.Position = 0; - memoryStream.CopyTo(existingValue); - existingValue.Position = 0; - return existingValue; - } - - #endregion -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index ec3496955..2db77c92b 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -164,7 +164,7 @@ public Folder CreateCopy() if (Parent == null) throw new ArtemisCoreException("Cannot create a copy of a folder without a parent"); - FolderEntity entityCopy = CoreJson.DeserializeObject(CoreJson.SerializeObject(FolderEntity, true), true)!; + FolderEntity entityCopy = CoreJson.Deserialize(CoreJson.Serialize(FolderEntity))!; entityCopy.Id = Guid.NewGuid(); entityCopy.Name += " - Copy"; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index ff9a7d514..5ecfeeca6 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Text.Json; using Artemis.Storage.Entities.Profile; -using Newtonsoft.Json; namespace Artemis.Core; @@ -324,8 +324,8 @@ public void ApplyDefaultValue() // Reference types make a deep clone (ab)using JSON else { - string json = CoreJson.SerializeObject(DefaultValue, true); - SetCurrentValue(CoreJson.DeserializeObject(json)!); + string json = CoreJson.Serialize(DefaultValue); + SetCurrentValue(CoreJson.Deserialize(json)!); } } @@ -420,7 +420,7 @@ public void AddKeyframe(LayerPropertyKeyframe keyframe) try { - T? value = CoreJson.DeserializeObject(keyframeEntity.Value); + T? value = CoreJson.Deserialize(keyframeEntity.Value); if (value == null) return null; @@ -625,7 +625,7 @@ public void Load() try { if (Entity.Value != null) - BaseValue = CoreJson.DeserializeObject(Entity.Value)!; + BaseValue = CoreJson.Deserialize(Entity.Value)!; } catch (JsonException) { @@ -664,7 +664,7 @@ public void Save() if (!_isInitialized) throw new ArtemisCoreException("Layer property is not yet initialized"); - Entity.Value = CoreJson.SerializeObject(BaseValue); + Entity.Value = CoreJson.Serialize(BaseValue); Entity.KeyframesEnabled = KeyframesEnabled; Entity.KeyframeEntities.Clear(); Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index b6f37c9ea..b5ceaa139 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -69,7 +69,7 @@ public KeyframeEntity GetKeyframeEntity() { return new KeyframeEntity { - Value = CoreJson.SerializeObject(Value), + Value = CoreJson.Serialize(Value), Position = Position, EasingFunction = (int) EasingFunction }; diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs deleted file mode 100644 index 075c5f54b..000000000 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.IO; -using Artemis.Core.JsonConverters; -using Artemis.Storage.Entities.Profile; -using Newtonsoft.Json; - -namespace Artemis.Core; - -/// -/// A model that can be used to serialize a profile configuration, it's profile and it's icon -/// -public class ProfileConfigurationExportModel : IDisposable -{ - /// - /// Gets or sets the storage entity of the profile configuration - /// - public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; } - - /// - /// Gets or sets the storage entity of the profile - /// - [JsonProperty(Required = Required.Always)] - public ProfileEntity ProfileEntity { get; set; } = null!; - - /// - /// Gets or sets a stream containing the profile image - /// - [JsonConverter(typeof(StreamConverter))] - public Stream? ProfileImage { get; set; } - - /// - public void Dispose() - { - ProfileImage?.Dispose(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/DataModel.cs b/src/Artemis.Core/Plugins/Modules/DataModel.cs index 6d80e3b29..1bdf68094 100644 --- a/src/Artemis.Core/Plugins/Modules/DataModel.cs +++ b/src/Artemis.Core/Plugins/Modules/DataModel.cs @@ -4,8 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; using Humanizer; -using Newtonsoft.Json; namespace Artemis.Core.Modules; diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index ccd9f4c9f..6c084ff51 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -3,14 +3,12 @@ using System.Linq; using Artemis.Storage.Entities.Plugins; using Humanizer; -using Newtonsoft.Json; namespace Artemis.Core; /// /// Represents basic info about a plugin feature and contains a reference to the instance of said feature /// -[JsonObject(MemberSerialization.OptIn)] public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject { private string? _description; @@ -64,7 +62,6 @@ public Exception? LoadException /// /// The name of the feature /// - [JsonProperty(Required = Required.Always)] public string Name { get => _name; @@ -74,7 +71,6 @@ public string Name /// /// A short description of the feature /// - [JsonProperty] public string? Description { get => _description; @@ -85,7 +81,6 @@ public string? Description /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. /// Note: always if this is the plugin's only feature /// - [JsonProperty] public bool AlwaysEnabled { get; internal set; } /// diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 91e49eeab..641471345 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using Artemis.Core.JsonConverters; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Artemis.Core; /// /// Represents basic info about a plugin and contains a reference to the instance of said plugin /// -[JsonObject(MemberSerialization.OptIn)] public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject { private Version? _api; @@ -28,10 +25,11 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject private string _version = null!; private Uri? _website; private Uri? _helpPage; - private bool _hotReloadSupported; + private bool _hotReloadSupported = true; private Uri? _license; private string? _licenseName; + [JsonConstructor] internal PluginInfo() { } @@ -39,7 +37,8 @@ internal PluginInfo() /// /// The plugins GUID /// - [JsonProperty(Required = Required.Always)] + [JsonRequired] + [JsonInclude] public Guid Guid { get => _guid; @@ -49,7 +48,8 @@ public Guid Guid /// /// The name of the plugin /// - [JsonProperty(Required = Required.Always)] + [JsonRequired] + [JsonInclude] public string Name { get => _name; @@ -59,7 +59,6 @@ public string Name /// /// A short description of the plugin /// - [JsonProperty] public string? Description { get => _description; @@ -69,7 +68,6 @@ public string? Description /// /// Gets or sets the author of this plugin /// - [JsonProperty] public string? Author { get => _author; @@ -79,7 +77,6 @@ public string? Author /// /// Gets or sets the website of this plugin or its author /// - [JsonProperty] public Uri? Website { get => _website; @@ -89,7 +86,6 @@ public Uri? Website /// /// Gets or sets the repository of this plugin /// - [JsonProperty] public Uri? Repository { get => _repository; @@ -99,7 +95,6 @@ public Uri? Repository /// /// Gets or sets the help page of this plugin /// - [JsonProperty] public Uri? HelpPage { get => _helpPage; @@ -109,7 +104,6 @@ public Uri? HelpPage /// /// Gets or sets the help page of this plugin /// - [JsonProperty] public Uri? License { get => _license; @@ -119,7 +113,6 @@ public Uri? License /// /// Gets or sets the author of this plugin /// - [JsonProperty] public string? LicenseName { get => _licenseName; @@ -130,7 +123,6 @@ public string? LicenseName /// The plugins display icon that's shown in the settings see for /// available icons /// - [JsonProperty] public string? Icon { get => _icon; @@ -140,7 +132,8 @@ public string? Icon /// /// The version of the plugin /// - [JsonProperty(Required = Required.Always)] + [JsonRequired] + [JsonInclude] public string Version { get => _version; @@ -150,7 +143,8 @@ public string Version /// /// The main entry DLL, should contain a class implementing Plugin /// - [JsonProperty(Required = Required.Always)] + [JsonRequired] + [JsonInclude] public string Main { get => _main; @@ -161,8 +155,6 @@ public string Main /// Gets or sets a boolean indicating whether this plugin should automatically enable all its features when it is first /// loaded /// - [DefaultValue(true)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] public bool AutoEnableFeatures { get => _autoEnableFeatures; @@ -172,7 +164,7 @@ public bool AutoEnableFeatures /// /// Gets a boolean indicating whether this plugin requires elevated admin privileges /// - [JsonProperty] + [JsonInclude] public bool RequiresAdmin { get => _requiresAdmin; @@ -182,8 +174,6 @@ public bool RequiresAdmin /// /// Gets or sets a boolean indicating whether hot reloading this plugin is supported /// - [DefaultValue(true)] - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] public bool HotReloadSupported { get => _hotReloadSupported; @@ -193,7 +183,7 @@ public bool HotReloadSupported /// /// Gets /// - [JsonProperty] + [JsonInclude] public PluginPlatform? Platforms { get => _platforms; @@ -203,8 +193,7 @@ public PluginPlatform? Platforms /// /// Gets the API version the plugin was built for /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - [JsonConverter(typeof(ForgivingVersionConverter))] + [JsonInclude] public Version? Api { get => _api; @@ -214,6 +203,7 @@ public Version? Api /// /// Gets the plugin this info is associated with /// + [JsonIgnore] public Plugin Plugin { get => _plugin; @@ -223,6 +213,7 @@ public Plugin Plugin /// /// Gets a string representing either a full path pointing to an svg or the markdown icon /// + [JsonIgnore] public string? ResolvedIcon { get @@ -247,9 +238,11 @@ public override string ToString() } /// + [JsonIgnore] public List Prerequisites { get; } = new(); /// + [JsonIgnore] public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); /// diff --git a/src/Artemis.Core/Plugins/PluginPlatform.cs b/src/Artemis.Core/Plugins/PluginPlatform.cs index 6f5ce1c34..bc10a703f 100644 --- a/src/Artemis.Core/Plugins/PluginPlatform.cs +++ b/src/Artemis.Core/Plugins/PluginPlatform.cs @@ -1,6 +1,5 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; namespace Artemis.Core; @@ -8,7 +7,7 @@ namespace Artemis.Core; /// Specifies OS platforms a plugin may support. /// [Flags] -[JsonConverter(typeof(StringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum PluginPlatform { /// The Windows platform. diff --git a/src/Artemis.Core/Plugins/Settings/PluginSetting.cs b/src/Artemis.Core/Plugins/Settings/PluginSetting.cs index cd94e48d6..8f14d542f 100644 --- a/src/Artemis.Core/Plugins/Settings/PluginSetting.cs +++ b/src/Artemis.Core/Plugins/Settings/PluginSetting.cs @@ -1,7 +1,7 @@ using System; +using System.Text.Json; using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Repositories.Interfaces; -using Newtonsoft.Json; namespace Artemis.Core; @@ -23,9 +23,9 @@ internal PluginSetting(IPluginRepository pluginRepository, PluginSettingEntity p Name = pluginSettingEntity.Name; try { - _value = CoreJson.DeserializeObject(pluginSettingEntity.Value)!; + _value = CoreJson.Deserialize(pluginSettingEntity.Value)!; } - catch (JsonReaderException) + catch (JsonException) { _value = default!; } @@ -76,7 +76,7 @@ protected internal virtual void OnSettingSaved() public string Name { get; } /// - public bool HasChanged => CoreJson.SerializeObject(Value) != _pluginSettingEntity.Value; + public bool HasChanged => CoreJson.Serialize(Value) != _pluginSettingEntity.Value; /// public bool AutoSave { get; set; } @@ -84,7 +84,7 @@ protected internal virtual void OnSettingSaved() /// public void RejectChanges() { - Value = CoreJson.DeserializeObject(_pluginSettingEntity.Value); + Value = CoreJson.Deserialize(_pluginSettingEntity.Value); } /// @@ -93,7 +93,7 @@ public void Save() if (!HasChanged) return; - _pluginSettingEntity.Value = CoreJson.SerializeObject(Value); + _pluginSettingEntity.Value = CoreJson.Serialize(Value); _pluginRepository.SaveSetting(_pluginSettingEntity); OnSettingSaved(); } diff --git a/src/Artemis.Core/Plugins/Settings/PluginSettings.cs b/src/Artemis.Core/Plugins/Settings/PluginSettings.cs index 84061e48e..89c1a10ea 100644 --- a/src/Artemis.Core/Plugins/Settings/PluginSettings.cs +++ b/src/Artemis.Core/Plugins/Settings/PluginSettings.cs @@ -49,7 +49,7 @@ public PluginSetting GetSetting(string name, T? defaultValue = default) { Name = name, PluginGuid = Plugin.Guid, - Value = CoreJson.SerializeObject(defaultValue) + Value = CoreJson.Serialize(defaultValue) }; _pluginRepository.AddSetting(settingEntity); } diff --git a/src/Artemis.Core/Services/Core/BuildInfo.cs b/src/Artemis.Core/Services/Core/BuildInfo.cs deleted file mode 100644 index 2c694d67a..000000000 --- a/src/Artemis.Core/Services/Core/BuildInfo.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Globalization; -using Newtonsoft.Json; - -namespace Artemis.Core.Services.Core; - -/// -/// Represents build information related to the currently running Artemis build -/// -public class BuildInfo -{ - /// - /// Gets the unique ID of this build - /// - [JsonProperty] - public int BuildId { get; internal set; } - - /// - /// Gets the build number. This contains the date and the build count for that day. - /// Per example 20210108.4 - /// - [JsonProperty] - public double BuildNumber { get; internal set; } - - /// - /// Gets the build number formatted as a string. This contains the date and the build count for that day. - /// Per example 20210108.4 - /// - public string BuildNumberDisplay => BuildNumber.ToString(CultureInfo.InvariantCulture); - - /// - /// Gets the branch of the triggering repo the build was created for. - /// - [JsonProperty] - public string SourceBranch { get; internal set; } = null!; - - /// - /// Gets the commit ID used to create this build - /// - [JsonProperty] - public string SourceVersion { get; internal set; } = null!; - - /// - /// Gets a boolean indicating whether the current build is a local build - /// - public bool IsLocalBuild { get; internal set; } -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index 1050484c0..e72ea6882 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; -using Newtonsoft.Json; using Serilog; namespace Artemis.Core.Services; diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 96a0b617b..79f118749 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -4,7 +4,6 @@ using System.Security.Cryptography; using System.Text; using Artemis.Storage.Entities.Profile.Nodes; -using Newtonsoft.Json; using SkiaSharp; namespace Artemis.Core.Services; @@ -42,12 +41,12 @@ public TypeColorRegistration GetTypeColorRegistration(Type type) public string ExportScript(NodeScript nodeScript) { nodeScript.Save(); - return JsonConvert.SerializeObject(nodeScript.Entity, IProfileService.ExportSettings); + return CoreJson.Serialize(nodeScript.Entity); } public void ImportScript(string json, NodeScript target) { - NodeScriptEntity? entity = JsonConvert.DeserializeObject(json); + NodeScriptEntity? entity = CoreJson.Deserialize(json); if (entity == null) throw new ArtemisCoreException("Failed to load node script"); diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 75cd31cb6..67e3083e4 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -127,7 +127,7 @@ private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirector throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName); using StreamReader reader = new(metaDataFileEntry.Open()); - PluginInfo builtInPluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; + PluginInfo builtInPluginInfo = CoreJson.Deserialize(reader.ReadToEnd())!; string preferred = builtInPluginInfo.PreferredPluginDirectory; // Find the matching plugin in the plugin folder @@ -360,7 +360,7 @@ public void UnloadPlugins() _logger.Warning(new ArtemisPluginException("Couldn't find the plugins metadata file at " + metadataFile), "Plugin exception"); // PluginInfo contains the ID which we need to move on - PluginInfo pluginInfo = CoreJson.DeserializeObject(File.ReadAllText(metadataFile))!; + PluginInfo pluginInfo = CoreJson.Deserialize(File.ReadAllText(metadataFile))!; if (!pluginInfo.IsCompatible) return null; @@ -610,7 +610,7 @@ public void DisablePlugin(Plugin plugin, bool saveState) throw new ArtemisPluginException("Couldn't find a plugin.json in " + fileName); using StreamReader reader = new(metaDataFileEntry.Open()); - PluginInfo pluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; + PluginInfo pluginInfo = CoreJson.Deserialize(reader.ReadToEnd())!; if (!pluginInfo.Main.EndsWith(".dll")) throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file"); diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 4e8d357fa..2d2dfae7c 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -2,7 +2,6 @@ using System.Collections.ObjectModel; using System.IO; using System.Threading.Tasks; -using Newtonsoft.Json; using SkiaSharp; namespace Artemis.Core.Services; @@ -12,11 +11,6 @@ namespace Artemis.Core.Services; /// public interface IProfileService : IArtemisService { - /// - /// Gets the JSON serializer settings used to import/export profiles. - /// - public static JsonSerializerSettings ExportSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; - /// /// Gets a read only collection containing all the profile categories. /// diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index d9f50cd1a..5cafbd7db 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -5,13 +5,13 @@ using System.IO.Compression; using System.Linq; using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Migrations; using Artemis.Storage.Repositories.Interfaces; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Serilog; using SkiaSharp; @@ -410,8 +410,8 @@ public async Task ExportProfile(ProfileConfiguration profileConfiguratio if (profileEntity == null) throw new ArtemisCoreException("Could not locate profile entity"); - string configurationJson = JsonConvert.SerializeObject(profileConfiguration.Entity, IProfileService.ExportSettings); - string profileJson = JsonConvert.SerializeObject(profileEntity, IProfileService.ExportSettings); + string configurationJson = CoreJson.Serialize(profileConfiguration.Entity); + string profileJson = CoreJson.Serialize(profileEntity); MemoryStream archiveStream = new(); @@ -461,21 +461,21 @@ public async Task ImportProfile(Stream archiveStream, Prof // Deserialize profile configuration to JObject await using Stream configurationStream = configurationEntry.Open(); using StreamReader configurationReader = new(configurationStream); - JObject? configurationJson = JsonConvert.DeserializeObject(await configurationReader.ReadToEndAsync(), IProfileService.ExportSettings); + JsonObject? configurationJson = CoreJson.Deserialize(await configurationReader.ReadToEndAsync()); // Deserialize profile to JObject await using Stream profileStream = profileEntry.Open(); using StreamReader profileReader = new(profileStream); - JObject? profileJson = JsonConvert.DeserializeObject(await profileReader.ReadToEndAsync(), IProfileService.ExportSettings); + JsonObject? profileJson = CoreJson.Deserialize(await profileReader.ReadToEndAsync()); // Before deserializing, apply any pending migrations MigrateProfile(configurationJson, profileJson); // Deserialize profile configuration to ProfileConfigurationEntity - ProfileConfigurationEntity? configurationEntity = configurationJson?.ToObject(JsonSerializer.Create(IProfileService.ExportSettings)); + ProfileConfigurationEntity? configurationEntity = configurationJson?.Deserialize(Constants.JsonConvertSettings); if (configurationEntity == null) throw new ArtemisCoreException("Could not import profile, failed to deserialize configuration.json"); // Deserialize profile to ProfileEntity - ProfileEntity? profileEntity = profileJson?.ToObject(JsonSerializer.Create(IProfileService.ExportSettings)); + ProfileEntity? profileEntity = profileJson?.Deserialize(Constants.JsonConvertSettings); if (profileEntity == null) throw new ArtemisCoreException("Could not import profile, failed to deserialize profile.json"); @@ -559,7 +559,7 @@ private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEvent } } - private void MigrateProfile(JObject? configurationJson, JObject? profileJson) + private void MigrateProfile(JsonObject? configurationJson, JsonObject? profileJson) { if (configurationJson == null || profileJson == null) return; @@ -568,7 +568,7 @@ private void MigrateProfile(JObject? configurationJson, JObject? profileJson) foreach (IProfileMigration profileMigrator in _profileMigrators.OrderBy(m => m.Version)) { - if (profileMigrator.Version <= configurationJson["Version"]!.Value()) + if (profileMigrator.Version <= configurationJson["Version"]!.GetValue()) continue; profileMigrator.Migrate(configurationJson, profileJson); diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs index e9aa58ad1..438a638fc 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/DataModelJsonPluginEndPoint.cs @@ -1,9 +1,13 @@ -using System; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Artemis.Core.Modules; using EmbedIO; -using Newtonsoft.Json; namespace Artemis.Core.Services; @@ -15,10 +19,12 @@ namespace Artemis.Core.Services; public class DataModelJsonPluginEndPoint : PluginEndPoint where T : DataModel, new() { private readonly Module _module; + private readonly Action _update; internal DataModelJsonPluginEndPoint(Module module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule) { _module = module ?? throw new ArgumentNullException(nameof(module)); + _update = CreateUpdateAction(); ThrowOnFail = true; Accepts = MimeType.Json; @@ -31,8 +37,6 @@ internal DataModelJsonPluginEndPoint(Module module, string name, PluginsModul /// public bool ThrowOnFail { get; set; } - #region Overrides of PluginEndPoint - /// protected override async Task ProcessRequest(IHttpContext context) { @@ -44,7 +48,9 @@ protected override async Task ProcessRequest(IHttpContext context) using TextReader reader = context.OpenRequestText(); try { - JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel); + T? dataModel = CoreJson.Deserialize(await reader.ReadToEndAsync()); + if (dataModel != null) + _update(dataModel, _module.DataModel); } catch (JsonException) { @@ -53,5 +59,26 @@ protected override async Task ProcessRequest(IHttpContext context) } } - #endregion + private Action CreateUpdateAction() + { + ParameterExpression sourceParameter = Expression.Parameter(typeof(T), "source"); + ParameterExpression targetParameter = Expression.Parameter(typeof(T), "target"); + + IEnumerable assignments = typeof(T) + .GetProperties() + .Where(prop => prop.CanWrite && prop.GetSetMethod() != null && + prop.GetSetMethod()!.IsPublic && + !prop.IsDefined(typeof(JsonIgnoreAttribute), false) && + !prop.PropertyType.IsAssignableTo(typeof(IDataModelEvent))) + .Select(prop => + { + MemberExpression sourceProperty = Expression.Property(sourceParameter, prop); + MemberExpression targetProperty = Expression.Property(targetParameter, prop); + return Expression.Assign(targetProperty, sourceProperty); + }); + + BlockExpression body = Expression.Block(assignments); + + return Expression.Lambda>(body, sourceParameter, targetParameter).Compile(); + } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs index 355a233ec..2fc5c6977 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/JsonPluginEndPoint.cs @@ -1,8 +1,8 @@ using System; using System.IO; +using System.Text.Json; using System.Threading.Tasks; using EmbedIO; -using Newtonsoft.Json; namespace Artemis.Core.Services; @@ -52,7 +52,7 @@ protected override async Task ProcessRequest(IHttpContext context) object? response = null; try { - T? deserialized = JsonConvert.DeserializeObject(await reader.ReadToEndAsync()); + T? deserialized = JsonSerializer.Deserialize(await reader.ReadToEndAsync()); if (deserialized == null) throw new JsonException("Deserialization returned null"); @@ -74,7 +74,7 @@ protected override async Task ProcessRequest(IHttpContext context) } await using TextWriter writer = context.OpenResponseText(); - await writer.WriteAsync(JsonConvert.SerializeObject(response)); + await writer.WriteAsync(JsonSerializer.Serialize(response)); } #endregion diff --git a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs index 55cbbd3d9..fed0e033a 100644 --- a/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs +++ b/src/Artemis.Core/Services/WebServer/EndPoints/PluginEndPoint.cs @@ -1,7 +1,7 @@ using System; +using System.Text.Json.Serialization; using System.Threading.Tasks; using EmbedIO; -using Newtonsoft.Json; namespace Artemis.Core.Services; diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index d185d33c9..e65d626fe 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -43,7 +43,7 @@ public interface IWebServerService : IArtemisService /// /// The resulting end point JsonPluginEndPoint AddResponsiveJsonEndPoint(PluginFeature feature, string endPointName, Func requestHandler); - + /// /// Adds a new endpoint that directly maps received JSON to the data model of the provided . /// @@ -51,8 +51,9 @@ public interface IWebServerService : IArtemisService /// The module whose datamodel to apply the received JSON to /// The name of the end point, must be unique /// The resulting end point + [Obsolete("This way of updating is too unpredictable in combination with nested events, use AddJsonEndPoint to update manually instead")] DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(Module module, string endPointName) where T : DataModel, new(); - + /// /// Adds a new endpoint for the given plugin feature receiving an a . /// diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 263d95315..e0ab36288 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -3,12 +3,13 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Artemis.Core.Modules; using EmbedIO; using EmbedIO.WebApi; -using Newtonsoft.Json; using Serilog; namespace Artemis.Core.Services; @@ -22,6 +23,7 @@ internal class WebServerService : IWebServerService, IDisposable private readonly PluginSetting _webServerEnabledSetting; private readonly PluginSetting _webServerPortSetting; private readonly object _webserverLock = new(); + private readonly JsonSerializerOptions _jsonOptions = new(CoreJson.GetJsonSerializerOptions()) {ReferenceHandler = ReferenceHandler.IgnoreCycles, WriteIndented = true}; private CancellationTokenSource? _cts; public WebServerService(ILogger logger, ICoreService coreService, ISettingsService settingsService, IPluginManagementService pluginManagementService) @@ -120,7 +122,7 @@ private WebServer CreateWebServer() Server = null; } - WebApiModule apiModule = new("/", JsonNetSerializer); + WebApiModule apiModule = new("/", SystemTextJsonSerializer); PluginsModule.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/"; WebServer server = new WebServer(o => o.WithUrlPrefix($"http://*:{_webServerPortSetting.Value}/").WithMode(HttpListenerMode.EmbedIO)) .WithLocalSessionManager() @@ -138,7 +140,7 @@ private WebServer CreateWebServer() // Add registered controllers to the API module foreach (WebApiControllerRegistration registration in _controllers) apiModule.RegisterController(registration.ControllerType, (Func) registration.UntypedFactory); - + // Listen for state changes. server.StateChanged += (s, e) => _logger.Verbose("WebServer new state - {state}", e.NewState); @@ -173,7 +175,7 @@ private void StartWebServer() OnWebServerStarted(); } } - + private void AutoStartWebServer() { try @@ -240,6 +242,7 @@ public RawPluginEndPoint AddRawEndPoint(PluginFeature feature, string endPointNa return endPoint; } + [Obsolete("Use AddJsonEndPoint(PluginFeature feature, string endPointName, Action requestHandler) instead")] public DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(Module module, string endPointName) where T : DataModel, new() { if (module == null) throw new ArgumentNullException(nameof(module)); @@ -316,7 +319,7 @@ private async Task JsonExceptionHandlerCallback(IHttpContext context, Exception context.Response.ContentType = MimeType.Json; await using TextWriter writer = context.OpenResponseText(); - string response = JsonConvert.SerializeObject(new Dictionary + string response = CoreJson.Serialize(new Dictionary { {"StatusCode", context.Response.StatusCode}, {"StackTrace", exception.StackTrace}, @@ -331,17 +334,16 @@ private async Task JsonExceptionHandlerCallback(IHttpContext context, Exception await writer.WriteAsync(response); } - private async Task JsonNetSerializer(IHttpContext context, object? data) + private async Task SystemTextJsonSerializer(IHttpContext context, object? data) { context.Response.ContentType = MimeType.Json; await using TextWriter writer = context.OpenResponseText(); - string json = JsonConvert.SerializeObject(data, new JsonSerializerSettings {PreserveReferencesHandling = PreserveReferencesHandling.Objects}); - await writer.WriteAsync(json); + await writer.WriteAsync(JsonSerializer.Serialize(data, _jsonOptions)); } private async Task HandleHttpExceptionJson(IHttpContext context, IHttpException httpException) { - await context.SendStringAsync(JsonConvert.SerializeObject(httpException, Formatting.Indented), MimeType.Json, Encoding.UTF8); + await context.SendStringAsync(JsonSerializer.Serialize(httpException, _jsonOptions), MimeType.Json, Encoding.UTF8); } #endregion diff --git a/src/Artemis.Core/Utilities/CoreJson.cs b/src/Artemis.Core/Utilities/CoreJson.cs index 1cb870e61..fa9477073 100644 --- a/src/Artemis.Core/Utilities/CoreJson.cs +++ b/src/Artemis.Core/Utilities/CoreJson.cs @@ -1,7 +1,6 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; +using System.Text.Json; namespace Artemis.Core; @@ -10,62 +9,36 @@ namespace Artemis.Core; /// public static class CoreJson { - #region Serialize - /// /// Serializes the specified object to a JSON string. /// /// The object to serialize. - /// If set to true sets TypeNameHandling to /// A JSON string representation of the object. [DebuggerStepThrough] - public static string SerializeObject(object? value, bool handleTypeNames = false) - { - return JsonConvert.SerializeObject(value, handleTypeNames ? Constants.JsonConvertTypedSettings : Constants.JsonConvertSettings); - } - - #endregion - - #region Deserialize - - /// - /// Deserializes the JSON to a .NET object. - /// - /// The JSON to deserialize. - /// If set to true sets TypeNameHandling to - /// The deserialized object from the JSON string. - [DebuggerStepThrough] - public static object? DeserializeObject(string value, bool handleTypeNames = false) + public static string Serialize(object? value) { - return JsonConvert.DeserializeObject(value, handleTypeNames ? Constants.JsonConvertTypedSettings : Constants.JsonConvertSettings); + return JsonSerializer.Serialize(value, Constants.JsonConvertSettings); } - + /// /// Deserializes the JSON to the specified .NET type. /// + /// The type of the object to deserialize to. /// The JSON to deserialize. - /// The of object being deserialized. - /// If set to true sets TypeNameHandling to /// The deserialized object from the JSON string. [DebuggerStepThrough] - public static object? DeserializeObject(string value, Type type, bool handleTypeNames = false) + [return: MaybeNull] + public static T Deserialize(string value) { - return JsonConvert.DeserializeObject(value, type, handleTypeNames ? Constants.JsonConvertTypedSettings : Constants.JsonConvertSettings); + return JsonSerializer.Deserialize(value, Constants.JsonConvertSettings); } - + /// - /// Deserializes the JSON to the specified .NET type. + /// Gets a copy of the JSON serializer options used by Artemis Core /// - /// The type of the object to deserialize to. - /// The JSON to deserialize. - /// If set to true sets TypeNameHandling to - /// The deserialized object from the JSON string. - [DebuggerStepThrough] - [return: MaybeNull] - public static T DeserializeObject(string value, bool handleTypeNames = false) + /// A copy of the JSON serializer options used by Artemis Core + public static JsonSerializerOptions GetJsonSerializerOptions() { - return JsonConvert.DeserializeObject(value, handleTypeNames ? Constants.JsonConvertTypedSettings : Constants.JsonConvertSettings); + return new JsonSerializerOptions(Constants.JsonConvertSettings); } - - #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs index 81d9e7eac..3d091a4b1 100644 --- a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs @@ -41,12 +41,12 @@ public TStorage? Storage /// public override string SerializeStorage() { - return CoreJson.SerializeObject(Storage, true); + return CoreJson.Serialize(Storage); } /// public override void DeserializeStorage(string serialized) { - Storage = CoreJson.DeserializeObject(serialized) ?? default(TStorage); + Storage = CoreJson.Deserialize(serialized) ?? default(TStorage); } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Pins/InputPin.cs b/src/Artemis.Core/VisualScripting/Pins/InputPin.cs index 8addc4627..a183885c5 100644 --- a/src/Artemis.Core/VisualScripting/Pins/InputPin.cs +++ b/src/Artemis.Core/VisualScripting/Pins/InputPin.cs @@ -1,5 +1,4 @@ using System; -using Newtonsoft.Json; namespace Artemis.Core; @@ -10,7 +9,6 @@ public sealed class InputPin : Pin { #region Constructors - [JsonConstructor] internal InputPin(INode node, string name) : base(node, name) { diff --git a/src/Artemis.Core/VisualScripting/Pins/OutputPin.cs b/src/Artemis.Core/VisualScripting/Pins/OutputPin.cs index 52bd0993a..8397eddf6 100644 --- a/src/Artemis.Core/VisualScripting/Pins/OutputPin.cs +++ b/src/Artemis.Core/VisualScripting/Pins/OutputPin.cs @@ -1,5 +1,4 @@ using System; -using Newtonsoft.Json; namespace Artemis.Core; @@ -10,7 +9,6 @@ public sealed class OutputPin : Pin { #region Constructors - [JsonConstructor] internal OutputPin(INode node, string name) : base(node, name) { diff --git a/src/Artemis.Storage/Artemis.Storage.csproj b/src/Artemis.Storage/Artemis.Storage.csproj index cc0ac235f..b01c7ef62 100644 --- a/src/Artemis.Storage/Artemis.Storage.csproj +++ b/src/Artemis.Storage/Artemis.Storage.csproj @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs index 57c914786..56444bd86 100644 --- a/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs @@ -1,3 +1,9 @@ -namespace Artemis.Storage.Entities.Profile.AdaptionHints; +using System.Text.Json.Serialization; +namespace Artemis.Storage.Entities.Profile.AdaptionHints; + +[JsonDerivedType(typeof(CategoryAdaptionHintEntity), "Category")] +[JsonDerivedType(typeof(DeviceAdaptionHintEntity), "Device")] +[JsonDerivedType(typeof(KeyboardSectionAdaptionHintEntity), "KeyboardSection")] +[JsonDerivedType(typeof(SingleLedAdaptionHintEntity), "SingleLed")] public interface IAdaptionHintEntity; \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs index f1f24ee14..93fc27834 100644 --- a/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs @@ -1,3 +1,10 @@ -namespace Artemis.Storage.Entities.Profile.Abstract; +using System.Text.Json.Serialization; +using Artemis.Storage.Entities.Profile.Conditions; +namespace Artemis.Storage.Entities.Profile.Abstract; + +[JsonDerivedType(typeof(AlwaysOnConditionEntity), "AlwaysOn")] +[JsonDerivedType(typeof(EventConditionEntity), "Event")] +[JsonDerivedType(typeof(PlayOnceConditionEntity), "PlayOnce")] +[JsonDerivedType(typeof(StaticConditionEntity), "Static")] public interface IConditionEntity; \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs index a3eeef113..aa20adf7f 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs @@ -1,5 +1,6 @@ using System; using Artemis.Storage.Entities.Profile.Nodes; +using Serilog.Core; namespace Artemis.Storage.Entities.Profile; @@ -26,5 +27,5 @@ public class ProfileConfigurationEntity public Guid ProfileId { get; set; } public bool FadeInAndOut { get; set; } - public int Version { get; set; } + public int Version { get; set; } = StorageMigrationService.PROFILE_VERSION; } \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/IProfileMigration.cs b/src/Artemis.Storage/Migrations/IProfileMigration.cs index c4fb710ec..d0e1b2126 100644 --- a/src/Artemis.Storage/Migrations/IProfileMigration.cs +++ b/src/Artemis.Storage/Migrations/IProfileMigration.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Artemis.Storage.Migrations; public interface IProfileMigration { int Version { get; } - void Migrate(JObject configurationJson, JObject profileJson); + void Migrate(JsonObject configurationJson, JsonObject profileJson); } \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/Profile/M0001NodeProviders.cs b/src/Artemis.Storage/Migrations/Profile/M0001NodeProviders.cs index a1006bab8..701ab91ee 100644 --- a/src/Artemis.Storage/Migrations/Profile/M0001NodeProviders.cs +++ b/src/Artemis.Storage/Migrations/Profile/M0001NodeProviders.cs @@ -1,88 +1,91 @@ -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; -namespace Artemis.Storage.Migrations.Profile; - -/// -/// Migrates nodes to be provider-based. -/// This requires giving them a ProviderId and updating the their namespaces to match the namespace of the new plugin. -/// -internal class M0001NodeProviders : IProfileMigration +namespace Artemis.Storage.Migrations.Profile { - /// - public int Version => 1; - - /// - public void Migrate(JObject configurationJson, JObject profileJson) + /// + /// Migrates nodes to be provider-based. + /// This requires giving them a ProviderId and updating the their namespaces to match the namespace of the new plugin. + /// + internal class M0001NodeProviders : IProfileMigration { - JArray? folders = (JArray?) profileJson["Folders"]?["$values"]; - JArray? layers = (JArray?) profileJson["Layers"]?["$values"]; + /// + public int Version => 1; - if (folders != null) + /// + public void Migrate(JsonObject configurationJson, JsonObject profileJson) { - foreach (JToken folder in folders) - MigrateProfileElement(folder); - } + JsonArray? folders = profileJson["Folders"]?["$values"]?.AsArray(); + JsonArray? layers = profileJson["Layers"]?["$values"]?.AsArray(); - if (layers != null) - { - foreach (JToken layer in layers) + if (folders != null) { - MigrateProfileElement(layer); - MigratePropertyGroup(layer["GeneralPropertyGroup"]); - MigratePropertyGroup(layer["TransformPropertyGroup"]); - MigratePropertyGroup(layer["LayerBrush"]?["PropertyGroup"]); + foreach (JsonNode? folder in folders) + MigrateProfileElement(folder); } - } - } - private void MigrateProfileElement(JToken profileElement) - { - JArray? layerEffects = (JArray?) profileElement["LayerEffects"]?["$values"]; - if (layerEffects != null) - { - foreach (JToken layerEffect in layerEffects) - MigratePropertyGroup(layerEffect["PropertyGroup"]); + if (layers != null) + { + foreach (JsonNode? layer in layers) + { + MigrateProfileElement(layer); + MigratePropertyGroup(layer?["GeneralPropertyGroup"]); + MigratePropertyGroup(layer?["TransformPropertyGroup"]); + MigratePropertyGroup(layer?["LayerBrush"]?["PropertyGroup"]); + } + } } - JToken? displayCondition = profileElement["DisplayCondition"]; - if (displayCondition != null) - MigrateNodeScript(displayCondition["Script"]); - } - - private void MigratePropertyGroup(JToken? propertyGroup) - { - if (propertyGroup == null || !propertyGroup.HasValues) - return; - - JArray? properties = (JArray?) propertyGroup["Properties"]?["$values"]; - JArray? propertyGroups = (JArray?) propertyGroup["PropertyGroups"]?["$values"]; - - if (properties != null) + private void MigrateProfileElement(JsonNode? profileElement) { - foreach (JToken property in properties) - MigrateNodeScript(property["DataBinding"]?["NodeScript"]); + if (profileElement == null) + return; + + JsonArray? layerEffects = profileElement["LayerEffects"]?["$values"]?.AsArray(); + if (layerEffects != null) + { + foreach (JsonNode? layerEffect in layerEffects) + MigratePropertyGroup(layerEffect?["PropertyGroup"]); + } + + JsonNode? displayCondition = profileElement["DisplayCondition"]; + if (displayCondition != null) + MigrateNodeScript(displayCondition["Script"]); } - if (propertyGroups != null) + private void MigratePropertyGroup(JsonNode? propertyGroup) { - foreach (JToken childPropertyGroup in propertyGroups) - MigratePropertyGroup(childPropertyGroup); - } - } + if (propertyGroup == null) + return; - private void MigrateNodeScript(JToken? nodeScript) - { - if (nodeScript == null || !nodeScript.HasValues) - return; + JsonArray? properties = propertyGroup["Properties"]?["$values"]?.AsArray(); + JsonArray? propertyGroups = propertyGroup["PropertyGroups"]?["$values"]?.AsArray(); + if (properties != null) + { + foreach (JsonNode? property in properties) + MigrateNodeScript(property?["DataBinding"]?["NodeScript"]); + } - JArray? nodes = (JArray?) nodeScript["Nodes"]?["$values"]; - if (nodes == null) - return; + if (propertyGroups != null) + { + foreach (JsonNode? childPropertyGroup in propertyGroups) + MigratePropertyGroup(childPropertyGroup); + } + } - foreach (JToken node in nodes) + private void MigrateNodeScript(JsonNode? nodeScript) { - node["Type"] = node["Type"]?.Value()?.Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes"); - node["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78"; + JsonArray? nodes = nodeScript?["Nodes"]?["$values"]?.AsArray(); + if (nodes == null) + return; + + foreach (JsonNode? jsonNode in nodes) + { + if (jsonNode == null) + continue; + JsonObject nodeObject = jsonNode.AsObject(); + nodeObject["Type"] = nodeObject["Type"]?.GetValue().Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes"); + nodeObject["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78"; + } } } -} \ No newline at end of file +} diff --git a/src/Artemis.Storage/Migrations/Profile/M0002NodeProvidersProfileConfig.cs b/src/Artemis.Storage/Migrations/Profile/M0002NodeProvidersProfileConfig.cs index 9c35a961a..8036092dd 100644 --- a/src/Artemis.Storage/Migrations/Profile/M0002NodeProvidersProfileConfig.cs +++ b/src/Artemis.Storage/Migrations/Profile/M0002NodeProvidersProfileConfig.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Artemis.Storage.Migrations.Profile; @@ -12,24 +12,24 @@ internal class M0002NodeProvidersProfileConfig : IProfileMigration public int Version => 2; /// - public void Migrate(JObject configurationJson, JObject profileJson) + public void Migrate(JsonObject configurationJson, JsonObject profileJson) { MigrateNodeScript(configurationJson["ActivationCondition"]); } - - private void MigrateNodeScript(JToken? nodeScript) - { - if (nodeScript == null || !nodeScript.HasValues) - return; - JArray? nodes = (JArray?) nodeScript["Nodes"]?["$values"]; + private void MigrateNodeScript(JsonNode? nodeScript) + { + JsonArray? nodes = nodeScript?["Nodes"]?["$values"]?.AsArray(); if (nodes == null) return; - foreach (JToken node in nodes) + foreach (JsonNode? jsonNode in nodes) { - node["Type"] = node["Type"]?.Value()?.Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes"); - node["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78"; + if (jsonNode == null) + continue; + JsonObject nodeObject = jsonNode.AsObject(); + nodeObject["Type"] = nodeObject["Type"]?.GetValue().Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes"); + nodeObject["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78"; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs b/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs new file mode 100644 index 000000000..b48cf3b1e --- /dev/null +++ b/src/Artemis.Storage/Migrations/Profile/M0003SystemTextJson.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Text.Json.Nodes; + +namespace Artemis.Storage.Migrations.Profile; + +/// +/// Migrates profiles to be deserializable by System.Text.Json, removing type information from the JSON arrays and most objects. +/// +internal class M0003SystemTextJson : IProfileMigration +{ + /// + public int Version => 3; + + /// + public void Migrate(JsonObject configurationJson, JsonObject profileJson) + { + ConvertToSystemTextJson(configurationJson); + ConvertToSystemTextJson(profileJson); + } + + private void ConvertToSystemTextJson(JsonObject jsonObject) + { + FilterType(jsonObject); + + // Recursively convert all JSON arrays from {$type: "xyz", $values: []} to [] + foreach ((string? key, JsonNode? value) in jsonObject.ToDictionary()) + { + if (value is not JsonObject obj) + continue; + + // if there is a $type and a $values, replace the entire node with $values regardless of the value of $type + if (obj["$type"] != null && obj["$values"] != null) + { + JsonArray? values = obj["$values"]?.AsArray(); + if (values != null) + { + obj.Remove("$values"); + jsonObject[key] = values; + foreach (JsonNode? jsonNode in values.ToList()) + { + if (jsonNode is JsonObject childObject) + ConvertToSystemTextJson(childObject); + } + } + + obj.Remove("$type"); + } + else + { + ConvertToSystemTextJson(obj); + } + } + } + + private void FilterType(JsonObject jsonObject) + { + // Replace or remove $type depending on whether there's a matching JsonDerivedType + // This could be done with reflection but that would mean this migration automatically gains new behaviour over time. + JsonNode? type = jsonObject["$type"]; + if (type != null) + { + // Adaption Hints + if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.CategoryAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "Category"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.DeviceAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "Device"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.KeyboardSectionAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "KeyboardSection"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.AdaptionHints.SingleLedAdaptionHintEntity, Artemis.Storage") + jsonObject["$type"] = "SingleLed"; + // Conditions + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.AlwaysOnConditionEntity, Artemis.Storage") + jsonObject["$type"] = "AlwaysOn"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.EventConditionEntity, Artemis.Storage") + jsonObject["$type"] = "Event"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.PlayOnceConditionEntity, Artemis.Storage") + jsonObject["$type"] = "PlayOnce"; + else if (type.GetValue() == "Artemis.Storage.Entities.Profile.Conditions.StaticConditionEntity, Artemis.Storage") + jsonObject["$type"] = "Static"; + else + jsonObject.Remove("$type"); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs index 8e1f44ecf..8036b9d7a 100644 --- a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs @@ -31,12 +31,24 @@ public void Remove(ProfileCategoryEntity profileCategoryEntity) public List GetAll() { - return _repository.Query().ToList(); + List categories = _repository.Query().ToList(); + + // Update all profile versions to the current version, profile migrations don't apply to LiteDB so anything loadable is assumed to be up to date + foreach (ProfileCategoryEntity profileCategoryEntity in categories) + UpdateProfileVersions(profileCategoryEntity); + + return categories; } public ProfileCategoryEntity? Get(Guid id) { - return _repository.FirstOrDefault(p => p.Id == id); + ProfileCategoryEntity? result = _repository.FirstOrDefault(p => p.Id == id); + if (result == null) + return null; + + // Update all profile versions to the current version, profile migrations don't apply to LiteDB so anything loadable is assumed to be up to date + UpdateProfileVersions(result); + return result; } public ProfileCategoryEntity IsUnique(string name, Guid? id) @@ -72,4 +84,10 @@ public void SaveProfileIconStream(ProfileConfigurationEntity profileConfiguratio _profileIcons.Upload(profileConfigurationEntity.FileIconId, profileConfigurationEntity.FileIconId + ".png", stream); } + + private static void UpdateProfileVersions(ProfileCategoryEntity profileCategoryEntity) + { + foreach (ProfileConfigurationEntity profileConfigurationEntity in profileCategoryEntity.ProfileConfigurations) + profileConfigurationEntity.Version = StorageMigrationService.PROFILE_VERSION; + } } \ No newline at end of file diff --git a/src/Artemis.Storage/StorageMigrationService.cs b/src/Artemis.Storage/StorageMigrationService.cs index cf728023b..1d726ac8f 100644 --- a/src/Artemis.Storage/StorageMigrationService.cs +++ b/src/Artemis.Storage/StorageMigrationService.cs @@ -9,6 +9,8 @@ namespace Artemis.Storage; public class StorageMigrationService { + public const int PROFILE_VERSION = 3; + private readonly ILogger _logger; private readonly IList _migrations; private readonly LiteRepository _repository; diff --git a/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs b/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs index 71e06013d..1a7a44c09 100644 --- a/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs +++ b/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs @@ -1,7 +1,8 @@ using System; +using System.Text.Json; +using System.Text.Json.Serialization; using Artemis.Core; using Artemis.UI.Shared.DataModelVisualization; -using Newtonsoft.Json; namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display; @@ -11,16 +12,15 @@ namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display; /// internal class DefaultDataModelDisplayViewModel : DataModelDisplayViewModel { - private readonly JsonSerializerSettings _serializerSettings; + private readonly JsonSerializerOptions _serializerSettings; private string _display; public DefaultDataModelDisplayViewModel() { - _serializerSettings = new JsonSerializerSettings - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - PreserveReferencesHandling = PreserveReferencesHandling.None - }; + _serializerSettings = CoreJson.GetJsonSerializerOptions(); + _serializerSettings.ReferenceHandler = ReferenceHandler.IgnoreCycles; + _serializerSettings.WriteIndented = true; + _display = "null"; } @@ -35,7 +35,7 @@ protected override void OnDisplayValueUpdated() if (DisplayValue is Enum enumDisplayValue) Display = EnumUtilities.HumanizeValue(enumDisplayValue); else if (DisplayValue is not string) - Display = JsonConvert.SerializeObject(DisplayValue, _serializerSettings); + Display = JsonSerializer.Serialize(DisplayValue, _serializerSettings); else Display = DisplayValue?.ToString() ?? "null"; } diff --git a/src/Artemis.UI.Shared/Extensions/ClipboardExtensions.cs b/src/Artemis.UI.Shared/Extensions/ClipboardExtensions.cs index b63cb4faf..e15689a9f 100644 --- a/src/Artemis.UI.Shared/Extensions/ClipboardExtensions.cs +++ b/src/Artemis.UI.Shared/Extensions/ClipboardExtensions.cs @@ -24,6 +24,6 @@ public static class ClipboardExtensions public static async Task GetJsonAsync(this IClipboard clipboard, string format) { byte[]? bytes = (byte[]?) await clipboard.GetDataAsync(format); - return bytes == null ? default : CoreJson.DeserializeObject(Encoding.Unicode.GetString(bytes), true); + return bytes == null ? default : CoreJson.Deserialize(Encoding.Unicode.GetString(bytes).TrimEnd('\0')); } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs index 6e8b9c90d..bc79644cf 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs @@ -32,12 +32,12 @@ public ResetLayerProperty(LayerProperty layerProperty) /// public void Execute() { - string json = CoreJson.SerializeObject(_layerProperty.DefaultValue, true); + string json = CoreJson.Serialize(_layerProperty.DefaultValue); if (_keyframesEnabled) _layerProperty.KeyframesEnabled = false; - _layerProperty.SetCurrentValue(CoreJson.DeserializeObject(json)!); + _layerProperty.SetCurrentValue(CoreJson.Deserialize(json)!); } /// diff --git a/src/Artemis.UI/Extensions/ProfileElementExtensions.cs b/src/Artemis.UI/Extensions/ProfileElementExtensions.cs index 8f811bd14..9dfba30d4 100644 --- a/src/Artemis.UI/Extensions/ProfileElementExtensions.cs +++ b/src/Artemis.UI/Extensions/ProfileElementExtensions.cs @@ -1,10 +1,7 @@ -using System; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using Artemis.Core; -using Artemis.Storage.Entities.Profile; using Artemis.UI.Models; -using Avalonia; +using Artemis.UI.Shared.Extensions; using Avalonia.Input; namespace Artemis.UI.Extensions; @@ -19,7 +16,7 @@ public static class ProfileElementExtensions public static async Task CopyToClipboard(this Folder folder) { DataObject dataObject = new(); - string copy = CoreJson.SerializeObject(new FolderClipboardModel(folder), true); + string copy = CoreJson.Serialize(new FolderClipboardModel(folder)); dataObject.Set(ClipboardDataFormat, copy); await Shared.UI.Clipboard.SetDataObjectAsync(dataObject); } @@ -27,7 +24,7 @@ public static async Task CopyToClipboard(this Folder folder) public static async Task CopyToClipboard(this Layer layer) { DataObject dataObject = new(); - string copy = CoreJson.SerializeObject(layer.LayerEntity, true); + string copy = CoreJson.Serialize(new LayerClipboardModel(layer)); dataObject.Set(ClipboardDataFormat, copy); await Shared.UI.Clipboard.SetDataObjectAsync(dataObject); } @@ -35,18 +32,13 @@ public static async Task CopyToClipboard(this Layer layer) public static async Task PasteChildFromClipboard(this Folder parent) { - byte[]? bytes = (byte[]?) await Shared.UI.Clipboard.GetDataAsync(ClipboardDataFormat); - if (bytes == null!) - return null; - - object? entity = CoreJson.DeserializeObject(Encoding.Unicode.GetString(bytes), true); + IClipboardModel? entity = await Shared.UI.Clipboard.GetJsonAsync(ClipboardDataFormat); switch (entity) { case FolderClipboardModel folderClipboardModel: return folderClipboardModel.Paste(parent.Profile, parent); - case LayerEntity layerEntity: - layerEntity.Id = Guid.NewGuid(); - return new Layer(parent.Profile, parent, layerEntity, true); + case LayerClipboardModel layerClipboardModel: + return layerClipboardModel.Paste(parent); default: return null; } diff --git a/src/Artemis.UI/Models/FolderClipboardModel.cs b/src/Artemis.UI/Models/FolderClipboardModel.cs index 98301d394..a8f4eaedb 100644 --- a/src/Artemis.UI/Models/FolderClipboardModel.cs +++ b/src/Artemis.UI/Models/FolderClipboardModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Artemis.Core; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; @@ -7,7 +8,7 @@ namespace Artemis.UI.Models; -public class FolderClipboardModel +public class FolderClipboardModel: IClipboardModel { public FolderClipboardModel(Folder folder) { @@ -20,7 +21,7 @@ public FolderClipboardModel(Folder folder) Layers.Add(allLayer.LayerEntity); } - // ReSharper disable once UnusedMember.Global - For JSON.NET + [JsonConstructor] public FolderClipboardModel() { FolderEntity = null; diff --git a/src/Artemis.UI/Models/IClipboardModel.cs b/src/Artemis.UI/Models/IClipboardModel.cs new file mode 100644 index 000000000..fc4667951 --- /dev/null +++ b/src/Artemis.UI/Models/IClipboardModel.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace Artemis.UI.Models; + +[JsonDerivedType(typeof(LayerClipboardModel), "ClipboardLayer")] +[JsonDerivedType(typeof(FolderClipboardModel), "ClipboardFolder")] +[JsonDerivedType(typeof(KeyframeClipboardModel), "ClipboardKeyframe")] +[JsonDerivedType(typeof(NodesClipboardModel), "ClipboardNodes")] +public interface IClipboardModel +{ +} \ No newline at end of file diff --git a/src/Artemis.UI/Models/KeyframeClipboardModel.cs b/src/Artemis.UI/Models/KeyframeClipboardModel.cs index e225fe225..0a57f2823 100644 --- a/src/Artemis.UI/Models/KeyframeClipboardModel.cs +++ b/src/Artemis.UI/Models/KeyframeClipboardModel.cs @@ -1,10 +1,10 @@ -using Artemis.Core; +using System.Text.Json.Serialization; +using Artemis.Core; using Artemis.Storage.Entities.Profile; -using Newtonsoft.Json; namespace Artemis.UI.Models; -public class KeyframeClipboardModel +public class KeyframeClipboardModel: IClipboardModel { public const string ClipboardDataFormat = "Artemis.Keyframes"; diff --git a/src/Artemis.UI/Models/LayerClipboardModel.cs b/src/Artemis.UI/Models/LayerClipboardModel.cs new file mode 100644 index 000000000..5578df270 --- /dev/null +++ b/src/Artemis.UI/Models/LayerClipboardModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json.Serialization; +using Artemis.Core; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.UI.Models; + +public class LayerClipboardModel : IClipboardModel +{ + public LayerClipboardModel(Layer layer) + { + Layer = layer.LayerEntity; + } + + [JsonConstructor] + public LayerClipboardModel() + { + } + + public LayerEntity Layer { get; set; } = null!; + + public RenderProfileElement Paste(Folder parent) + { + Layer.Id = Guid.NewGuid(); + return new Layer(parent.Profile, parent, Layer, true); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Models/NodesClipboardModel.cs b/src/Artemis.UI/Models/NodesClipboardModel.cs index 42cd9832f..5aef8b9b0 100644 --- a/src/Artemis.UI/Models/NodesClipboardModel.cs +++ b/src/Artemis.UI/Models/NodesClipboardModel.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using Artemis.Core; using Artemis.Storage.Entities.Profile.Nodes; namespace Artemis.UI.Models; -public class NodesClipboardModel +public class NodesClipboardModel: IClipboardModel { public NodesClipboardModel(NodeScript nodeScript, List nodes) { @@ -18,6 +19,7 @@ public NodesClipboardModel(NodeScript nodeScript, List nodes) Connections = nodeScript.Entity.Connections.Where(e => nodes.Any(n => n.Id == e.SourceNode) && nodes.Any(n => n.Id == e.TargetNode)).ToList(); } + [JsonConstructor] public NodesClipboardModel() { Nodes = new List(); diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs index 26a1e031f..e58a4fe22 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs @@ -1,15 +1,14 @@ +using System.Text.Json; using System.Threading; using Artemis.UI.Extensions; using Artemis.UI.Shared; using Artemis.WebClient.Workshop.Services; -using Newtonsoft.Json; using PropertyChanged.SourceGenerator; namespace Artemis.UI.Screens.Debugger.Workshop; public partial class WorkshopDebugViewModel : ActivatableViewModelBase { - [Notify] private string? _token; [Notify] private bool _emailVerified; [Notify] private string? _claims; @@ -18,12 +17,12 @@ public partial class WorkshopDebugViewModel : ActivatableViewModelBase public WorkshopDebugViewModel(IWorkshopService workshopService, IAuthenticationService authenticationService) { DisplayName = "Workshop"; - + this.WhenActivatedAsync(async _ => { Token = await authenticationService.GetBearer(); EmailVerified = authenticationService.GetIsEmailVerified(); - Claims = JsonConvert.SerializeObject(authenticationService.Claims, Formatting.Indented); + Claims = JsonSerializer.Serialize(authenticationService.Claims, new JsonSerializerOptions {WriteIndented = true}); WorkshopStatus = await workshopService.GetWorkshopStatus(CancellationToken.None); }); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 3faad62e1..0cf1a7ce2 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -14,7 +14,6 @@ using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; -using Newtonsoft.Json; using PropertyChanged.SourceGenerator; using ReactiveUI; diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index df30c8b48..c5cf0fd31 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -6,6 +6,7 @@ using Artemis.Storage.Entities.Profile; using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; +using Artemis.UI.Models; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; @@ -36,11 +37,9 @@ protected override async Task ExecuteDuplicate() { await ProfileEditorService.SaveProfileAsync(); - FolderEntity copy = CoreJson.DeserializeObject(CoreJson.SerializeObject(Folder.FolderEntity, true), true)!; - copy.Id = Guid.NewGuid(); - copy.Name = Folder.Parent.GetNewFolderName(copy.Name + " - copy"); + FolderClipboardModel copy = CoreJson.Deserialize(CoreJson.Serialize(new FolderClipboardModel(Folder)))!; + Folder copied = copy.Paste(Folder.Profile, Folder.Parent); - Folder copied = new(Folder.Profile, Folder.Parent, copy); ProfileEditorService.ExecuteCommand(new AddProfileElement(copied, Folder.Parent, Folder.Order - 1)); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index 83554ac12..81cbcce7c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -37,7 +37,7 @@ protected override async Task ExecuteDuplicate() { await ProfileEditorService.SaveProfileAsync(); - LayerEntity copy = CoreJson.DeserializeObject(CoreJson.SerializeObject(Layer.LayerEntity, true), true)!; + LayerEntity copy = CoreJson.Deserialize(CoreJson.Serialize(Layer.LayerEntity))!; copy.Id = Guid.NewGuid(); copy.Name = Layer.Parent.GetNewFolderName(copy.Name + " - copy"); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs index a0d2b5de6..c568924bc 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs @@ -135,7 +135,7 @@ private async Task ExecuteCopy() else keyframes.AddRange(_profileEditorService.SelectedKeyframes.Select(k => new KeyframeClipboardModel(k))); - string copy = CoreJson.SerializeObject(keyframes, true); + string copy = CoreJson.Serialize(keyframes); DataObject dataObject = new(); dataObject.Set(KeyframeClipboardModel.ClipboardDataFormat, copy); await Shared.UI.Clipboard.SetDataObjectAsync(dataObject); diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index 5b8ad504b..3892fee70 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -2,12 +2,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; -using System.IO.Compression; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; @@ -18,7 +16,6 @@ using Artemis.UI.Shared.Services.Builders; using DynamicData; using DynamicData.Binding; -using Newtonsoft.Json; using PropertyChanged.SourceGenerator; using ReactiveUI; @@ -151,7 +148,7 @@ private async Task ExecuteAddProfile() private async Task ExecuteImportProfile() { string[]? result = await _windowService.CreateOpenFileDialog() - .HavingFilter(f => f.WithExtension("zip").WithExtension("json").WithName("Artemis profile")) + .HavingFilter(f => f.WithExtension("zip").WithName("Artemis profile")) .ShowAsync(); if (result == null) @@ -159,24 +156,8 @@ private async Task ExecuteImportProfile() try { - // Removing this at some point in the future - if (result[0].EndsWith("json")) - { - ProfileConfigurationExportModel? exportModel = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(result[0]), IProfileService.ExportSettings); - if (exportModel == null) - { - await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null); - return; - } - - await using Stream convertedFileStream = await ConvertLegacyExport(exportModel); - await _profileService.ImportProfile(convertedFileStream, ProfileCategory, true, true); - } - else - { - await using FileStream fileStream = File.OpenRead(result[0]); - await _profileService.ImportProfile(fileStream, ProfileCategory, true, true); - } + await using FileStream fileStream = File.OpenRead(result[0]); + await _profileService.ImportProfile(fileStream, ProfileCategory, true, true); } catch (Exception e) { @@ -230,38 +211,4 @@ private void ApplyCategoryOrder(List categories) _profileService.SaveProfileCategory(categories[i]); } } - - private async Task ConvertLegacyExport(ProfileConfigurationExportModel exportModel) - { - MemoryStream archiveStream = new(); - - string configurationJson = JsonConvert.SerializeObject(exportModel.ProfileConfigurationEntity, IProfileService.ExportSettings); - string profileJson = JsonConvert.SerializeObject(exportModel.ProfileEntity, IProfileService.ExportSettings); - - // Create a ZIP archive - using (ZipArchive archive = new(archiveStream, ZipArchiveMode.Create, true)) - { - ZipArchiveEntry configurationEntry = archive.CreateEntry("configuration.json"); - await using (Stream entryStream = configurationEntry.Open()) - { - await entryStream.WriteAsync(Encoding.Default.GetBytes(configurationJson)); - } - - ZipArchiveEntry profileEntry = archive.CreateEntry("profile.json"); - await using (Stream entryStream = profileEntry.Open()) - { - await entryStream.WriteAsync(Encoding.Default.GetBytes(profileJson)); - } - - if (exportModel.ProfileImage != null) - { - ZipArchiveEntry iconEntry = archive.CreateEntry("icon.png"); - await using Stream entryStream = iconEntry.Open(); - await exportModel.ProfileImage.CopyToAsync(entryStream); - } - } - - archiveStream.Seek(0, SeekOrigin.Begin); - return archiveStream; - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 9ffb3b2db..bcf74f587 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -15,6 +15,7 @@ using Artemis.UI.Models; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; @@ -277,18 +278,14 @@ private async Task ExecuteCopySelected() { List nodes = NodeViewModels.Where(vm => vm.IsSelected).Select(vm => vm.Node).Where(n => !n.IsDefaultNode && !n.IsExitNode).ToList(); DataObject dataObject = new(); - string copy = CoreJson.SerializeObject(new NodesClipboardModel(NodeScript, nodes), true); + string copy = CoreJson.Serialize(new NodesClipboardModel(NodeScript, nodes)); dataObject.Set(CLIPBOARD_DATA_FORMAT, copy); await Shared.UI.Clipboard.SetDataObjectAsync(dataObject); } private async Task ExecutePasteSelected() { - byte[]? bytes = (byte[]?) await Shared.UI.Clipboard.GetDataAsync(CLIPBOARD_DATA_FORMAT); - if (bytes == null!) - return; - - NodesClipboardModel? nodesClipboardModel = CoreJson.DeserializeObject(Encoding.Unicode.GetString(bytes), true); + NodesClipboardModel? nodesClipboardModel = await Shared.UI.Clipboard.GetJsonAsync(CLIPBOARD_DATA_FORMAT); if (nodesClipboardModel == null) return; diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs index a44a7e05b..2d8cc45d9 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Plugin/PluginSelectionStepViewModel.cs @@ -55,7 +55,7 @@ private async Task ExecuteBrowse() throw new ArtemisPluginException("Couldn't find a plugin.json in " + files[0]); using StreamReader reader = new(metaDataFileEntry.Open()); - PluginInfo pluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; + PluginInfo pluginInfo = CoreJson.Deserialize(reader.ReadToEnd())!; if (!pluginInfo.Main.EndsWith(".dll")) throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file"); diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs index 334055b3b..38e2c9789 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs @@ -1,10 +1,9 @@ using System.IO.Compression; using System.Net.Http.Headers; +using System.Net.Http.Json; using Artemis.Core; -using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop.Entities; using Artemis.WebClient.Workshop.Exceptions; -using Newtonsoft.Json; using RGB.NET.Layout; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; @@ -70,7 +69,7 @@ public async Task CreateReleaseAsync(long entryId, IEntrySour if (!response.IsSuccessStatusCode) return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); - Release? release = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); + Release? release = await response.Content.ReadFromJsonAsync(cancellationToken); return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response"); } diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs index 50fa0a283..f8c51034c 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/PluginEntryUploadHandler.cs @@ -1,6 +1,6 @@ using System.Net.Http.Headers; +using System.Net.Http.Json; using Artemis.WebClient.Workshop.Entities; -using Newtonsoft.Json; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; @@ -34,7 +34,7 @@ public async Task CreateReleaseAsync(long entryId, IEntrySour if (!response.IsSuccessStatusCode) return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); - Release? release = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); + Release? release = await response.Content.ReadFromJsonAsync(cancellationToken); return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response"); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs index e5a5fa961..012491729 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/ProfileEntryUploadHandler.cs @@ -1,8 +1,7 @@ using System.Net.Http.Headers; +using System.Net.Http.Json; using Artemis.Core.Services; -using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop.Entities; -using Newtonsoft.Json; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; @@ -39,7 +38,7 @@ public async Task CreateReleaseAsync(long entryId, IEntrySour if (!response.IsSuccessStatusCode) return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); - Release? release = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); + Release? release = await response.Content.ReadFromJsonAsync(cancellationToken); return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response"); } } \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 51e95be43..214bbcd9c 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -33,7 +33,6 @@ - @@ -52,6 +51,7 @@ + \ No newline at end of file